Matthias Nott
2026-03-23 07ad99d7c4f8c52930442a34d316e634435bd75a
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