diff --git a/flutter_app/lib/dialog.dart b/flutter_app/lib/dialog.dart index acb7447b4..8408ef9ae 100644 --- a/flutter_app/lib/dialog.dart +++ b/flutter_app/lib/dialog.dart @@ -18,6 +18,7 @@ class _Dialog extends StatefulWidget { class _DialogState extends State<_Dialog> { late final _name = TextEditingController(text: widget.taskToEdit?.title); + late final _attachmentNote = TextEditingController(); late var _done = widget.taskToEdit?.done ?? false; @override @@ -30,6 +31,7 @@ class _DialogState extends State<_Dialog> { mainAxisSize: MainAxisSize.min, children: [ _textInput(_name, "Name"), + _textInput(_attachmentNote, "Attachment Note (optional)"), _doneSwitch, ], ), @@ -45,6 +47,10 @@ class _DialogState extends State<_Dialog> { title: _name.text, done: _done, deleted: false, + // Pass the attachment note text if provided + attachment: _attachmentNote.text.isNotEmpty + ? {"note": _attachmentNote.text} + : null, ); Navigator.of(context).pop(task); }, diff --git a/flutter_app/lib/main.dart b/flutter_app/lib/main.dart index 4f2b1b8dd..b0a5c4d8a 100644 --- a/flutter_app/lib/main.dart +++ b/flutter_app/lib/main.dart @@ -100,10 +100,24 @@ class _DittoExampleState extends State { final task = await showAddTaskDialog(context); if (task == null) return; + Map taskJson = task.toJson(); + + // If the task has an attachment note, create a Ditto attachment + if (task.attachment != null && task.attachment!['note'] != null) { + final noteText = task.attachment!['note'] as String; + final noteBytes = Uint8List.fromList(noteText.codeUnits); + + // Create attachment from the note bytes + final attachment = await _ditto!.store.newAttachment(noteBytes); + + // Replace the placeholder with the actual attachment token + taskJson['attachment'] = attachment; + } + // https://docs.ditto.live/sdk/latest/crud/create await _ditto!.store.execute( "INSERT INTO tasks DOCUMENTS (:task)", - arguments: {"task": task.toJson()}, + arguments: {"task": taskJson}, ); } @@ -112,6 +126,50 @@ class _DittoExampleState extends State { await _ditto!.store.execute("EVICT FROM tasks WHERE true"); } + Future _showAttachment(Map attachmentToken) async { + // Show loading dialog + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const Center(child: CircularProgressIndicator()), + ); + + String? attachmentContent; + + // Fetch the attachment using the Ditto attachment API + _ditto!.store.fetchAttachment( + attachmentToken, + (event) async { + if (event is AttachmentFetchEventCompleted) { + // Get the attachment data + final bytes = await event.attachment.data; + // Convert bytes back to string + attachmentContent = String.fromCharCodes(bytes); + + // Close loading dialog + if (mounted) Navigator.of(context).pop(); + + // Show the attachment content in a dialog + if (mounted) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Attachment Content"), + content: Text(attachmentContent ?? "No content"), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("Close"), + ), + ], + ), + ); + } + } + }, + ); + } + @override Widget build(BuildContext context) { if (_ditto == null) return _loading; @@ -207,6 +265,20 @@ class _DittoExampleState extends State { secondaryBackground: _dismissibleBackground(false), child: CheckboxListTile( title: Text(task.title), + subtitle: task.attachment != null + ? Row( + children: [ + const Icon(Icons.attachment, size: 16), + const SizedBox(width: 4), + Expanded( + child: TextButton( + onPressed: () => _showAttachment(task.attachment!), + child: const Text("View Attachment"), + ), + ), + ], + ) + : null, value: task.done, onChanged: (value) => _ditto!.store.execute( "UPDATE tasks SET done = $value WHERE _id = '${task.id}'", diff --git a/flutter_app/lib/task.dart b/flutter_app/lib/task.dart index ddd83a9ed..2d3c86255 100644 --- a/flutter_app/lib/task.dart +++ b/flutter_app/lib/task.dart @@ -9,12 +9,15 @@ class Task { final String title; final bool done; final bool deleted; + @JsonKey(includeIfNull: false) + final Map? attachment; const Task({ this.id, required this.title, required this.done, required this.deleted, + this.attachment, }); factory Task.fromJson(Map json) => _$TaskFromJson(json); diff --git a/flutter_app/lib/task.g.dart b/flutter_app/lib/task.g.dart index eec95d843..dd211e648 100644 --- a/flutter_app/lib/task.g.dart +++ b/flutter_app/lib/task.g.dart @@ -11,6 +11,7 @@ Task _$TaskFromJson(Map json) => Task( title: json['title'] as String, done: json['done'] as bool, deleted: json['deleted'] as bool, + attachment: json['attachment'] as Map?, ); Map _$TaskToJson(Task instance) { @@ -26,5 +27,6 @@ Map _$TaskToJson(Task instance) { val['title'] = instance.title; val['done'] = instance.done; val['deleted'] = instance.deleted; + writeNotNull('attachment', instance.attachment); return val; } diff --git a/flutter_app/macos/Runner/DebugProfile.entitlements b/flutter_app/macos/Runner/DebugProfile.entitlements index dddb8a30c..08c3ab17c 100644 --- a/flutter_app/macos/Runner/DebugProfile.entitlements +++ b/flutter_app/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.network.client + diff --git a/flutter_app/macos/Runner/Info.plist b/flutter_app/macos/Runner/Info.plist index 4789daa6a..902aa9936 100644 --- a/flutter_app/macos/Runner/Info.plist +++ b/flutter_app/macos/Runner/Info.plist @@ -28,5 +28,13 @@ MainMenu NSPrincipalClass NSApplication + NSBluetoothAlwaysUsageDescription + Uses Bluetooth to connect and sync with nearby devices. + NSLocalNetworkUsageDescription + Uses WiFi to connect and sync with nearby devices. + NSBonjourServices + + _http-alt._tcp. + diff --git a/flutter_app/macos/Runner/Release.entitlements b/flutter_app/macos/Runner/Release.entitlements index 852fa1a47..7a2230dc3 100644 --- a/flutter_app/macos/Runner/Release.entitlements +++ b/flutter_app/macos/Runner/Release.entitlements @@ -4,5 +4,9 @@ com.apple.security.app-sandbox + com.apple.security.network.client + + com.apple.security.network.server +