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

---
 lib/screens/chat_screen.dart |  133 ++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 127 insertions(+), 6 deletions(-)

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;
 

--
Gitblit v1.3.1