ios/Podfile.lock
.. .. @@ -4,6 +4,40 @@ 4 4 - FlutterMacOS 5 5 - device_info_plus (0.0.1): 6 6 - Flutter 7 + - DKImagePickerController/Core (4.3.9):8 + - DKImagePickerController/ImageDataManager9 + - DKImagePickerController/Resource10 + - DKImagePickerController/ImageDataManager (4.3.9)11 + - DKImagePickerController/PhotoGallery (4.3.9):12 + - DKImagePickerController/Core13 + - DKPhotoGallery14 + - 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 + - SDWebImage21 + - SwiftyGif22 + - DKPhotoGallery/Core (0.0.19):23 + - DKPhotoGallery/Model24 + - DKPhotoGallery/Preview25 + - SDWebImage26 + - SwiftyGif27 + - DKPhotoGallery/Model (0.0.19):28 + - SDWebImage29 + - SwiftyGif30 + - DKPhotoGallery/Preview (0.0.19):31 + - DKPhotoGallery/Model32 + - DKPhotoGallery/Resource33 + - SDWebImage34 + - SwiftyGif35 + - DKPhotoGallery/Resource (0.0.19):36 + - SDWebImage37 + - SwiftyGif38 + - file_picker (0.0.1):39 + - DKImagePickerController/PhotoGallery40 + - Flutter7 41 - Flutter (1.0.0) 8 42 - flutter_secure_storage (6.0.0): 9 43 - Flutter .. .. @@ -15,11 +49,15 @@ 15 49 - Flutter 16 50 - record_ios (1.2.0): 17 51 - Flutter 52 + - SDWebImage (5.21.7):53 + - SDWebImage/Core (= 5.21.7)54 + - SDWebImage/Core (5.21.7)18 55 - share_plus (0.0.1): 19 56 - Flutter 20 57 - shared_preferences_foundation (0.0.1): 21 58 - Flutter 22 59 - FlutterMacOS 60 + - SwiftyGif (5.4.5)23 61 - vibration (1.7.5): 24 62 - Flutter 25 63 - wakelock_plus (0.0.1): .. .. @@ -28,6 +66,7 @@ 28 66 DEPENDENCIES: 29 67 - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`) 30 68 - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) 69 + - file_picker (from `.symlinks/plugins/file_picker/ios`)31 70 - Flutter (from `Flutter`) 32 71 - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) 33 72 - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) .. .. @@ -39,11 +78,20 @@ 39 78 - vibration (from `.symlinks/plugins/vibration/ios`) 40 79 - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) 41 80 81 +SPEC REPOS:82 + trunk:83 + - DKImagePickerController84 + - DKPhotoGallery85 + - SDWebImage86 + - SwiftyGif87 +42 88 EXTERNAL SOURCES: 43 89 audioplayers_darwin: 44 90 :path: ".symlinks/plugins/audioplayers_darwin/darwin" 45 91 device_info_plus: 46 92 :path: ".symlinks/plugins/device_info_plus/ios" 93 + file_picker:94 + :path: ".symlinks/plugins/file_picker/ios"47 95 Flutter: 48 96 :path: Flutter 49 97 flutter_secure_storage: .. .. @@ -68,14 +116,19 @@ 68 116 SPEC CHECKSUMS: 69 117 audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5 70 118 device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe 119 + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c120 + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60121 + file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be71 122 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 72 123 flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 73 124 image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 74 125 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 75 126 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d 76 127 record_ios: 412daca2350b228e698fffcd08f1f94ceb1e3844 128 + SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf77 129 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a 78 130 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb 131 + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d479 132 vibration: 8e2f50fc35bb736f9eecb7dd9f7047fbb6a6e888 80 133 wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 81 134 lib/screens/chat_screen.dart
.. .. @@ -7,6 +7,7 @@ 7 7 import 'package:flutter_riverpod/flutter_riverpod.dart'; 8 8 import 'package:go_router/go_router.dart'; 9 9 import 'package:image_picker/image_picker.dart'; 10 +import 'package:file_picker/file_picker.dart';10 11 import 'package:shared_preferences/shared_preferences.dart'; 11 12 12 13 import '../models/message.dart'; .. .. @@ -663,6 +664,82 @@ 663 664 } 664 665 } 665 666 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 session698 + 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 metadata709 + _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 +666 743 void _requestScreenshot() { 667 744 _screenshotForChat = true; 668 745 _sendCommand('screenshot', {'sessionId': ref.read(activeSessionIdProvider)}); .. .. @@ -683,15 +760,59 @@ 683 760 } 684 761 685 762 Future<void> _pickPhoto() async { 686 - // Capture session ID now — before any async gaps (dialog, encoding)687 763 final targetSessionId = ref.read(activeSessionIdProvider); 688 764 689 - final picker = ImagePicker();690 - final images = await picker.pickMultiImage(691 - maxWidth: 1920,692 - maxHeight: 1080,693 - imageQuality: 85,765 + // Show picker options766 + 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 + ),694 790 ); 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 + }695 816 696 817 if (images.isEmpty) return; 697 818 lib/widgets/command_bar.dart
.. .. @@ -45,7 +45,7 @@ 45 45 ), 46 46 _CommandButton( 47 47 icon: Icons.attach_file, 48 - label: 'Photo',48 + label: 'Attach',49 49 onTap: onPhoto, 50 50 ), 51 51 _CommandButton( macos/Flutter/GeneratedPluginRegistrant.swift
.. .. @@ -7,6 +7,7 @@ 7 7 8 8 import audioplayers_darwin 9 9 import device_info_plus 10 +import file_picker10 11 import file_selector_macos 11 12 import flutter_secure_storage_macos 12 13 import package_info_plus .. .. @@ -18,6 +19,7 @@ 18 19 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 19 20 AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) 20 21 DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) 22 + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))21 23 FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) 22 24 FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) 23 25 FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) pubspec.lock
.. .. @@ -193,6 +193,14 @@ 193 193 url: "https://pub.dev" 194 194 source: hosted 195 195 version: "7.0.1" 196 + file_picker:197 + dependency: "direct main"198 + description:199 + name: file_picker200 + sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343"201 + url: "https://pub.dev"202 + source: hosted203 + version: "10.3.10"196 204 file_selector_linux: 197 205 dependency: transitive 198 206 description: pubspec.yaml
.. .. @@ -29,6 +29,7 @@ 29 29 mqtt_client: ^10.6.0 30 30 uuid: ^4.5.1 31 31 collection: ^1.19.1 32 + file_picker: ^10.3.1032 33 33 34 dev_dependencies: 34 35 flutter_test: