Initial commit
This commit is contained in:
24
frontend/lib/gen/notes/v1/empty.pb.dart
Normal file
24
frontend/lib/gen/notes/v1/empty.pb.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
// google.protobuf.Empty — пустое сообщение для RPC без возврата данных.
|
||||
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
class Empty extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
'Empty',
|
||||
package: const $pb.PackageName('google.protobuf'),
|
||||
createEmptyInstance: create,
|
||||
)..hasRequiredFields = false;
|
||||
|
||||
Empty._() : super();
|
||||
factory Empty() => create();
|
||||
static Empty create() => Empty._();
|
||||
|
||||
@override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@override
|
||||
Empty createEmptyInstance() => create();
|
||||
|
||||
@override
|
||||
Empty clone() => Empty()..mergeFromMessage(this);
|
||||
}
|
||||
318
frontend/lib/gen/notes/v1/notes.pb.dart
Normal file
318
frontend/lib/gen/notes/v1/notes.pb.dart
Normal file
@@ -0,0 +1,318 @@
|
||||
// Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
// source: notes/v1/notes.proto
|
||||
//
|
||||
// ЭТО СГЕНЕРИРОВАННЫЙ ФАЙЛ — не редактируй вручную.
|
||||
// Перегенерируй командой: cd proto && buf generate
|
||||
//
|
||||
// Для демо: создан вручную, чтобы проект компилировался.
|
||||
// В реальном проекте — автогенерация из proto.
|
||||
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
/// Note — заметка. Сгенерирована из message Note в proto.
|
||||
/// Тот же тип, что и на Go стороне. Один proto — два языка.
|
||||
class Note extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
'Note',
|
||||
package: const $pb.PackageName('notes.v1'),
|
||||
createEmptyInstance: create,
|
||||
)
|
||||
..aOS(1, 'id')
|
||||
..aOS(2, 'title')
|
||||
..aOS(3, 'content')
|
||||
..aOS(4, 'createdAt')
|
||||
..aOS(5, 'updatedAt')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
Note._() : super();
|
||||
|
||||
factory Note({
|
||||
String? id,
|
||||
String? title,
|
||||
String? content,
|
||||
String? createdAt,
|
||||
String? updatedAt,
|
||||
}) {
|
||||
final result = create();
|
||||
if (id != null) result.id = id;
|
||||
if (title != null) result.title = title;
|
||||
if (content != null) result.content = content;
|
||||
if (createdAt != null) result.createdAt = createdAt;
|
||||
if (updatedAt != null) result.updatedAt = updatedAt;
|
||||
return result;
|
||||
}
|
||||
|
||||
static Note create() => Note._();
|
||||
static $pb.PbList<Note> createRepeated() => $pb.PbList<Note>();
|
||||
|
||||
@override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@override
|
||||
Note createEmptyInstance() => create();
|
||||
|
||||
@override
|
||||
Note clone() => Note()..mergeFromMessage(this);
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
String get id => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set id(String v) => $_setString(0, v);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
String get title => $_getSZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set title(String v) => $_setString(1, v);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
String get content => $_getSZ(2);
|
||||
@$pb.TagNumber(3)
|
||||
set content(String v) => $_setString(2, v);
|
||||
|
||||
@$pb.TagNumber(4)
|
||||
String get createdAt => $_getSZ(3);
|
||||
@$pb.TagNumber(4)
|
||||
set createdAt(String v) => $_setString(3, v);
|
||||
|
||||
@$pb.TagNumber(5)
|
||||
String get updatedAt => $_getSZ(4);
|
||||
@$pb.TagNumber(5)
|
||||
set updatedAt(String v) => $_setString(4, v);
|
||||
}
|
||||
|
||||
class CreateNoteRequest extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
'CreateNoteRequest',
|
||||
package: const $pb.PackageName('notes.v1'),
|
||||
createEmptyInstance: create,
|
||||
)
|
||||
..aOS(1, 'title')
|
||||
..aOS(2, 'content')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
CreateNoteRequest._() : super();
|
||||
|
||||
factory CreateNoteRequest({String? title, String? content}) {
|
||||
final result = create();
|
||||
if (title != null) result.title = title;
|
||||
if (content != null) result.content = content;
|
||||
return result;
|
||||
}
|
||||
|
||||
static CreateNoteRequest create() => CreateNoteRequest._();
|
||||
|
||||
@override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@override
|
||||
CreateNoteRequest createEmptyInstance() => create();
|
||||
|
||||
@override
|
||||
CreateNoteRequest clone() => CreateNoteRequest()..mergeFromMessage(this);
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
String get title => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set title(String v) => $_setString(0, v);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
String get content => $_getSZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set content(String v) => $_setString(1, v);
|
||||
}
|
||||
|
||||
class GetNoteRequest extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
'GetNoteRequest',
|
||||
package: const $pb.PackageName('notes.v1'),
|
||||
createEmptyInstance: create,
|
||||
)
|
||||
..aOS(1, 'id')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
GetNoteRequest._() : super();
|
||||
|
||||
factory GetNoteRequest({String? id}) {
|
||||
final result = create();
|
||||
if (id != null) result.id = id;
|
||||
return result;
|
||||
}
|
||||
|
||||
static GetNoteRequest create() => GetNoteRequest._();
|
||||
|
||||
@override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@override
|
||||
GetNoteRequest createEmptyInstance() => create();
|
||||
|
||||
@override
|
||||
GetNoteRequest clone() => GetNoteRequest()..mergeFromMessage(this);
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
String get id => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set id(String v) => $_setString(0, v);
|
||||
}
|
||||
|
||||
class UpdateNoteRequest extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
'UpdateNoteRequest',
|
||||
package: const $pb.PackageName('notes.v1'),
|
||||
createEmptyInstance: create,
|
||||
)
|
||||
..aOS(1, 'id')
|
||||
..aOS(2, 'title')
|
||||
..aOS(3, 'content')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
UpdateNoteRequest._() : super();
|
||||
|
||||
factory UpdateNoteRequest({String? id, String? title, String? content}) {
|
||||
final result = create();
|
||||
if (id != null) result.id = id;
|
||||
if (title != null) result.title = title;
|
||||
if (content != null) result.content = content;
|
||||
return result;
|
||||
}
|
||||
|
||||
static UpdateNoteRequest create() => UpdateNoteRequest._();
|
||||
|
||||
@override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@override
|
||||
UpdateNoteRequest createEmptyInstance() => create();
|
||||
|
||||
@override
|
||||
UpdateNoteRequest clone() => UpdateNoteRequest()..mergeFromMessage(this);
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
String get id => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set id(String v) => $_setString(0, v);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
String get title => $_getSZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set title(String v) => $_setString(1, v);
|
||||
|
||||
@$pb.TagNumber(3)
|
||||
String get content => $_getSZ(2);
|
||||
@$pb.TagNumber(3)
|
||||
set content(String v) => $_setString(2, v);
|
||||
}
|
||||
|
||||
class DeleteNoteRequest extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
'DeleteNoteRequest',
|
||||
package: const $pb.PackageName('notes.v1'),
|
||||
createEmptyInstance: create,
|
||||
)
|
||||
..aOS(1, 'id')
|
||||
..hasRequiredFields = false;
|
||||
|
||||
DeleteNoteRequest._() : super();
|
||||
|
||||
factory DeleteNoteRequest({String? id}) {
|
||||
final result = create();
|
||||
if (id != null) result.id = id;
|
||||
return result;
|
||||
}
|
||||
|
||||
static DeleteNoteRequest create() => DeleteNoteRequest._();
|
||||
|
||||
@override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@override
|
||||
DeleteNoteRequest createEmptyInstance() => create();
|
||||
|
||||
@override
|
||||
DeleteNoteRequest clone() => DeleteNoteRequest()..mergeFromMessage(this);
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
String get id => $_getSZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set id(String v) => $_setString(0, v);
|
||||
}
|
||||
|
||||
class ListNotesRequest extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
'ListNotesRequest',
|
||||
package: const $pb.PackageName('notes.v1'),
|
||||
createEmptyInstance: create,
|
||||
)
|
||||
..a<int>(1, 'page', $pb.PbFieldType.O3)
|
||||
..a<int>(2, 'pageSize', $pb.PbFieldType.O3)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
ListNotesRequest._() : super();
|
||||
|
||||
factory ListNotesRequest({int? page, int? pageSize}) {
|
||||
final result = create();
|
||||
if (page != null) result.page = page;
|
||||
if (pageSize != null) result.pageSize = pageSize;
|
||||
return result;
|
||||
}
|
||||
|
||||
static ListNotesRequest create() => ListNotesRequest._();
|
||||
|
||||
@override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@override
|
||||
ListNotesRequest createEmptyInstance() => create();
|
||||
|
||||
@override
|
||||
ListNotesRequest clone() => ListNotesRequest()..mergeFromMessage(this);
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
int get page => $_getIZ(0);
|
||||
@$pb.TagNumber(1)
|
||||
set page(int v) => $_setSignedInt32(0, v);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
int get pageSize => $_getIZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set pageSize(int v) => $_setSignedInt32(1, v);
|
||||
}
|
||||
|
||||
class ListNotesResponse extends $pb.GeneratedMessage {
|
||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||
'ListNotesResponse',
|
||||
package: const $pb.PackageName('notes.v1'),
|
||||
createEmptyInstance: create,
|
||||
)
|
||||
..pc<Note>(1, 'notes', $pb.PbFieldType.PM, subBuilder: Note.create)
|
||||
..a<int>(2, 'totalCount', $pb.PbFieldType.O3)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
ListNotesResponse._() : super();
|
||||
|
||||
factory ListNotesResponse({List<Note>? notes, int? totalCount}) {
|
||||
final result = create();
|
||||
if (notes != null) result.notes.addAll(notes);
|
||||
if (totalCount != null) result.totalCount = totalCount;
|
||||
return result;
|
||||
}
|
||||
|
||||
static ListNotesResponse create() => ListNotesResponse._();
|
||||
|
||||
@override
|
||||
$pb.BuilderInfo get info_ => _i;
|
||||
|
||||
@override
|
||||
ListNotesResponse createEmptyInstance() => create();
|
||||
|
||||
@override
|
||||
ListNotesResponse clone() => ListNotesResponse()..mergeFromMessage(this);
|
||||
|
||||
@$pb.TagNumber(1)
|
||||
$pb.PbList<Note> get notes => $_getList(0);
|
||||
|
||||
@$pb.TagNumber(2)
|
||||
int get totalCount => $_getIZ(1);
|
||||
@$pb.TagNumber(2)
|
||||
set totalCount(int v) => $_setSignedInt32(1, v);
|
||||
}
|
||||
91
frontend/lib/gen/notes/v1/notes.pbgrpc.dart
Normal file
91
frontend/lib/gen/notes/v1/notes.pbgrpc.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
// Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
// source: notes/v1/notes.proto
|
||||
//
|
||||
// ЭТО СГЕНЕРИРОВАННЫЙ ФАЙЛ — gRPC клиентский код.
|
||||
// В реальном проекте генерируется из proto.
|
||||
//
|
||||
// NoteServiceClient — типизированный клиент.
|
||||
// Dart знает сигнатуры всех RPC методов.
|
||||
// Если proto изменится и ты перегенерируешь —
|
||||
// все вызовы с неверными типами дадут ошибку компиляции.
|
||||
|
||||
import 'package:grpc/grpc.dart' as $grpc;
|
||||
|
||||
import 'empty.pb.dart' as $empty;
|
||||
import 'notes.pb.dart' as $0;
|
||||
|
||||
/// Типизированный gRPC клиент — сгенерирован из service NoteService.
|
||||
/// Каждый метод соответствует rpc из proto файла.
|
||||
class NoteServiceClient extends $grpc.Client {
|
||||
static final _$createNote =
|
||||
$grpc.ClientMethod<$0.CreateNoteRequest, $0.Note>(
|
||||
'/notes.v1.NoteService/CreateNote',
|
||||
($0.CreateNoteRequest value) => value.writeToBuffer(),
|
||||
(List<int> value) => $0.Note()..mergeFromBuffer(value),
|
||||
);
|
||||
|
||||
static final _$getNote = $grpc.ClientMethod<$0.GetNoteRequest, $0.Note>(
|
||||
'/notes.v1.NoteService/GetNote',
|
||||
($0.GetNoteRequest value) => value.writeToBuffer(),
|
||||
(List<int> value) => $0.Note()..mergeFromBuffer(value),
|
||||
);
|
||||
|
||||
static final _$updateNote =
|
||||
$grpc.ClientMethod<$0.UpdateNoteRequest, $0.Note>(
|
||||
'/notes.v1.NoteService/UpdateNote',
|
||||
($0.UpdateNoteRequest value) => value.writeToBuffer(),
|
||||
(List<int> value) => $0.Note()..mergeFromBuffer(value),
|
||||
);
|
||||
|
||||
static final _$deleteNote =
|
||||
$grpc.ClientMethod<$0.DeleteNoteRequest, $empty.Empty>(
|
||||
'/notes.v1.NoteService/DeleteNote',
|
||||
($0.DeleteNoteRequest value) => value.writeToBuffer(),
|
||||
(List<int> value) => $empty.Empty()..mergeFromBuffer(value),
|
||||
);
|
||||
|
||||
static final _$listNotes =
|
||||
$grpc.ClientMethod<$0.ListNotesRequest, $0.ListNotesResponse>(
|
||||
'/notes.v1.NoteService/ListNotes',
|
||||
($0.ListNotesRequest value) => value.writeToBuffer(),
|
||||
(List<int> value) => $0.ListNotesResponse()..mergeFromBuffer(value),
|
||||
);
|
||||
|
||||
NoteServiceClient(
|
||||
super.channel, {
|
||||
super.options,
|
||||
super.interceptors,
|
||||
});
|
||||
|
||||
/// Создать заметку
|
||||
$grpc.ResponseFuture<$0.Note> createNote($0.CreateNoteRequest request,
|
||||
{$grpc.CallOptions? options}) {
|
||||
return $createUnaryCall(_$createNote, request, options: options);
|
||||
}
|
||||
|
||||
/// Получить заметку по ID
|
||||
$grpc.ResponseFuture<$0.Note> getNote($0.GetNoteRequest request,
|
||||
{$grpc.CallOptions? options}) {
|
||||
return $createUnaryCall(_$getNote, request, options: options);
|
||||
}
|
||||
|
||||
/// Обновить заметку
|
||||
$grpc.ResponseFuture<$0.Note> updateNote($0.UpdateNoteRequest request,
|
||||
{$grpc.CallOptions? options}) {
|
||||
return $createUnaryCall(_$updateNote, request, options: options);
|
||||
}
|
||||
|
||||
/// Удалить заметку
|
||||
$grpc.ResponseFuture<$empty.Empty> deleteNote(
|
||||
$0.DeleteNoteRequest request,
|
||||
{$grpc.CallOptions? options}) {
|
||||
return $createUnaryCall(_$deleteNote, request, options: options);
|
||||
}
|
||||
|
||||
/// Получить список заметок
|
||||
$grpc.ResponseFuture<$0.ListNotesResponse> listNotes(
|
||||
$0.ListNotesRequest request,
|
||||
{$grpc.CallOptions? options}) {
|
||||
return $createUnaryCall(_$listNotes, request, options: options);
|
||||
}
|
||||
}
|
||||
24
frontend/lib/main.dart
Normal file
24
frontend/lib/main.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'screens/notes_screen.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const NotesApp());
|
||||
}
|
||||
|
||||
class NotesApp extends StatelessWidget {
|
||||
const NotesApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Notes — Flutter + Go gRPC',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const NotesScreen(),
|
||||
);
|
||||
}
|
||||
}
|
||||
202
frontend/lib/screens/notes_screen.dart
Normal file
202
frontend/lib/screens/notes_screen.dart
Normal file
@@ -0,0 +1,202 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:grpc/grpc.dart';
|
||||
|
||||
import '../gen/notes/v1/notes.pb.dart';
|
||||
import '../services/grpc_client.dart';
|
||||
|
||||
class NotesScreen extends StatefulWidget {
|
||||
const NotesScreen({super.key});
|
||||
|
||||
@override
|
||||
State<NotesScreen> createState() => _NotesScreenState();
|
||||
}
|
||||
|
||||
class _NotesScreenState extends State<NotesScreen> {
|
||||
final _client = GrpcClient.instance.noteService;
|
||||
List<Note> _notes = [];
|
||||
bool _loading = true;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadNotes();
|
||||
}
|
||||
|
||||
Future<void> _loadNotes() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
// Типизированный вызов — ListNotesRequest и ListNotesResponse
|
||||
// сгенерированы из proto. Если на Go стороне изменится
|
||||
// сигнатура, после перегенерации этот код не скомпилируется.
|
||||
final response = await _client.listNotes(
|
||||
ListNotesRequest(page: 1, pageSize: 50),
|
||||
);
|
||||
setState(() {
|
||||
_notes = response.notes;
|
||||
_loading = false;
|
||||
});
|
||||
} on GrpcError catch (e) {
|
||||
setState(() {
|
||||
_error = 'gRPC error: ${e.message}';
|
||||
_loading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = 'Connection error: $e';
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _createNote() async {
|
||||
final titleController = TextEditingController();
|
||||
final contentController = TextEditingController();
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('New Note'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: titleController,
|
||||
decoration: const InputDecoration(labelText: 'Title'),
|
||||
autofocus: true,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: contentController,
|
||||
decoration: const InputDecoration(labelText: 'Content'),
|
||||
maxLines: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text('Create'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
try {
|
||||
// Типизированный вызов CreateNote.
|
||||
// CreateNoteRequest точно соответствует proto определению.
|
||||
await _client.createNote(
|
||||
CreateNoteRequest(
|
||||
title: titleController.text,
|
||||
content: contentController.text,
|
||||
),
|
||||
);
|
||||
_loadNotes();
|
||||
} on GrpcError catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: ${e.message}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteNote(Note note) async {
|
||||
try {
|
||||
await _client.deleteNote(DeleteNoteRequest(id: note.id));
|
||||
_loadNotes();
|
||||
} on GrpcError catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error: ${e.message}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Notes (Flutter + Go gRPC)'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _loadNotes,
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _createNote,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: _buildBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (_loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(_error!, style: const TextStyle(color: Colors.red)),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Make sure Go backend is running:\n'
|
||||
'cd backend && go run main.go',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
onPressed: _loadNotes,
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_notes.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('No notes yet. Tap + to create one.'),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: _notes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final note = _notes[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: ListTile(
|
||||
title: Text(note.title),
|
||||
subtitle: Text(
|
||||
note.content,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
onPressed: () => _deleteNote(note),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
37
frontend/lib/services/grpc_client.dart
Normal file
37
frontend/lib/services/grpc_client.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:grpc/grpc.dart';
|
||||
|
||||
import '../gen/notes/v1/notes.pbgrpc.dart';
|
||||
|
||||
/// Singleton gRPC клиент.
|
||||
/// Подключается к Go бекенду на localhost:50051.
|
||||
class GrpcClient {
|
||||
static GrpcClient? _instance;
|
||||
late final ClientChannel _channel;
|
||||
late final NoteServiceClient noteService;
|
||||
|
||||
GrpcClient._() {
|
||||
_channel = ClientChannel(
|
||||
'localhost',
|
||||
port: 50051,
|
||||
options: const ChannelOptions(
|
||||
credentials: ChannelCredentials.insecure(),
|
||||
),
|
||||
);
|
||||
|
||||
// noteService — типизированный клиент.
|
||||
// Все методы (createNote, listNotes и т.д.)
|
||||
// сгенерированы из proto и принимают/возвращают
|
||||
// типизированные объекты. Невозможно передать
|
||||
// неверный тип — ошибка компиляции Dart.
|
||||
noteService = NoteServiceClient(_channel);
|
||||
}
|
||||
|
||||
static GrpcClient get instance {
|
||||
_instance ??= GrpcClient._();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Future<void> shutdown() async {
|
||||
await _channel.shutdown();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user