Matthias Nott
2026-03-23 07ad99d7c4f8c52930442a34d316e634435bd75a
feat: attach button with camera, gallery, and file picker
6 files modified
changed files
ios/Podfile.lock patch | view | blame | history
lib/screens/chat_screen.dart patch | view | blame | history
lib/widgets/command_bar.dart patch | view | blame | history
macos/Flutter/GeneratedPluginRegistrant.swift patch | view | blame | history
pubspec.lock patch | view | blame | history
pubspec.yaml patch | view | blame | history
ios/Podfile.lock
....@@ -4,6 +4,40 @@
44 - FlutterMacOS
55 - device_info_plus (0.0.1):
66 - Flutter
7
+ - DKImagePickerController/Core (4.3.9):
8
+ - DKImagePickerController/ImageDataManager
9
+ - DKImagePickerController/Resource
10
+ - DKImagePickerController/ImageDataManager (4.3.9)
11
+ - DKImagePickerController/PhotoGallery (4.3.9):
12
+ - DKImagePickerController/Core
13
+ - DKPhotoGallery
14
+ - DKImagePickerController/Resource (4.3.9)
15
+ - DKPhotoGallery (0.0.19):
16
+ - DKPhotoGallery/Core (= 0.0.19)
17
+ - DKPhotoGallery/Model (= 0.0.19)
18
+ - DKPhotoGallery/Preview (= 0.0.19)
19
+ - DKPhotoGallery/Resource (= 0.0.19)
20
+ - SDWebImage
21
+ - SwiftyGif
22
+ - DKPhotoGallery/Core (0.0.19):
23
+ - DKPhotoGallery/Model
24
+ - DKPhotoGallery/Preview
25
+ - SDWebImage
26
+ - SwiftyGif
27
+ - DKPhotoGallery/Model (0.0.19):
28
+ - SDWebImage
29
+ - SwiftyGif
30
+ - DKPhotoGallery/Preview (0.0.19):
31
+ - DKPhotoGallery/Model
32
+ - DKPhotoGallery/Resource
33
+ - SDWebImage
34
+ - SwiftyGif
35
+ - DKPhotoGallery/Resource (0.0.19):
36
+ - SDWebImage
37
+ - SwiftyGif
38
+ - file_picker (0.0.1):
39
+ - DKImagePickerController/PhotoGallery
40
+ - Flutter
741 - Flutter (1.0.0)
842 - flutter_secure_storage (6.0.0):
943 - Flutter
....@@ -15,11 +49,15 @@
1549 - Flutter
1650 - record_ios (1.2.0):
1751 - Flutter
52
+ - SDWebImage (5.21.7):
53
+ - SDWebImage/Core (= 5.21.7)
54
+ - SDWebImage/Core (5.21.7)
1855 - share_plus (0.0.1):
1956 - Flutter
2057 - shared_preferences_foundation (0.0.1):
2158 - Flutter
2259 - FlutterMacOS
60
+ - SwiftyGif (5.4.5)
2361 - vibration (1.7.5):
2462 - Flutter
2563 - wakelock_plus (0.0.1):
....@@ -28,6 +66,7 @@
2866 DEPENDENCIES:
2967 - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
3068 - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
69
+ - file_picker (from `.symlinks/plugins/file_picker/ios`)
3170 - Flutter (from `Flutter`)
3271 - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
3372 - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
....@@ -39,11 +78,20 @@
3978 - vibration (from `.symlinks/plugins/vibration/ios`)
4079 - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
4180
81
+SPEC REPOS:
82
+ trunk:
83
+ - DKImagePickerController
84
+ - DKPhotoGallery
85
+ - SDWebImage
86
+ - SwiftyGif
87
+
4288 EXTERNAL SOURCES:
4389 audioplayers_darwin:
4490 :path: ".symlinks/plugins/audioplayers_darwin/darwin"
4591 device_info_plus:
4692 :path: ".symlinks/plugins/device_info_plus/ios"
93
+ file_picker:
94
+ :path: ".symlinks/plugins/file_picker/ios"
4795 Flutter:
4896 :path: Flutter
4997 flutter_secure_storage:
....@@ -68,14 +116,19 @@
68116 SPEC CHECKSUMS:
69117 audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
70118 device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
119
+ DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
120
+ DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
121
+ file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
71122 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
72123 flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
73124 image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
74125 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
75126 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
76127 record_ios: 412daca2350b228e698fffcd08f1f94ceb1e3844
128
+ SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
77129 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
78130 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
131
+ SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
79132 vibration: 8e2f50fc35bb736f9eecb7dd9f7047fbb6a6e888
80133 wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
81134
lib/screens/chat_screen.dart
....@@ -7,6 +7,7 @@
77 import 'package:flutter_riverpod/flutter_riverpod.dart';
88 import 'package:go_router/go_router.dart';
99 import 'package:image_picker/image_picker.dart';
10
+import 'package:file_picker/file_picker.dart';
1011 import 'package:shared_preferences/shared_preferences.dart';
1112
1213 import '../models/message.dart';
....@@ -663,6 +664,82 @@
663664 }
664665 }
665666
667
+ Future<void> _pickFiles(String? targetSessionId) async {
668
+ final result = await FilePicker.platform.pickFiles(
669
+ allowMultiple: true,
670
+ type: FileType.any,
671
+ );
672
+ if (result == null || result.files.isEmpty) return;
673
+
674
+ for (final file in result.files) {
675
+ if (file.path == null) continue;
676
+ final bytes = await File(file.path!).readAsBytes();
677
+ final b64 = base64Encode(bytes);
678
+ final mimeType = _guessMimeType(file.name);
679
+ final isImage = mimeType.startsWith('image/');
680
+
681
+ if (isImage) {
682
+ final message = Message.image(
683
+ role: MessageRole.user,
684
+ imageBase64: b64,
685
+ content: file.name,
686
+ status: MessageStatus.sent,
687
+ );
688
+ ref.read(messagesProvider.notifier).addMessage(message);
689
+ _ws?.send({
690
+ 'type': 'image',
691
+ 'imageBase64': b64,
692
+ 'mimeType': mimeType,
693
+ 'caption': file.name,
694
+ 'sessionId': targetSessionId,
695
+ });
696
+ } else {
697
+ // Non-image file: send as text with file info, save file to temp for session
698
+ final tmpDir = await getTemporaryDirectory();
699
+ final tmpPath = '${tmpDir.path}/${file.name}';
700
+ await File(tmpPath).writeAsBytes(bytes);
701
+
702
+ final message = Message.text(
703
+ role: MessageRole.user,
704
+ content: '📎 ${file.name} (${_formatSize(bytes.length)})',
705
+ status: MessageStatus.sent,
706
+ );
707
+ ref.read(messagesProvider.notifier).addMessage(message);
708
+ // Send file as base64 with metadata
709
+ _ws?.send({
710
+ 'type': 'file',
711
+ 'fileBase64': b64,
712
+ 'fileName': file.name,
713
+ 'mimeType': mimeType,
714
+ 'fileSize': bytes.length,
715
+ 'sessionId': targetSessionId,
716
+ });
717
+ }
718
+ }
719
+ _scrollToBottom();
720
+ }
721
+
722
+ String _guessMimeType(String name) {
723
+ final ext = name.split('.').last.toLowerCase();
724
+ const map = {
725
+ 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png',
726
+ 'gif': 'image/gif', 'webp': 'image/webp', 'heic': 'image/heic',
727
+ 'pdf': 'application/pdf', 'doc': 'application/msword',
728
+ 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
729
+ 'xls': 'application/vnd.ms-excel',
730
+ 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
731
+ 'txt': 'text/plain', 'csv': 'text/csv', 'json': 'application/json',
732
+ 'zip': 'application/zip', 'mp3': 'audio/mpeg', 'mp4': 'video/mp4',
733
+ };
734
+ return map[ext] ?? 'application/octet-stream';
735
+ }
736
+
737
+ String _formatSize(int bytes) {
738
+ if (bytes < 1024) return '$bytes B';
739
+ if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
740
+ return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
741
+ }
742
+
666743 void _requestScreenshot() {
667744 _screenshotForChat = true;
668745 _sendCommand('screenshot', {'sessionId': ref.read(activeSessionIdProvider)});
....@@ -683,15 +760,59 @@
683760 }
684761
685762 Future<void> _pickPhoto() async {
686
- // Capture session ID now — before any async gaps (dialog, encoding)
687763 final targetSessionId = ref.read(activeSessionIdProvider);
688764
689
- final picker = ImagePicker();
690
- final images = await picker.pickMultiImage(
691
- maxWidth: 1920,
692
- maxHeight: 1080,
693
- imageQuality: 85,
765
+ // Show picker options
766
+ final source = await showModalBottomSheet<String>(
767
+ context: context,
768
+ builder: (ctx) => SafeArea(
769
+ child: Column(
770
+ mainAxisSize: MainAxisSize.min,
771
+ children: [
772
+ ListTile(
773
+ leading: const Icon(Icons.camera_alt),
774
+ title: const Text('Take Photo'),
775
+ onTap: () => Navigator.pop(ctx, 'camera'),
776
+ ),
777
+ ListTile(
778
+ leading: const Icon(Icons.photo_library),
779
+ title: const Text('Photo Library'),
780
+ onTap: () => Navigator.pop(ctx, 'gallery'),
781
+ ),
782
+ ListTile(
783
+ leading: const Icon(Icons.attach_file),
784
+ title: const Text('Files'),
785
+ onTap: () => Navigator.pop(ctx, 'files'),
786
+ ),
787
+ ],
788
+ ),
789
+ ),
694790 );
791
+ if (source == null) return;
792
+
793
+ if (source == 'files') {
794
+ await _pickFiles(targetSessionId);
795
+ return;
796
+ }
797
+
798
+ List<XFile> images;
799
+ final picker = ImagePicker();
800
+ if (source == 'camera') {
801
+ final photo = await picker.pickImage(
802
+ source: ImageSource.camera,
803
+ maxWidth: 1920,
804
+ maxHeight: 1080,
805
+ imageQuality: 85,
806
+ );
807
+ if (photo == null) return;
808
+ images = [photo];
809
+ } else {
810
+ images = await picker.pickMultiImage(
811
+ maxWidth: 1920,
812
+ maxHeight: 1080,
813
+ imageQuality: 85,
814
+ );
815
+ }
695816
696817 if (images.isEmpty) return;
697818
lib/widgets/command_bar.dart
....@@ -45,7 +45,7 @@
4545 ),
4646 _CommandButton(
4747 icon: Icons.attach_file,
48
- label: 'Photo',
48
+ label: 'Attach',
4949 onTap: onPhoto,
5050 ),
5151 _CommandButton(
macos/Flutter/GeneratedPluginRegistrant.swift
....@@ -7,6 +7,7 @@
77
88 import audioplayers_darwin
99 import device_info_plus
10
+import file_picker
1011 import file_selector_macos
1112 import flutter_secure_storage_macos
1213 import package_info_plus
....@@ -18,6 +19,7 @@
1819 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
1920 AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
2021 DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
22
+ FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
2123 FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
2224 FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
2325 FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
pubspec.lock
....@@ -193,6 +193,14 @@
193193 url: "https://pub.dev"
194194 source: hosted
195195 version: "7.0.1"
196
+ file_picker:
197
+ dependency: "direct main"
198
+ description:
199
+ name: file_picker
200
+ sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343"
201
+ url: "https://pub.dev"
202
+ source: hosted
203
+ version: "10.3.10"
196204 file_selector_linux:
197205 dependency: transitive
198206 description:
pubspec.yaml
....@@ -29,6 +29,7 @@
2929 mqtt_client: ^10.6.0
3030 uuid: ^4.5.1
3131 collection: ^1.19.1
32
+ file_picker: ^10.3.10
3233
3334 dev_dependencies:
3435 flutter_test: