diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart index 6cf1a0f..fe82d88 100644 --- a/lib/providers/auth_provider.dart +++ b/lib/providers/auth_provider.dart @@ -4,18 +4,15 @@ import 'api_client_provider.dart'; enum AuthStatus { unknown, authenticated, unauthenticated } -final authProvider = StateNotifierProvider((ref) { - return AuthNotifier(ref); -}); +final authProvider = NotifierProvider(AuthNotifier.new); -class AuthNotifier extends StateNotifier { - final Ref _ref; - - AuthNotifier(this._ref) : super(AuthStatus.unknown); +class AuthNotifier extends Notifier { + @override + AuthStatus build() => AuthStatus.unknown; Future verify() async { try { - final repo = _ref.read(authRepositoryProvider); + final repo = ref.read(authRepositoryProvider); final ok = await repo.verify(); state = ok ? AuthStatus.authenticated : AuthStatus.unauthenticated; } catch (_) { @@ -24,14 +21,14 @@ class AuthNotifier extends StateNotifier { } Future login(String username, String password) async { - final repo = _ref.read(authRepositoryProvider); + final repo = ref.read(authRepositoryProvider); await repo.login(username, password); state = AuthStatus.authenticated; } Future logout() async { try { - final repo = _ref.read(authRepositoryProvider); + final repo = ref.read(authRepositoryProvider); await repo.logout(); } finally { state = AuthStatus.unauthenticated; diff --git a/lib/providers/briefing_provider.dart b/lib/providers/briefing_provider.dart index 3597552..40786f1 100644 --- a/lib/providers/briefing_provider.dart +++ b/lib/providers/briefing_provider.dart @@ -5,7 +5,13 @@ import '../data/models/message.dart'; import 'api_client_provider.dart'; /// Drives the loading indicator in BriefingScreen's reply area. -final isBriefingStreamingProvider = StateProvider((ref) => false); +final isBriefingStreamingProvider = + NotifierProvider<_BoolNotifier, bool>(_BoolNotifier.new); + +class _BoolNotifier extends Notifier { + @override + bool build() => false; +} final briefingProvider = AsyncNotifierProvider( @@ -32,7 +38,7 @@ class BriefingNotifier extends AsyncNotifier { /// 3. SSE stream (best-effort) /// 4. Poll until complete Future sendReply(String content) async { - final conv = state.valueOrNull; + final conv = state.value; if (conv == null) return; final convId = conv.id; final chatApi = ref.read(chatApiProvider); @@ -65,7 +71,7 @@ class BriefingNotifier extends AsyncNotifier { try { await for (final chunk in chatApi.streamGeneration(convId)) { streamedContent = true; - final current = state.valueOrNull; + final current = state.value; if (current == null) break; final msgs = current.messages; if (msgs.isEmpty) continue; @@ -88,7 +94,7 @@ class BriefingNotifier extends AsyncNotifier { final hasContent = fresh.any( (m) => m.role == MessageRole.assistant && m.content.isNotEmpty, ); - final current = state.valueOrNull; + final current = state.value; if (current != null && (!streamedContent || done || hasContent)) { state = AsyncData(current.copyWith(messages: fresh)); } @@ -96,7 +102,7 @@ class BriefingNotifier extends AsyncNotifier { } } catch (_) { // Clear the generating placeholder so UI doesn't spin forever. - final current = state.valueOrNull; + final current = state.value; if (current != null) { final msgs = current.messages; if (msgs.isNotEmpty && msgs.last.status == 'generating') { diff --git a/lib/providers/capture_queue_provider.dart b/lib/providers/capture_queue_provider.dart index 89f2bcb..2805ac7 100644 --- a/lib/providers/capture_queue_provider.dart +++ b/lib/providers/capture_queue_provider.dart @@ -4,16 +4,20 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'settings_provider.dart'; final captureQueueProvider = - StateNotifierProvider>( - (ref) => CaptureQueueNotifier(ref.watch(sharedPreferencesProvider)), + NotifierProvider>( + CaptureQueueNotifier.new, ); -class CaptureQueueNotifier extends StateNotifier> { +class CaptureQueueNotifier extends Notifier> { static const _key = 'capture_queue'; - final SharedPreferences _prefs; - CaptureQueueNotifier(this._prefs) - : super(_prefs.getStringList(_key) ?? []); + SharedPreferences get _prefs => ref.read(sharedPreferencesProvider); + + @override + List build() { + final prefs = ref.watch(sharedPreferencesProvider); + return prefs.getStringList(_key) ?? []; + } Future enqueue(String text) async { final updated = [...state, text]; diff --git a/lib/providers/capture_work_queue_provider.dart b/lib/providers/capture_work_queue_provider.dart index 3c83ed8..afa0f5a 100644 --- a/lib/providers/capture_work_queue_provider.dart +++ b/lib/providers/capture_work_queue_provider.dart @@ -15,20 +15,27 @@ class CaptureResult { /// 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 = StateProvider((_) => null); +final captureResultProvider = + NotifierProvider<_CaptureResultNotifier, CaptureResult?>( + _CaptureResultNotifier.new); + +class _CaptureResultNotifier extends Notifier { + @override + CaptureResult? build() => null; +} /// In-memory sequential work queue for quick captures. /// Separate from [captureQueueProvider] (which is the offline persistence queue). final captureWorkQueueProvider = - StateNotifierProvider>( - (ref) => CaptureWorkQueueNotifier(ref), + NotifierProvider>( + CaptureWorkQueueNotifier.new, ); -class CaptureWorkQueueNotifier extends StateNotifier> { - final Ref _ref; +class CaptureWorkQueueNotifier extends Notifier> { bool _running = false; - CaptureWorkQueueNotifier(this._ref) : super([]); + @override + List build() => []; /// Add text to the queue and start the drain loop if not already running. void enqueue(String text) { @@ -43,9 +50,9 @@ class CaptureWorkQueueNotifier extends StateNotifier> { 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; + ref.read(captureResultProvider.notifier).state = null; try { - final api = _ref.read(quickCaptureApiProvider); + final api = ref.read(quickCaptureApiProvider); final result = await api.capture(text); // Dequeue on success. @@ -54,32 +61,32 @@ class CaptureWorkQueueNotifier extends StateNotifier> { // Invalidate content providers so lists refresh. switch (result.type) { case 'note': - _ref.invalidate(notesProvider); + ref.invalidate(notesProvider); case 'task': case 'todo': - _ref.invalidate(tasksProvider); + 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); + 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); + await ref.read(captureQueueProvider.notifier).enqueue(text); state = state.length > 1 ? state.sublist(1) : []; - _ref.read(captureResultProvider.notifier).state = CaptureResult( + 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 = + ref.read(captureResultProvider.notifier).state = CaptureResult(e.message, isError: true); } catch (_) { state = state.length > 1 ? state.sublist(1) : []; - _ref.read(captureResultProvider.notifier).state = + ref.read(captureResultProvider.notifier).state = CaptureResult('Capture failed. Please try again.', isError: true); } } diff --git a/lib/providers/chat_provider.dart b/lib/providers/chat_provider.dart index 50fa882..b52ba69 100644 --- a/lib/providers/chat_provider.dart +++ b/lib/providers/chat_provider.dart @@ -4,8 +4,19 @@ import '../data/models/conversation.dart'; import '../data/models/message.dart'; import 'api_client_provider.dart'; -// Separate StateProvider so UI re-builds immediately when streaming starts/stops. -final isStreamingProvider = StateProvider.family((ref, _) => false); +// Separate NotifierProvider.family so UI re-builds immediately when streaming starts/stops. +final isStreamingProvider = + NotifierProvider.family<_IsStreamingNotifier, bool, int>( + (convId) => _IsStreamingNotifier(convId), +); + +class _IsStreamingNotifier extends Notifier { + // ignore: avoid_unused_constructor_parameters + _IsStreamingNotifier(int convId); + + @override + bool build() => false; +} final conversationsProvider = AsyncNotifierProvider>( @@ -20,14 +31,14 @@ class ConversationsNotifier extends AsyncNotifier> { Future create(String title) async { final conv = await ref.read(chatRepositoryProvider).createConversation(title); - state = AsyncData([conv, ...state.valueOrNull ?? []]); + state = AsyncData([conv, ...state.value ?? []]); return conv; } Future delete(int id) async { await ref.read(chatRepositoryProvider).deleteConversation(id); state = AsyncData([ - for (final c in state.valueOrNull ?? []) + for (final c in state.value ?? []) if (c.id != id) c, ]); } @@ -35,7 +46,7 @@ class ConversationsNotifier extends AsyncNotifier> { // Called after a message is sent to patch the server-generated title // in-place without triggering a full reload or loading state. void patchConversation(Conversation updated) { - final list = state.valueOrNull; + final list = state.value; if (list == null) return; state = AsyncData([ for (final c in list) @@ -44,21 +55,26 @@ class ConversationsNotifier extends AsyncNotifier> { } } -final messagesProvider = AsyncNotifierProvider.family, int>(MessagesNotifier.new); +final messagesProvider = + AsyncNotifierProvider.family, int>( + (convId) => MessagesNotifier(convId), +); + +class MessagesNotifier extends AsyncNotifier> { + final int _convId; + MessagesNotifier(this._convId); -class MessagesNotifier extends FamilyAsyncNotifier, int> { @override - Future> build(int arg) async { + Future> build() async { final (_, messages) = - await ref.watch(chatRepositoryProvider).getMessages(arg); + await ref.watch(chatRepositoryProvider).getMessages(_convId); return messages; } Future sendMessage(String content) async { - final convId = arg; + final convId = _convId; final repo = ref.read(chatRepositoryProvider); - final previousMessages = state.valueOrNull ?? []; + final previousMessages = state.value ?? []; // Optimistic UI: show the user message + assistant placeholder immediately. final userMsg = Message( @@ -132,7 +148,7 @@ class MessagesNotifier extends FamilyAsyncNotifier, int> { } catch (_) { // Polling failed entirely — clear the generating placeholder so the UI // doesn't spin forever. - final msgs = state.valueOrNull; + final msgs = state.value; if (msgs != null && msgs.isNotEmpty && msgs.last.status == 'generating') { state = AsyncData([ ...msgs.sublist(0, msgs.length - 1), diff --git a/lib/providers/notes_provider.dart b/lib/providers/notes_provider.dart index 532f241..fcbaa14 100644 --- a/lib/providers/notes_provider.dart +++ b/lib/providers/notes_provider.dart @@ -21,7 +21,7 @@ class NotesNotifier extends AsyncNotifier> { final note = await ref .read(notesRepositoryProvider) .create(title, body, tags: tags, projectId: projectId); - state = AsyncData([...state.valueOrNull ?? [], note]); + state = AsyncData([...state.value ?? [], note]); return note; } @@ -42,7 +42,7 @@ class NotesNotifier extends AsyncNotifier> { clearProject: clearProject, ); state = AsyncData([ - for (final n in state.valueOrNull ?? []) + for (final n in state.value ?? []) if (n.id == id) updated else n, ]); return updated; @@ -51,7 +51,7 @@ class NotesNotifier extends AsyncNotifier> { Future delete(int id) async { await ref.read(notesRepositoryProvider).delete(id); state = AsyncData([ - for (final n in state.valueOrNull ?? []) + for (final n in state.value ?? []) if (n.id != id) n, ]); } diff --git a/lib/providers/projects_provider.dart b/lib/providers/projects_provider.dart index eac052b..5c9d940 100644 --- a/lib/providers/projects_provider.dart +++ b/lib/providers/projects_provider.dart @@ -25,7 +25,7 @@ class ProjectsNotifier extends AsyncNotifier> { goal: goal, color: color, ); - state = AsyncData([...state.valueOrNull ?? [], project]); + state = AsyncData([...state.value ?? [], project]); return project; } @@ -33,7 +33,7 @@ class ProjectsNotifier extends AsyncNotifier> { final updated = await ref.read(projectsRepositoryProvider).update(id, fields); state = AsyncData([ - for (final p in state.valueOrNull ?? []) + for (final p in state.value ?? []) if (p.id == id) updated else p, ]); return updated; @@ -42,7 +42,7 @@ class ProjectsNotifier extends AsyncNotifier> { Future delete(int id) async { await ref.read(projectsRepositoryProvider).delete(id); state = AsyncData([ - for (final p in state.valueOrNull ?? []) + for (final p in state.value ?? []) if (p.id != id) p, ]); } diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 167e279..38478b4 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -15,16 +15,14 @@ final cookiesPathProvider = Provider((ref) { }); final themeModeProvider = - StateNotifierProvider((ref) { - final prefs = ref.watch(sharedPreferencesProvider); - return ThemeModeNotifier(prefs); -}); + NotifierProvider(ThemeModeNotifier.new); -class ThemeModeNotifier extends StateNotifier { - final SharedPreferences _prefs; - - ThemeModeNotifier(this._prefs) - : super(_fromString(_prefs.getString(_kThemeMode))); +class ThemeModeNotifier extends Notifier { + @override + ThemeMode build() { + final prefs = ref.watch(sharedPreferencesProvider); + return _fromString(prefs.getString(_kThemeMode)); + } static ThemeMode _fromString(String? value) => switch (value) { 'light' => ThemeMode.light, @@ -38,48 +36,49 @@ class ThemeModeNotifier extends StateNotifier { ThemeMode.dark => 'dark', _ => 'system', }; - await _prefs.setString(_kThemeMode, value); + await ref.read(sharedPreferencesProvider).setString(_kThemeMode, value); state = mode; } } final forgejoRepoUrlProvider = - StateNotifierProvider((ref) { - return ForgejoRepoUrlNotifier(ref.watch(sharedPreferencesProvider)); -}); + NotifierProvider( + ForgejoRepoUrlNotifier.new); -class ForgejoRepoUrlNotifier extends StateNotifier { - final SharedPreferences _prefs; - ForgejoRepoUrlNotifier(this._prefs) - : super(_prefs.getString(_kForgejoRepoUrl) ?? - 'https://git.fabledsword.com/bvandeusen/FabledApp'); +class ForgejoRepoUrlNotifier extends Notifier { + @override + String? build() { + final prefs = ref.watch(sharedPreferencesProvider); + return prefs.getString(_kForgejoRepoUrl) ?? + 'https://git.fabledsword.com/bvandeusen/FabledApp'; + } Future setUrl(String url) async { final clean = url.endsWith('/') ? url.substring(0, url.length - 1) : url; - await _prefs.setString(_kForgejoRepoUrl, clean); + await ref.read(sharedPreferencesProvider).setString(_kForgejoRepoUrl, clean); state = clean; } } -final serverUrlProvider = StateNotifierProvider((ref) { - final prefs = ref.watch(sharedPreferencesProvider); - return ServerUrlNotifier(prefs); -}); +final serverUrlProvider = + NotifierProvider(ServerUrlNotifier.new); -class ServerUrlNotifier extends StateNotifier { - final SharedPreferences _prefs; - - ServerUrlNotifier(this._prefs) : super(_prefs.getString(_kServerUrl)); +class ServerUrlNotifier extends Notifier { + @override + String? build() { + final prefs = ref.watch(sharedPreferencesProvider); + return prefs.getString(_kServerUrl); + } Future setUrl(String url) async { // Strip trailing slash final clean = url.endsWith('/') ? url.substring(0, url.length - 1) : url; - await _prefs.setString(_kServerUrl, clean); + await ref.read(sharedPreferencesProvider).setString(_kServerUrl, clean); state = clean; } Future clear() async { - await _prefs.remove(_kServerUrl); + await ref.read(sharedPreferencesProvider).remove(_kServerUrl); state = null; } } diff --git a/lib/providers/tasks_provider.dart b/lib/providers/tasks_provider.dart index 29b9855..b9555d4 100644 --- a/lib/providers/tasks_provider.dart +++ b/lib/providers/tasks_provider.dart @@ -28,14 +28,14 @@ class TasksNotifier extends AsyncNotifier> { dueDate: dueDate, projectId: projectId, ); - state = AsyncData([...state.valueOrNull ?? [], task]); + state = AsyncData([...state.value ?? [], task]); return task; } Future updateTask(int id, Map fields) async { final updated = await ref.read(tasksRepositoryProvider).update(id, fields); state = AsyncData([ - for (final t in state.valueOrNull ?? []) + for (final t in state.value ?? []) if (t.id == id) updated else t, ]); return updated; @@ -44,7 +44,7 @@ class TasksNotifier extends AsyncNotifier> { Future delete(int id) async { await ref.read(tasksRepositoryProvider).delete(id); state = AsyncData([ - for (final t in state.valueOrNull ?? []) + for (final t in state.value ?? []) if (t.id != id) t, ]); } diff --git a/lib/screens/chat/chat_screen.dart b/lib/screens/chat/chat_screen.dart index deb6169..91efd5d 100644 --- a/lib/screens/chat/chat_screen.dart +++ b/lib/screens/chat/chat_screen.dart @@ -70,7 +70,7 @@ class _ChatScreenState extends ConsumerState { final convTitle = ref .watch(conversationsProvider) - .valueOrNull + .value ?.where((c) => c.id == widget.conversationId) .firstOrNull ?.title; diff --git a/lib/screens/library/library_screen.dart b/lib/screens/library/library_screen.dart index a66578b..e39a216 100644 --- a/lib/screens/library/library_screen.dart +++ b/lib/screens/library/library_screen.dart @@ -228,8 +228,8 @@ class _LibraryScreenState extends ConsumerState { AsyncValue> notesAsync, AsyncValue> tasksAsync, ) { - final notes = notesAsync.valueOrNull ?? []; - final tasks = tasksAsync.valueOrNull ?? []; + final notes = notesAsync.value ?? []; + final tasks = tasksAsync.value ?? []; // Merge and sort by updatedAt desc final items = <(DateTime, Widget)>[]; diff --git a/lib/screens/notes/note_detail_screen.dart b/lib/screens/notes/note_detail_screen.dart index d547759..19dea9a 100644 --- a/lib/screens/notes/note_detail_screen.dart +++ b/lib/screens/notes/note_detail_screen.dart @@ -16,7 +16,7 @@ class NoteDetailScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final noteAsync = ref.watch(noteDetailProvider(noteId)); - final allNotes = ref.watch(notesProvider).valueOrNull ?? []; + final allNotes = ref.watch(notesProvider).value ?? []; void navigateByTitle(String title) { final matches = allNotes.where(