diff --git a/lib/app.dart b/lib/app.dart index 15164f4..294aec3 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -8,6 +8,8 @@ import 'core/constants.dart'; import 'core/theme.dart'; import 'providers/api_client_provider.dart'; import 'providers/auth_provider.dart'; +import 'core/exceptions.dart'; +import 'providers/capture_queue_provider.dart'; import 'providers/capture_work_queue_provider.dart'; import 'providers/briefing_provider.dart'; import 'providers/calendar_provider.dart'; @@ -512,6 +514,12 @@ class _QuickCaptureBar extends ConsumerStatefulWidget { class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> { final _controller = TextEditingController(); + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => _drainOfflineQueue()); + } + @override void dispose() { _controller.dispose(); @@ -527,6 +535,28 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> { ref.read(captureWorkQueueProvider.notifier).enqueue(text); } + Future _drainOfflineQueue() async { + if (!mounted) return; + final queue = ref.read(captureQueueProvider); + if (queue.isEmpty) return; + for (final text in List.from(queue)) { + if (!mounted) break; + try { + final conv = + await ref.read(conversationsProvider.notifier).create(''); + final chatRepo = ref.read(chatRepositoryProvider); + await chatRepo.sendMessage(conv.id, text); + chatRepo.streamGeneration(conv.id).drain().ignore(); + await ref.read(captureQueueProvider.notifier).dequeue(text); + } on NetworkException { + break; + } catch (_) { + // Server error — drop from queue to prevent ghost items. + await ref.read(captureQueueProvider.notifier).dequeue(text); + } + } + } + Future _toggleCaptureMic() async { final voice = ref.read(voiceProvider); if (voice.voiceModeActive) { @@ -556,8 +586,10 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> { @override Widget build(BuildContext context) { final location = GoRouterState.of(context).matchedLocation; + final offlineQueueCount = ref.watch(captureQueueProvider).length; final workQueue = ref.watch(captureWorkQueueProvider); final isWorking = workQueue.isNotEmpty; + final totalPending = workQueue.length + offlineQueueCount; // Show snackbar when a result is published. ref.listen(captureResultProvider, (_, result) { @@ -592,7 +624,12 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> { isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 14, vertical: 10), - prefixIcon: isWorking + prefixIcon: totalPending > 0 + ? Badge( + label: Text('$totalPending'), + child: const Icon(Icons.cloud_upload_outlined), + ) + : isWorking ? const Padding( padding: EdgeInsets.all(12), child: SizedBox( diff --git a/lib/providers/capture_work_queue_provider.dart b/lib/providers/capture_work_queue_provider.dart index 6cd4cc3..28b8e1f 100644 --- a/lib/providers/capture_work_queue_provider.dart +++ b/lib/providers/capture_work_queue_provider.dart @@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/exceptions.dart'; import 'api_client_provider.dart'; +import 'capture_queue_provider.dart'; import 'chat_provider.dart'; /// Outcome of a single capture attempt — consumed by the UI for snackbars. @@ -63,11 +64,13 @@ class CaptureWorkQueueNotifier extends Notifier> { ref.read(captureResultProvider.notifier).state = const CaptureResult('Sent to Fabled.'); } on NetworkException catch (_) { + // Persist to offline queue and stop draining — still offline. + await ref.read(captureQueueProvider.notifier).enqueue(text); state = state.length > 1 ? state.sublist(1) : []; ref.read(captureResultProvider.notifier).state = const CaptureResult( - "You're offline — message not sent.", - isError: true, + "You're offline — capture saved and will retry automatically.", ); + break; } on AppException catch (e) { state = state.length > 1 ? state.sublist(1) : []; ref.read(captureResultProvider.notifier).state =