fix: restore offline queue for captures, drain via chat on retry
Offline queue (SharedPreferences) persists captures when the device is offline and replays them as chat conversations on next launch, preserving the same fire-and-forget guarantee as the online path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+38
-1
@@ -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<void> _drainOfflineQueue() async {
|
||||
if (!mounted) return;
|
||||
final queue = ref.read(captureQueueProvider);
|
||||
if (queue.isEmpty) return;
|
||||
for (final text in List<String>.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<void>().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<void> _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(
|
||||
|
||||
Reference in New Issue
Block a user