From 07ad99d7c4f8c52930442a34d316e634435bd75a Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Mon, 23 Mar 2026 10:18:46 +0100
Subject: [PATCH] feat: attach button with camera, gallery, and file picker
---
ios/Podfile.lock | 53 +++++++++++++
lib/widgets/command_bar.dart | 2
macos/Flutter/GeneratedPluginRegistrant.swift | 2
pubspec.lock | 8 ++
lib/screens/chat_screen.dart | 133 +++++++++++++++++++++++++++++++-
pubspec.yaml | 1
6 files changed, 192 insertions(+), 7 deletions(-)
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 0815178..b24639a 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -4,6 +4,40 @@
- FlutterMacOS
- device_info_plus (0.0.1):
- Flutter
+ - DKImagePickerController/Core (4.3.9):
+ - DKImagePickerController/ImageDataManager
+ - DKImagePickerController/Resource
+ - DKImagePickerController/ImageDataManager (4.3.9)
+ - DKImagePickerController/PhotoGallery (4.3.9):
+ - DKImagePickerController/Core
+ - DKPhotoGallery
+ - DKImagePickerController/Resource (4.3.9)
+ - DKPhotoGallery (0.0.19):
+ - DKPhotoGallery/Core (= 0.0.19)
+ - DKPhotoGallery/Model (= 0.0.19)
+ - DKPhotoGallery/Preview (= 0.0.19)
+ - DKPhotoGallery/Resource (= 0.0.19)
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Core (0.0.19):
+ - DKPhotoGallery/Model
+ - DKPhotoGallery/Preview
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Model (0.0.19):
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Preview (0.0.19):
+ - DKPhotoGallery/Model
+ - DKPhotoGallery/Resource
+ - SDWebImage
+ - SwiftyGif
+ - DKPhotoGallery/Resource (0.0.19):
+ - SDWebImage
+ - SwiftyGif
+ - file_picker (0.0.1):
+ - DKImagePickerController/PhotoGallery
+ - Flutter
- Flutter (1.0.0)
- flutter_secure_storage (6.0.0):
- Flutter
@@ -15,11 +49,15 @@
- Flutter
- record_ios (1.2.0):
- Flutter
+ - SDWebImage (5.21.7):
+ - SDWebImage/Core (= 5.21.7)
+ - SDWebImage/Core (5.21.7)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
+ - SwiftyGif (5.4.5)
- vibration (1.7.5):
- Flutter
- wakelock_plus (0.0.1):
@@ -28,6 +66,7 @@
DEPENDENCIES:
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
+ - file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
@@ -39,11 +78,20 @@
- vibration (from `.symlinks/plugins/vibration/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
+SPEC REPOS:
+ trunk:
+ - DKImagePickerController
+ - DKPhotoGallery
+ - SDWebImage
+ - SwiftyGif
+
EXTERNAL SOURCES:
audioplayers_darwin:
:path: ".symlinks/plugins/audioplayers_darwin/darwin"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
+ file_picker:
+ :path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
flutter_secure_storage:
@@ -68,14 +116,19 @@
SPEC CHECKSUMS:
audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
+ DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
+ DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
+ file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
record_ios: 412daca2350b228e698fffcd08f1f94ceb1e3844
+ SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
+ SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
vibration: 8e2f50fc35bb736f9eecb7dd9f7047fbb6a6e888
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart
index 23ce43d..cec56f8 100644
--- a/lib/screens/chat_screen.dart
+++ b/lib/screens/chat_screen.dart
@@ -7,6 +7,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
+import 'package:file_picker/file_picker.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/message.dart';
@@ -663,6 +664,82 @@
}
}
+ Future<void> _pickFiles(String? targetSessionId) async {
+ final result = await FilePicker.platform.pickFiles(
+ allowMultiple: true,
+ type: FileType.any,
+ );
+ if (result == null || result.files.isEmpty) return;
+
+ for (final file in result.files) {
+ if (file.path == null) continue;
+ final bytes = await File(file.path!).readAsBytes();
+ final b64 = base64Encode(bytes);
+ final mimeType = _guessMimeType(file.name);
+ final isImage = mimeType.startsWith('image/');
+
+ if (isImage) {
+ final message = Message.image(
+ role: MessageRole.user,
+ imageBase64: b64,
+ content: file.name,
+ status: MessageStatus.sent,
+ );
+ ref.read(messagesProvider.notifier).addMessage(message);
+ _ws?.send({
+ 'type': 'image',
+ 'imageBase64': b64,
+ 'mimeType': mimeType,
+ 'caption': file.name,
+ 'sessionId': targetSessionId,
+ });
+ } else {
+ // Non-image file: send as text with file info, save file to temp for session
+ final tmpDir = await getTemporaryDirectory();
+ final tmpPath = '${tmpDir.path}/${file.name}';
+ await File(tmpPath).writeAsBytes(bytes);
+
+ final message = Message.text(
+ role: MessageRole.user,
+ content: '📎 ${file.name} (${_formatSize(bytes.length)})',
+ status: MessageStatus.sent,
+ );
+ ref.read(messagesProvider.notifier).addMessage(message);
+ // Send file as base64 with metadata
+ _ws?.send({
+ 'type': 'file',
+ 'fileBase64': b64,
+ 'fileName': file.name,
+ 'mimeType': mimeType,
+ 'fileSize': bytes.length,
+ 'sessionId': targetSessionId,
+ });
+ }
+ }
+ _scrollToBottom();
+ }
+
+ String _guessMimeType(String name) {
+ final ext = name.split('.').last.toLowerCase();
+ const map = {
+ 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png',
+ 'gif': 'image/gif', 'webp': 'image/webp', 'heic': 'image/heic',
+ 'pdf': 'application/pdf', 'doc': 'application/msword',
+ 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'xls': 'application/vnd.ms-excel',
+ 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'txt': 'text/plain', 'csv': 'text/csv', 'json': 'application/json',
+ 'zip': 'application/zip', 'mp3': 'audio/mpeg', 'mp4': 'video/mp4',
+ };
+ return map[ext] ?? 'application/octet-stream';
+ }
+
+ String _formatSize(int bytes) {
+ if (bytes < 1024) return '$bytes B';
+ if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
+ return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
+ }
+
void _requestScreenshot() {
_screenshotForChat = true;
_sendCommand('screenshot', {'sessionId': ref.read(activeSessionIdProvider)});
@@ -683,15 +760,59 @@
}
Future<void> _pickPhoto() async {
- // Capture session ID now — before any async gaps (dialog, encoding)
final targetSessionId = ref.read(activeSessionIdProvider);
- final picker = ImagePicker();
- final images = await picker.pickMultiImage(
- maxWidth: 1920,
- maxHeight: 1080,
- imageQuality: 85,
+ // Show picker options
+ final source = await showModalBottomSheet<String>(
+ context: context,
+ builder: (ctx) => SafeArea(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ListTile(
+ leading: const Icon(Icons.camera_alt),
+ title: const Text('Take Photo'),
+ onTap: () => Navigator.pop(ctx, 'camera'),
+ ),
+ ListTile(
+ leading: const Icon(Icons.photo_library),
+ title: const Text('Photo Library'),
+ onTap: () => Navigator.pop(ctx, 'gallery'),
+ ),
+ ListTile(
+ leading: const Icon(Icons.attach_file),
+ title: const Text('Files'),
+ onTap: () => Navigator.pop(ctx, 'files'),
+ ),
+ ],
+ ),
+ ),
);
+ if (source == null) return;
+
+ if (source == 'files') {
+ await _pickFiles(targetSessionId);
+ return;
+ }
+
+ List<XFile> images;
+ final picker = ImagePicker();
+ if (source == 'camera') {
+ final photo = await picker.pickImage(
+ source: ImageSource.camera,
+ maxWidth: 1920,
+ maxHeight: 1080,
+ imageQuality: 85,
+ );
+ if (photo == null) return;
+ images = [photo];
+ } else {
+ images = await picker.pickMultiImage(
+ maxWidth: 1920,
+ maxHeight: 1080,
+ imageQuality: 85,
+ );
+ }
if (images.isEmpty) return;
diff --git a/lib/widgets/command_bar.dart b/lib/widgets/command_bar.dart
index 8d5e52b..583978c 100644
--- a/lib/widgets/command_bar.dart
+++ b/lib/widgets/command_bar.dart
@@ -45,7 +45,7 @@
),
_CommandButton(
icon: Icons.attach_file,
- label: 'Photo',
+ label: 'Attach',
onTap: onPhoto,
),
_CommandButton(
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 51772ed..67a6ae9 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -7,6 +7,7 @@
import audioplayers_darwin
import device_info_plus
+import file_picker
import file_selector_macos
import flutter_secure_storage_macos
import package_info_plus
@@ -18,6 +19,7 @@
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
+ FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
diff --git a/pubspec.lock b/pubspec.lock
index 61cba3b..c246b00 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -193,6 +193,14 @@
url: "https://pub.dev"
source: hosted
version: "7.0.1"
+ file_picker:
+ dependency: "direct main"
+ description:
+ name: file_picker
+ sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343"
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.3.10"
file_selector_linux:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 7ad8705..a892e29 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -29,6 +29,7 @@
mqtt_client: ^10.6.0
uuid: ^4.5.1
collection: ^1.19.1
+ file_picker: ^10.3.10
dev_dependencies:
flutter_test:
--
Gitblit v1.3.1