a687e3637f
Migrate StateNotifierProvider/StateNotifier → NotifierProvider/Notifier, StateProvider → NotifierProvider with simple notifier, FamilyAsyncNotifier removed in Riverpod 3 (replaced with constructor-arg pattern), and rename .valueOrNull → .value throughout all providers and screens. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
106 lines
3.5 KiB
Dart
106 lines
3.5 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../core/exceptions.dart';
|
|
import 'api_client_provider.dart';
|
|
import 'capture_queue_provider.dart';
|
|
import 'notes_provider.dart';
|
|
import 'tasks_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.
|
|
/// Separate from [captureQueueProvider] (which is the offline persistence queue).
|
|
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 {
|
|
final api = ref.read(quickCaptureApiProvider);
|
|
final result = await api.capture(text);
|
|
|
|
// Dequeue on success.
|
|
state = state.length > 1 ? state.sublist(1) : [];
|
|
|
|
// Invalidate content providers so lists refresh.
|
|
switch (result.type) {
|
|
case 'note':
|
|
ref.invalidate(notesProvider);
|
|
case 'task':
|
|
case 'todo':
|
|
ref.invalidate(tasksProvider);
|
|
}
|
|
|
|
// Publish result for snackbar.
|
|
final msg = result.message.isNotEmpty
|
|
? result.message
|
|
: '${_typeLabel(result.type)} created: ${result.title}';
|
|
ref.read(captureResultProvider.notifier).state = CaptureResult(msg);
|
|
} 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 = 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 =
|
|
CaptureResult('Capture failed. Please try again.', isError: true);
|
|
}
|
|
}
|
|
} finally {
|
|
_running = false;
|
|
}
|
|
}
|
|
|
|
String _typeLabel(String type) => switch (type) {
|
|
'note' => 'Note',
|
|
'task' => 'Task',
|
|
'event' => 'Event',
|
|
'todo' => 'To-do',
|
|
_ => type,
|
|
};
|
|
}
|