c177bf0691
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>
90 lines
3.2 KiB
Dart
90 lines
3.2 KiB
Dart
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.
|
|
class CaptureResult {
|
|
final String message;
|
|
final bool isError;
|
|
const CaptureResult(this.message, {this.isError = false});
|
|
}
|
|
|
|
/// The most recent capture result. UI watches this to show snackbars.
|
|
/// Reset to null by the notifier before each new item so listeners always fire.
|
|
final captureResultProvider =
|
|
NotifierProvider<_CaptureResultNotifier, CaptureResult?>(
|
|
_CaptureResultNotifier.new);
|
|
|
|
class _CaptureResultNotifier extends Notifier<CaptureResult?> {
|
|
@override
|
|
CaptureResult? build() => null;
|
|
}
|
|
|
|
/// In-memory sequential work queue for quick captures.
|
|
final captureWorkQueueProvider =
|
|
NotifierProvider<CaptureWorkQueueNotifier, List<String>>(
|
|
CaptureWorkQueueNotifier.new,
|
|
);
|
|
|
|
class CaptureWorkQueueNotifier extends Notifier<List<String>> {
|
|
bool _running = false;
|
|
|
|
@override
|
|
List<String> build() => [];
|
|
|
|
/// Add text to the queue and start the drain loop if not already running.
|
|
void enqueue(String text) {
|
|
state = [...state, text];
|
|
_drain();
|
|
}
|
|
|
|
Future<void> _drain() async {
|
|
if (_running) return;
|
|
_running = true;
|
|
try {
|
|
while (state.isNotEmpty) {
|
|
final text = state.first;
|
|
// Signal "no result yet" so the same result value can re-trigger watch.
|
|
ref.read(captureResultProvider.notifier).state = null;
|
|
try {
|
|
// Create a new conversation, add it to the conversations list, then
|
|
// send the message and kick off generation in the background.
|
|
final conv =
|
|
await ref.read(conversationsProvider.notifier).create('');
|
|
final chatRepo = ref.read(chatRepositoryProvider);
|
|
await chatRepo.sendMessage(conv.id, text);
|
|
// Fire-and-forget: drain the SSE stream so the server generates a
|
|
// response (creating notes/tasks/etc.) without blocking the UI.
|
|
chatRepo.streamGeneration(conv.id).drain<void>().ignore();
|
|
|
|
state = state.length > 1 ? state.sublist(1) : [];
|
|
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 — capture saved and will retry automatically.",
|
|
);
|
|
break;
|
|
} on AppException catch (e) {
|
|
state = state.length > 1 ? state.sublist(1) : [];
|
|
ref.read(captureResultProvider.notifier).state =
|
|
CaptureResult(e.message, isError: true);
|
|
} catch (_) {
|
|
state = state.length > 1 ? state.sublist(1) : [];
|
|
ref.read(captureResultProvider.notifier).state = const CaptureResult(
|
|
'Failed to send. Please try again.',
|
|
isError: true);
|
|
}
|
|
}
|
|
} finally {
|
|
_running = false;
|
|
}
|
|
}
|
|
}
|