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