diff --git a/lib/core/auth/auth_state_manager.dart b/lib/core/auth/auth_state_manager.dart index 662dfe5..f442388 100644 --- a/lib/core/auth/auth_state_manager.dart +++ b/lib/core/auth/auth_state_manager.dart @@ -78,23 +78,28 @@ enum AuthStatus { } /// Unified auth state manager - single source of truth for all auth operations -class AuthStateManager extends StateNotifier { - AuthStateManager(this._ref) - : super(const AuthState(status: AuthStatus.initial)) { - _initialize(); - } - - final Ref _ref; +class AuthStateManager extends Notifier { final AuthCacheManager _cacheManager = AuthCacheManager(); // Prevent overlapping silent-login attempts from multiple triggers Future? _silentLoginFuture; + bool _initialized = false; + + @override + AuthState build() { + if (!_initialized) { + _initialized = true; + Future.microtask(_initialize); + } + + return const AuthState(status: AuthStatus.initial); + } /// Initialize auth state from storage Future _initialize() async { state = state.copyWith(status: AuthStatus.loading, isLoading: true); try { - final storage = _ref.read(optimizedStorageServiceProvider); + final storage = ref.read(optimizedStorageServiceProvider); final token = await storage.getAuthToken(); if (token != null && token.isNotEmpty) { @@ -171,7 +176,7 @@ class AuthStateManager extends StateNotifier { // Ensure API service is available await _ensureApiServiceAvailable(); - final api = _ref.read(apiServiceProvider); + final api = ref.read(apiServiceProvider); if (api == null) { throw Exception('No server connection available'); } @@ -192,12 +197,12 @@ class AuthStateManager extends StateNotifier { await api.getCurrentUser(); // Just validate, don't store user data yet // Save token to storage - final storage = _ref.read(optimizedStorageServiceProvider); + final storage = ref.read(optimizedStorageServiceProvider); await storage.saveAuthToken(tokenStr); // Save API key if requested (for convenience, though less secure than credentials) if (rememberCredentials) { - final activeServer = await _ref.read(activeServerProvider.future); + final activeServer = await ref.read(activeServerProvider.future); if (activeServer != null) { // Store API key as a special credential type await storage.saveCredentials( @@ -260,7 +265,7 @@ class AuthStateManager extends StateNotifier { try { // Ensure API service is available (active server/provider rebuild race) await _ensureApiServiceAvailable(); - final api = _ref.read(apiServiceProvider); + final api = ref.read(apiServiceProvider); if (api == null) { throw Exception('No server connection available'); } @@ -280,12 +285,12 @@ class AuthStateManager extends StateNotifier { } // Save token to storage - final storage = _ref.read(optimizedStorageServiceProvider); + final storage = ref.read(optimizedStorageServiceProvider); await storage.saveAuthToken(tokenStr); // Save credentials if requested if (rememberCredentials) { - final activeServer = await _ref.read(activeServerProvider.future); + final activeServer = await ref.read(activeServerProvider.future); if (activeServer != null) { await storage.saveCredentials( serverId: activeServer.id, @@ -332,7 +337,7 @@ class AuthStateManager extends StateNotifier { }) async { final end = DateTime.now().add(timeout); while (DateTime.now().isBefore(end)) { - final api = _ref.read(apiServiceProvider); + final api = ref.read(apiServiceProvider); if (api != null) return; await Future.delayed(const Duration(milliseconds: 50)); } @@ -363,7 +368,7 @@ class AuthStateManager extends StateNotifier { ); try { - final storage = _ref.read(optimizedStorageServiceProvider); + final storage = ref.read(optimizedStorageServiceProvider); final savedCredentials = await storage.getSavedCredentials(); if (savedCredentials == null) { @@ -381,10 +386,10 @@ class AuthStateManager extends StateNotifier { // Set active server if needed await storage.setActiveServerId(serverId); - _ref.invalidate(activeServerProvider); + ref.invalidate(activeServerProvider); // Wait for server connection - final activeServer = await _ref.read(activeServerProvider.future); + final activeServer = await ref.read(activeServerProvider.future); if (activeServer == null) { await storage.setActiveServerId(null); state = state.copyWith( @@ -411,7 +416,7 @@ class AuthStateManager extends StateNotifier { e.toString().contains('403') || e.toString().contains('authentication') || e.toString().contains('unauthorized')) { - final storage = _ref.read(optimizedStorageServiceProvider); + final storage = ref.read(optimizedStorageServiceProvider); await storage.deleteSavedCredentials(); } @@ -434,7 +439,7 @@ class AuthStateManager extends StateNotifier { } // Clear token from storage - final storage = _ref.read(optimizedStorageServiceProvider); + final storage = ref.read(optimizedStorageServiceProvider); await storage.deleteAuthToken(); // Update state @@ -461,7 +466,7 @@ class AuthStateManager extends StateNotifier { try { // Call server logout if possible - final api = _ref.read(apiServiceProvider); + final api = ref.read(apiServiceProvider); if (api != null) { try { await api.logout(); @@ -471,7 +476,7 @@ class AuthStateManager extends StateNotifier { } // Clear all local auth data - final storage = _ref.read(optimizedStorageServiceProvider); + final storage = ref.read(optimizedStorageServiceProvider); await storage.clearAuthData(); // Update state @@ -524,7 +529,7 @@ class AuthStateManager extends StateNotifier { /// Load complete user data from server Future _loadServerUserData() async { try { - final api = _ref.read(apiServiceProvider); + final api = ref.read(apiServiceProvider); if (api != null && state.isAuthenticated) { // Check if we already have user data from token validation if (state.user != null) { @@ -546,7 +551,7 @@ class AuthStateManager extends StateNotifier { /// Update API service with current token void _updateApiServiceToken(String token) { - final api = _ref.read(apiServiceProvider); + final api = ref.read(apiServiceProvider); api?.updateAuthToken(token); } @@ -582,7 +587,7 @@ class AuthStateManager extends StateNotifier { // Server validation (async with timeout) try { - final api = _ref.read(apiServiceProvider); + final api = ref.read(apiServiceProvider); if (api == null) { debugPrint('DEBUG: No API service available for token validation'); return formatResult.isValid; // Fall back to format validation @@ -630,7 +635,7 @@ class AuthStateManager extends StateNotifier { } try { - final storage = _ref.read(optimizedStorageServiceProvider); + final storage = ref.read(optimizedStorageServiceProvider); final hasCredentials = await storage.hasCredentials(); // Cache the result @@ -668,10 +673,9 @@ class AuthStateManager extends StateNotifier { } /// Provider for the unified auth state manager -final authStateManagerProvider = - StateNotifierProvider((ref) { - return AuthStateManager(ref); - }); +final authStateManagerProvider = NotifierProvider( + AuthStateManager.new, +); /// Computed providers for common auth state queries final isAuthenticatedProvider = Provider((ref) { diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 32579ad..5c2b6ad 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -63,28 +63,24 @@ final optimizedStorageServiceProvider = Provider(( }); // Theme provider -final themeModeProvider = StateNotifierProvider(( - ref, -) { - final storage = ref.watch(optimizedStorageServiceProvider); - return ThemeModeNotifier(storage); -}); +final themeModeProvider = NotifierProvider( + ThemeModeNotifier.new, +); -class ThemeModeNotifier extends StateNotifier { - final OptimizedStorageService _storage; +class ThemeModeNotifier extends Notifier { + late final OptimizedStorageService _storage; - ThemeModeNotifier(this._storage) : super(ThemeMode.system) { - _loadTheme(); - } - - void _loadTheme() { - final mode = _storage.getThemeMode(); - if (mode != null) { - state = ThemeMode.values.firstWhere( - (e) => e.toString() == mode, + @override + ThemeMode build() { + _storage = ref.watch(optimizedStorageServiceProvider); + final storedMode = _storage.getThemeMode(); + if (storedMode != null) { + return ThemeMode.values.firstWhere( + (e) => e.toString() == storedMode, orElse: () => ThemeMode.system, ); } + return ThemeMode.system; } void setTheme(ThemeMode mode) { @@ -94,25 +90,21 @@ class ThemeModeNotifier extends StateNotifier { } // Locale provider -final localeProvider = StateNotifierProvider((ref) { - final storage = ref.watch(optimizedStorageServiceProvider); - return LocaleNotifier(storage); -}); +final localeProvider = NotifierProvider( + LocaleNotifier.new, +); -class LocaleNotifier extends StateNotifier { - final OptimizedStorageService _storage; +class LocaleNotifier extends Notifier { + late final OptimizedStorageService _storage; - LocaleNotifier(this._storage) : super(null) { - _loadLocale(); - } - - void _loadLocale() { + @override + Locale? build() { + _storage = ref.watch(optimizedStorageServiceProvider); final code = _storage.getLocaleCode(); if (code != null && code.isNotEmpty) { - state = Locale(code); - } else { - state = null; // system + return Locale(code); } + return null; // system default } Future setLocale(Locale? locale) async { @@ -325,17 +317,38 @@ final modelsProvider = FutureProvider>((ref) async { } }); -final selectedModelProvider = StateProvider((ref) => null); +final selectedModelProvider = NotifierProvider( + SelectedModelNotifier.new, +); // Track if the current model selection is manual (user-selected) or automatic (default) -final isManualModelSelectionProvider = StateProvider((ref) => false); +final isManualModelSelectionProvider = + NotifierProvider( + IsManualModelSelectionNotifier.new, + ); + +class SelectedModelNotifier extends Notifier { + @override + Model? build() => null; + + void set(Model? model) => state = model; + + void clear() => state = null; +} + +class IsManualModelSelectionNotifier extends Notifier { + @override + bool build() => false; + + void set(bool value) => state = value; +} // Listen for settings changes and reset manual selection when default model changes final _settingsWatcherProvider = Provider((ref) { ref.listen(appSettingsProvider, (previous, next) { if (previous?.defaultModel != next.defaultModel) { // Reset manual selection when default model changes - ref.read(isManualModelSelectionProvider.notifier).state = false; + ref.read(isManualModelSelectionProvider.notifier).set(false); } }); }); @@ -376,7 +389,7 @@ final defaultModelAutoSelectionProvider = Provider((ref) { (models.isNotEmpty ? models.first : null); if (selected != null) { - ref.read(selectedModelProvider.notifier).state = selected; + ref.read(selectedModelProvider.notifier).set(selected); foundation.debugPrint( 'DEBUG: Auto-applied default model (by ID): ${selected.name}', ); @@ -391,7 +404,17 @@ final defaultModelAutoSelectionProvider = Provider((ref) { }); // Cache timestamp for conversations to prevent rapid re-fetches -final _conversationsCacheTimestamp = StateProvider((ref) => null); +final _conversationsCacheTimestamp = + NotifierProvider<_ConversationsCacheTimestampNotifier, DateTime?>( + _ConversationsCacheTimestampNotifier.new, + ); + +class _ConversationsCacheTimestampNotifier extends Notifier { + @override + DateTime? build() => null; + + void set(DateTime? timestamp) => state = timestamp; +} // Conversation providers - Now using correct OpenWebUI API with caching final conversationsProvider = FutureProvider>((ref) async { @@ -585,7 +608,7 @@ final conversationsProvider = FutureProvider>((ref) async { ); // Update cache timestamp - ref.read(_conversationsCacheTimestamp.notifier).state = DateTime.now(); + ref.read(_conversationsCacheTimestamp.notifier).set(DateTime.now()); return sortedConversations; } catch (e) { @@ -597,7 +620,7 @@ final conversationsProvider = FutureProvider>((ref) async { ); // Update cache timestamp - ref.read(_conversationsCacheTimestamp.notifier).state = DateTime.now(); + ref.read(_conversationsCacheTimestamp.notifier).set(DateTime.now()); return conversations; // Return original conversations if folder fetch fails } @@ -618,7 +641,19 @@ final conversationsProvider = FutureProvider>((ref) async { } }); -final activeConversationProvider = StateProvider((ref) => null); +final activeConversationProvider = + NotifierProvider( + ActiveConversationNotifier.new, + ); + +class ActiveConversationNotifier extends Notifier { + @override + Conversation? build() => null; + + void set(Conversation? conversation) => state = conversation; + + void clear() => state = null; +} // Provider to load full conversation with messages final loadConversationProvider = FutureProvider.family(( @@ -662,7 +697,7 @@ final defaultModelProvider = FutureProvider((ref) async { if (models.isNotEmpty) { final defaultModel = models.first; if (!ref.read(isManualModelSelectionProvider)) { - ref.read(selectedModelProvider.notifier).state = defaultModel; + ref.read(selectedModelProvider.notifier).set(defaultModel); foundation.debugPrint( 'DEBUG: Auto-selected demo model: ${defaultModel.name}', ); @@ -692,7 +727,7 @@ final defaultModelProvider = FutureProvider((ref) async { name: storedDefaultId, supportsStreaming: true, ); - ref.read(selectedModelProvider.notifier).state = placeholder; + ref.read(selectedModelProvider.notifier).set(placeholder); } // Reconcile against real models in background Future.microtask(() async { @@ -709,7 +744,7 @@ final defaultModelProvider = FutureProvider((ref) async { } resolved ??= models.isNotEmpty ? models.first : null; if (resolved != null && !ref.read(isManualModelSelectionProvider)) { - ref.read(selectedModelProvider.notifier).state = resolved; + ref.read(selectedModelProvider.notifier).set(resolved); foundation.debugPrint( 'DEBUG: Reconciled default model to ${resolved.name}', ); @@ -730,7 +765,7 @@ final defaultModelProvider = FutureProvider((ref) async { name: serverDefault, supportsStreaming: true, ); - ref.read(selectedModelProvider.notifier).state = placeholder; + ref.read(selectedModelProvider.notifier).set(placeholder); } // Reconcile against real models in background Future.microtask(() async { @@ -747,7 +782,7 @@ final defaultModelProvider = FutureProvider((ref) async { } resolved ??= models.isNotEmpty ? models.first : null; if (resolved != null && !ref.read(isManualModelSelectionProvider)) { - ref.read(selectedModelProvider.notifier).state = resolved; + ref.read(selectedModelProvider.notifier).set(resolved); foundation.debugPrint( 'DEBUG: Reconciled server default to ${resolved.name}', ); @@ -766,7 +801,7 @@ final defaultModelProvider = FutureProvider((ref) async { } final selectedModel = models.first; if (!ref.read(isManualModelSelectionProvider)) { - ref.read(selectedModelProvider.notifier).state = selectedModel; + ref.read(selectedModelProvider.notifier).set(selectedModel); foundation.debugPrint( 'DEBUG: Set default model (fallback): ${selectedModel.name}', ); @@ -806,7 +841,16 @@ final backgroundModelLoadProvider = Provider((ref) { }); // Search query provider -final searchQueryProvider = StateProvider((ref) => ''); +final searchQueryProvider = NotifierProvider( + SearchQueryNotifier.new, +); + +class SearchQueryNotifier extends Notifier { + @override + String build() => ''; + + void set(String query) => state = query; +} // Server-side search provider for chats final serverSearchProvider = FutureProvider.family, String>(( @@ -1002,17 +1046,29 @@ final archivedConversationsProvider = Provider>((ref) { }); // Reviewer mode provider (persisted) -final reviewerModeProvider = StateNotifierProvider( - (ref) => ReviewerModeNotifier(ref.watch(optimizedStorageServiceProvider)), +final reviewerModeProvider = NotifierProvider( + ReviewerModeNotifier.new, ); -class ReviewerModeNotifier extends StateNotifier { - final OptimizedStorageService _storage; - ReviewerModeNotifier(this._storage) : super(false) { - _load(); +class ReviewerModeNotifier extends Notifier { + late final OptimizedStorageService _storage; + bool _initialized = false; + + @override + bool build() { + _storage = ref.watch(optimizedStorageServiceProvider); + if (!_initialized) { + _initialized = true; + Future.microtask(_load); + } + return false; } + Future _load() async { final enabled = await _storage.getReviewerMode(); + if (!ref.mounted) { + return; + } state = enabled; } diff --git a/lib/core/providers/app_startup_providers.dart b/lib/core/providers/app_startup_providers.dart index 23a2e15..99aee23 100644 --- a/lib/core/providers/app_startup_providers.dart +++ b/lib/core/providers/app_startup_providers.dart @@ -16,19 +16,37 @@ import '../utils/debug_logger.dart'; enum _ConversationWarmupStatus { idle, warming, complete } final _conversationWarmupStatusProvider = - StateProvider<_ConversationWarmupStatus>( - (ref) => _ConversationWarmupStatus.idle, + NotifierProvider< + _ConversationWarmupStatusNotifier, + _ConversationWarmupStatus + >(_ConversationWarmupStatusNotifier.new); + +final _conversationWarmupLastAttemptProvider = + NotifierProvider<_ConversationWarmupLastAttemptNotifier, DateTime?>( + _ConversationWarmupLastAttemptNotifier.new, ); -final _conversationWarmupLastAttemptProvider = StateProvider( - (ref) => null, -); +class _ConversationWarmupStatusNotifier + extends Notifier<_ConversationWarmupStatus> { + @override + _ConversationWarmupStatus build() => _ConversationWarmupStatus.idle; + + void set(_ConversationWarmupStatus status) => state = status; +} + +class _ConversationWarmupLastAttemptNotifier extends Notifier { + @override + DateTime? build() => null; + + void set(DateTime? value) => state = value; +} void _scheduleConversationWarmup(Ref ref, {bool force = false}) { final navState = ref.read(authNavigationStateProvider); if (navState != AuthNavigationState.authenticated) { - ref.read(_conversationWarmupStatusProvider.notifier).state = - _ConversationWarmupStatus.idle; + ref + .read(_conversationWarmupStatusProvider.notifier) + .set(_ConversationWarmupStatus.idle); return; } @@ -38,7 +56,7 @@ void _scheduleConversationWarmup(Ref ref, {bool force = false}) { } final statusController = ref.read(_conversationWarmupStatusProvider.notifier); - final status = statusController.state; + final status = ref.read(_conversationWarmupStatusProvider); if (!force) { if (status == _ConversationWarmupStatus.warming || @@ -56,28 +74,28 @@ void _scheduleConversationWarmup(Ref ref, {bool force = false}) { now.difference(lastAttempt) < const Duration(seconds: 30)) { return; } - ref.read(_conversationWarmupLastAttemptProvider.notifier).state = now; + ref.read(_conversationWarmupLastAttemptProvider.notifier).set(now); - statusController.state = _ConversationWarmupStatus.warming; + statusController.set(_ConversationWarmupStatus.warming); Future.microtask(() async { try { final existing = ref.read(conversationsProvider); if (existing.hasValue) { - statusController.state = _ConversationWarmupStatus.complete; + statusController.set(_ConversationWarmupStatus.complete); return; } if (existing.hasError) { ref.invalidate(conversationsProvider); } final conversations = await ref.read(conversationsProvider.future); - statusController.state = _ConversationWarmupStatus.complete; + statusController.set(_ConversationWarmupStatus.complete); DebugLogger.info( 'Background chats warmup fetched ${conversations.length} conversations', ); } catch (error) { DebugLogger.warning('Background chats warmup failed: $error'); - statusController.state = _ConversationWarmupStatus.idle; + statusController.set(_ConversationWarmupStatus.idle); } }); } @@ -148,8 +166,9 @@ final appStartupFlowProvider = Provider((ref) { }); } else { // Reset warmup state when leaving authenticated flow - ref.read(_conversationWarmupStatusProvider.notifier).state = - _ConversationWarmupStatus.idle; + ref + .read(_conversationWarmupStatusProvider.notifier) + .set(_ConversationWarmupStatus.idle); } }); @@ -167,8 +186,9 @@ final appStartupFlowProvider = Provider((ref) { ) { final wasReady = previous?.hasValue == true || previous?.hasError == true; if (wasReady && next.isLoading) { - ref.read(_conversationWarmupStatusProvider.notifier).state = - _ConversationWarmupStatus.idle; + ref + .read(_conversationWarmupStatusProvider.notifier) + .set(_ConversationWarmupStatus.idle); Future.microtask(() => _scheduleConversationWarmup(ref, force: true)); } }); @@ -195,8 +215,9 @@ class _ForegroundRefreshObserver extends WidgetsBindingObserver { Future.microtask(() { try { _ref.invalidate(conversationsProvider); - _ref.read(_conversationWarmupStatusProvider.notifier).state = - _ConversationWarmupStatus.idle; + _ref + .read(_conversationWarmupStatusProvider.notifier) + .set(_ConversationWarmupStatus.idle); } catch (_) {} _scheduleConversationWarmup(_ref, force: true); }); diff --git a/lib/core/services/animation_service.dart b/lib/core/services/animation_service.dart index 960ebc0..f9def0e 100644 --- a/lib/core/services/animation_service.dart +++ b/lib/core/services/animation_service.dart @@ -208,12 +208,29 @@ class AnimationService { enum PageTransitionType { fade, slide, scale } /// Provider for reduced motion preference -final reducedMotionProvider = StateProvider((ref) => false); +final reducedMotionProvider = NotifierProvider( + ReducedMotionNotifier.new, +); /// Provider for animation performance settings -final animationPerformanceProvider = StateProvider((ref) { - return AnimationPerformance.adaptive; -}); +final animationPerformanceProvider = + NotifierProvider( + AnimationPerformanceNotifier.new, + ); + +class ReducedMotionNotifier extends Notifier { + @override + bool build() => false; + + void set(bool value) => state = value; +} + +class AnimationPerformanceNotifier extends Notifier { + @override + AnimationPerformance build() => AnimationPerformance.adaptive; + + void set(AnimationPerformance performance) => state = performance; +} /// Animation performance levels enum AnimationPerformance { @@ -225,8 +242,8 @@ enum AnimationPerformance { /// Provider for managing animation settings final animationSettingsProvider = - StateNotifierProvider( - (ref) => AnimationSettingsNotifier(), + NotifierProvider( + AnimationSettingsNotifier.new, ); class AnimationSettings { @@ -253,8 +270,9 @@ class AnimationSettings { } } -class AnimationSettingsNotifier extends StateNotifier { - AnimationSettingsNotifier() : super(const AnimationSettings()); +class AnimationSettingsNotifier extends Notifier { + @override + AnimationSettings build() => const AnimationSettings(); void setReduceMotion(bool reduce) { state = state.copyWith(reduceMotion: reduce); diff --git a/lib/core/services/settings_service.dart b/lib/core/services/settings_service.dart index 84745c4..8eafddd 100644 --- a/lib/core/services/settings_service.dart +++ b/lib/core/services/settings_service.dart @@ -20,9 +20,11 @@ class SettingsService { static const String _voiceHoldToTalkKey = 'voice_hold_to_talk'; static const String _voiceAutoSendKey = 'voice_auto_send_final'; // Realtime transport preference - static const String _socketTransportModeKey = 'socket_transport_mode'; // 'auto' or 'ws' + static const String _socketTransportModeKey = + 'socket_transport_mode'; // 'auto' or 'ws' // Quick pill visibility selections (max 2) - static const String _quickPillsKey = 'quick_pills'; // StringList of identifiers e.g. ['web','image','tools'] + static const String _quickPillsKey = + 'quick_pills'; // StringList of identifiers e.g. ['web','image','tools'] // Chat input behavior static const String _sendOnEnterKey = 'send_on_enter'; @@ -335,9 +337,14 @@ class AppSettings { highContrast: highContrast ?? this.highContrast, largeText: largeText ?? this.largeText, darkMode: darkMode ?? this.darkMode, - defaultModel: defaultModel is _DefaultValue ? this.defaultModel : defaultModel as String?, - omitProviderInModelName: omitProviderInModelName ?? this.omitProviderInModelName, - voiceLocaleId: voiceLocaleId is _DefaultValue ? this.voiceLocaleId : voiceLocaleId as String?, + defaultModel: defaultModel is _DefaultValue + ? this.defaultModel + : defaultModel as String?, + omitProviderInModelName: + omitProviderInModelName ?? this.omitProviderInModelName, + voiceLocaleId: voiceLocaleId is _DefaultValue + ? this.voiceLocaleId + : voiceLocaleId as String?, voiceHoldToTalk: voiceHoldToTalk ?? this.voiceHoldToTalk, voiceAutoSendFinal: voiceAutoSendFinal ?? this.voiceAutoSendFinal, socketTransportMode: socketTransportMode ?? this.socketTransportMode, @@ -363,7 +370,7 @@ class AppSettings { other.voiceAutoSendFinal == voiceAutoSendFinal && other.sendOnEnter == sendOnEnter && _listEquals(other.quickPills, quickPills); - // socketTransportMode intentionally not included in == to avoid frequent rebuilds + // socketTransportMode intentionally not included in == to avoid frequent rebuilds } @override @@ -397,18 +404,27 @@ bool _listEquals(List a, List b) { } /// Provider for app settings -final appSettingsProvider = - StateNotifierProvider( - (ref) => AppSettingsNotifier(), - ); +final appSettingsProvider = NotifierProvider( + AppSettingsNotifier.new, +); -class AppSettingsNotifier extends StateNotifier { - AppSettingsNotifier() : super(const AppSettings()) { - _loadSettings(); +class AppSettingsNotifier extends Notifier { + bool _initialized = false; + + @override + AppSettings build() { + if (!_initialized) { + _initialized = true; + Future.microtask(_loadSettings); + } + return const AppSettings(); } Future _loadSettings() async { final settings = await SettingsService.loadSettings(); + if (!ref.mounted) { + return; + } state = settings; } diff --git a/lib/core/services/share_receiver_service.dart b/lib/core/services/share_receiver_service.dart index 99841b5..d29ea86 100644 --- a/lib/core/services/share_receiver_service.dart +++ b/lib/core/services/share_receiver_service.dart @@ -25,7 +25,17 @@ class SharedPayload { } /// Holds a pending shared payload until the app is ready (e.g., authed + model loaded) -final pendingSharedPayloadProvider = StateProvider((_) => null); +final pendingSharedPayloadProvider = + NotifierProvider( + PendingSharedPayloadNotifier.new, + ); + +class PendingSharedPayloadNotifier extends Notifier { + @override + SharedPayload? build() => null; + + void set(SharedPayload? payload) => state = payload; +} /// Initializes listening to OS share intents and handles them final shareReceiverInitializerProvider = Provider((ref) { @@ -45,7 +55,7 @@ final shareReceiverInitializerProvider = Provider((ref) { model != null && isOnChatRoute) { _processPayload(ref, pending); - ref.read(pendingSharedPayloadProvider.notifier).state = null; + ref.read(pendingSharedPayloadProvider.notifier).set(null); } } @@ -70,7 +80,7 @@ final shareReceiverInitializerProvider = Provider((ref) { final dynamic media = await handler.getInitialSharedMedia(); final payload = _toPayload(media); if (payload.hasAnything) { - ref.read(pendingSharedPayloadProvider.notifier).state = payload; + ref.read(pendingSharedPayloadProvider.notifier).set(payload); maybeProcessPending(); } } catch (e) { @@ -83,7 +93,7 @@ final shareReceiverInitializerProvider = Provider((ref) { try { final payload = _toPayload(media); if (payload.hasAnything) { - ref.read(pendingSharedPayloadProvider.notifier).state = payload; + ref.read(pendingSharedPayloadProvider.notifier).set(payload); maybeProcessPending(); } } catch (e) { @@ -178,10 +188,10 @@ Future _processPayload(Ref ref, SharedPayload payload) async { // Prefill text in the composer (do not auto-send) and request focus final text = payload.text?.trim(); if (text != null && text.isNotEmpty) { - ref.read(prefilledInputTextProvider.notifier).state = text; + ref.read(prefilledInputTextProvider.notifier).set(text); // Bump focus trigger to ensure input focuses after navigation/build final current = ref.read(inputFocusTriggerProvider); - ref.read(inputFocusTriggerProvider.notifier).state = current + 1; + ref.read(inputFocusTriggerProvider.notifier).set(current + 1); } // Do NOT create a server chat here. The chat is created on first send // (with server syncing + title generation) in chat_providers.dart. diff --git a/lib/features/chat/providers/chat_providers.dart b/lib/features/chat/providers/chat_providers.dart index fcac8f4..ef372f5 100644 --- a/lib/features/chat/providers/chat_providers.dart +++ b/lib/features/chat/providers/chat_providers.dart @@ -23,90 +23,155 @@ const bool kSocketVerboseLogging = false; // Chat messages for current conversation final chatMessagesProvider = - StateNotifierProvider>((ref) { - return ChatMessagesNotifier(ref); - }); + NotifierProvider>( + ChatMessagesNotifier.new, + ); // Loading state for conversation (used to show chat skeletons during fetch) -final isLoadingConversationProvider = StateProvider((ref) => false); +final isLoadingConversationProvider = + NotifierProvider( + IsLoadingConversationNotifier.new, + ); // Prefilled input text (e.g., when sharing text from other apps) -final prefilledInputTextProvider = StateProvider((ref) => null); +final prefilledInputTextProvider = + NotifierProvider( + PrefilledInputTextNotifier.new, + ); // Trigger to request focus on the chat input (increment to signal) -final inputFocusTriggerProvider = StateProvider((ref) => 0); +final inputFocusTriggerProvider = + NotifierProvider( + InputFocusTriggerNotifier.new, + ); // Whether the chat composer currently has focus -final composerHasFocusProvider = StateProvider((ref) => false); +final composerHasFocusProvider = NotifierProvider( + ComposerFocusNotifier.new, +); -class ChatMessagesNotifier extends StateNotifier> { - final Ref _ref; +class IsLoadingConversationNotifier extends Notifier { + @override + bool build() => false; + + void set(bool value) => state = value; +} + +class PrefilledInputTextNotifier extends Notifier { + @override + String? build() => null; + + void set(String? value) => state = value; + + void clear() => state = null; +} + +class InputFocusTriggerNotifier extends Notifier { + @override + int build() => 0; + + void set(int value) => state = value; + + int increment() { + final next = state + 1; + state = next; + return next; + } +} + +class ComposerFocusNotifier extends Notifier { + @override + bool build() => false; + + void set(bool value) => state = value; +} + +class ChatMessagesNotifier extends Notifier> { StreamSubscription? _messageStream; ProviderSubscription? _conversationListener; final List _subscriptions = []; // Activity-based watchdog to prevent stuck typing indicator InactivityWatchdog? _typingWatchdog; - ChatMessagesNotifier(this._ref) : super([]) { - // Load messages when conversation changes with proper cleanup - _conversationListener = _ref.listen(activeConversationProvider, ( - previous, - next, - ) { - debugPrint('Conversation changed: ${previous?.id} -> ${next?.id}'); + bool _initialized = false; - // Only react when the conversation actually changes - if (previous?.id == next?.id) { - // If same conversation but server updated it (e.g., title/content), avoid overwriting - // locally streamed assistant content with an outdated server copy. - if (previous?.updatedAt != next?.updatedAt) { - final serverMessages = next?.messages ?? const []; - // Primary rule: adopt server messages when there are strictly more of them. - if (serverMessages.length > state.length) { - state = serverMessages; - return; - } + @override + List build() { + if (!_initialized) { + _initialized = true; + _conversationListener = ref.listen(activeConversationProvider, ( + previous, + next, + ) { + debugPrint('Conversation changed: ${previous?.id} -> ${next?.id}'); - // Secondary rule: if counts are equal but the last assistant message grew, - // adopt the server copy to recover from missed socket events. - if (serverMessages.isNotEmpty && state.isNotEmpty) { - final serverLast = serverMessages.last; - final localLast = state.last; - final serverText = serverLast.content.trim(); - final localText = localLast.content.trim(); - final sameLastId = serverLast.id == localLast.id; - final isAssistant = serverLast.role == 'assistant'; - final serverHasMore = - serverText.isNotEmpty && serverText.length > localText.length; - final localEmptyButServerHas = - localText.isEmpty && serverText.isNotEmpty; - if (sameLastId && - isAssistant && - (serverHasMore || localEmptyButServerHas)) { + // Only react when the conversation actually changes + if (previous?.id == next?.id) { + // If same conversation but server updated it (e.g., title/content), avoid overwriting + // locally streamed assistant content with an outdated server copy. + if (previous?.updatedAt != next?.updatedAt) { + final serverMessages = next?.messages ?? const []; + // Primary rule: adopt server messages when there are strictly more of them. + if (serverMessages.length > state.length) { state = serverMessages; return; } + + // Secondary rule: if counts are equal but the last assistant message grew, + // adopt the server copy to recover from missed socket events. + if (serverMessages.isNotEmpty && state.isNotEmpty) { + final serverLast = serverMessages.last; + final localLast = state.last; + final serverText = serverLast.content.trim(); + final localText = localLast.content.trim(); + final sameLastId = serverLast.id == localLast.id; + final isAssistant = serverLast.role == 'assistant'; + final serverHasMore = + serverText.isNotEmpty && serverText.length > localText.length; + final localEmptyButServerHas = + localText.isEmpty && serverText.isNotEmpty; + if (sameLastId && + isAssistant && + (serverHasMore || localEmptyButServerHas)) { + state = serverMessages; + return; + } + } } + return; } - return; - } - // Cancel any existing message stream when switching conversations - _cancelMessageStream(); - // Also cancel typing guard on conversation switch - _cancelTypingGuard(); + // Cancel any existing message stream when switching conversations + _cancelMessageStream(); + // Also cancel typing guard on conversation switch + _cancelTypingGuard(); - if (next != null) { - state = next.messages; + if (next != null) { + state = next.messages; - // Update selected model if conversation has a different model - _updateModelForConversation(next); - } else { - state = []; - } - }); + // Update selected model if conversation has a different model + _updateModelForConversation(next); + } else { + state = []; + } + }); - // ProviderSubscription will be cleaned up in dispose method + ref.onDispose(() { + for (final subscription in _subscriptions) { + subscription.cancel(); + } + _subscriptions.clear(); + + _cancelMessageStream(); + _cancelTypingGuard(); + + _conversationListener?.close(); + _conversationListener = null; + }); + } + + final activeConversation = ref.read(activeConversationProvider); + return activeConversation?.messages ?? const []; } void _addSubscription(StreamSubscription subscription) { @@ -137,8 +202,8 @@ class ChatMessagesNotifier extends StateNotifier> { // Attempt a soft recovery: if content is still empty, try fetching final content from server if ((last.content).trim().isEmpty) { try { - final apiSvc = _ref.read(apiServiceProvider); - final activeConv = _ref.read(activeConversationProvider); + final apiSvc = ref.read(apiServiceProvider); + final activeConv = ref.read(activeConversationProvider); final msgId = last.id; final chatId = activeConv?.id; if (apiSvc != null && chatId != null && chatId.isNotEmpty) { @@ -228,9 +293,9 @@ class ChatMessagesNotifier extends StateNotifier> { final isImageGenFlow = (meta['imageGenerationFlow'] == true); // Also consult global toggles if metadata not present - final globalWebSearch = _ref.read(webSearchEnabledProvider); - final webSearchAvailable = _ref.read(webSearchAvailableProvider); - final globalImageGen = _ref.read(imageGenerationEnabledProvider); + final globalWebSearch = ref.read(webSearchEnabledProvider); + final webSearchAvailable = ref.read(webSearchAvailableProvider); + final globalImageGen = ref.read(imageGenerationEnabledProvider); // Extend guard windows to tolerate long reasoning/tools (> 1 min) if (isWebSearchFlow || (globalWebSearch && webSearchAvailable)) { @@ -262,13 +327,13 @@ class ChatMessagesNotifier extends StateNotifier> { return; } - final currentSelectedModel = _ref.read(selectedModelProvider); + final currentSelectedModel = ref.read(selectedModelProvider); // If the conversation's model is different from the currently selected one if (currentSelectedModel?.id != conversation.model) { // Get available models to find the matching one try { - final models = await _ref.read(modelsProvider.future); + final models = await ref.read(modelsProvider.future); if (models.isEmpty) { return; @@ -281,7 +346,7 @@ class ChatMessagesNotifier extends StateNotifier> { if (conversationModel != null) { // Update the selected model - _ref.read(selectedModelProvider.notifier).state = conversationModel; + ref.read(selectedModelProvider.notifier).set(conversationModel); } else { // Model not found in available models - silently continue } @@ -447,33 +512,9 @@ class ChatMessagesNotifier extends StateNotifier> { // can pick up updated titles and ordering once streaming completes. // Best-effort: ignore if ref lifecycle/context prevents invalidation. try { - _ref.invalidate(conversationsProvider); + ref.invalidate(conversationsProvider); } catch (_) {} } - - @override - void dispose() { - debugPrint( - 'ChatMessagesNotifier disposing - ${_subscriptions.length} subscriptions', - ); - - // Cancel all tracked subscriptions - for (final subscription in _subscriptions) { - subscription.cancel(); - } - _subscriptions.clear(); - - // Cancel message stream specifically - _cancelMessageStream(); - // Cancel any active typing guard - _cancelTypingGuard(); - - // Cancel conversation listener specifically - _conversationListener?.close(); - _conversationListener = null; - - super.dispose(); - } } // Pre-seed an assistant skeleton message (with a given id or a new one), @@ -526,8 +567,8 @@ Future _preseedAssistantAndPersist( // Persist the skeleton to the server so the web client sees a correct chain try { if (api != null && activeConv != null) { - final resolvedSystemPrompt = (systemPrompt != null && - systemPrompt.trim().isNotEmpty) + final resolvedSystemPrompt = + (systemPrompt != null && systemPrompt.trim().isNotEmpty) ? systemPrompt.trim() : activeConv.systemPrompt; final current = ref.read(chatMessagesProvider); @@ -567,45 +608,92 @@ String? _extractSystemPromptFromSettings(Map? settings) { // Start a new chat (unified function for both "New Chat" button and home screen) void startNewChat(dynamic ref) { // Clear active conversation - ref.read(activeConversationProvider.notifier).state = null; + ref.read(activeConversationProvider.notifier).clear(); // Clear messages ref.read(chatMessagesProvider.notifier).clearMessages(); } // Available tools provider -final availableToolsProvider = StateProvider>((ref) => []); +final availableToolsProvider = + NotifierProvider>( + AvailableToolsNotifier.new, + ); // Web search enabled state for API-based web search -final webSearchEnabledProvider = StateProvider((ref) => false); +final webSearchEnabledProvider = + NotifierProvider( + WebSearchEnabledNotifier.new, + ); // Image generation enabled state - behaves like web search -final imageGenerationEnabledProvider = StateProvider((ref) => false); +final imageGenerationEnabledProvider = + NotifierProvider( + ImageGenerationEnabledNotifier.new, + ); // Vision capable models provider -final visionCapableModelsProvider = StateProvider>((ref) { - final selectedModel = ref.watch(selectedModelProvider); - if (selectedModel == null) return []; - - // Check if the model supports vision (multimodal) - if (selectedModel.isMultimodal == true) { - return [selectedModel.id]; - } - - // For now, assume all models support vision unless explicitly marked - // This can be enhanced with proper model capability detection - return [selectedModel.id]; -}); +final visionCapableModelsProvider = + NotifierProvider>( + VisionCapableModelsNotifier.new, + ); // File upload capable models provider -final fileUploadCapableModelsProvider = StateProvider>((ref) { - final selectedModel = ref.watch(selectedModelProvider); - if (selectedModel == null) return []; +final fileUploadCapableModelsProvider = + NotifierProvider>( + FileUploadCapableModelsNotifier.new, + ); - // For now, assume all models support file upload - // This can be enhanced with proper model capability detection - return [selectedModel.id]; -}); +class AvailableToolsNotifier extends Notifier> { + @override + List build() => []; + + void set(List tools) => state = List.from(tools); +} + +class WebSearchEnabledNotifier extends Notifier { + @override + bool build() => false; + + void set(bool value) => state = value; +} + +class ImageGenerationEnabledNotifier extends Notifier { + @override + bool build() => false; + + void set(bool value) => state = value; +} + +class VisionCapableModelsNotifier extends Notifier> { + @override + List build() { + final selectedModel = ref.watch(selectedModelProvider); + if (selectedModel == null) { + return []; + } + + if (selectedModel.isMultimodal == true) { + return [selectedModel.id]; + } + + // For now, assume all models support vision unless explicitly marked + return [selectedModel.id]; + } +} + +class FileUploadCapableModelsNotifier extends Notifier> { + @override + List build() { + final selectedModel = ref.watch(selectedModelProvider); + if (selectedModel == null) { + return []; + } + + // For now, assume all models support file upload + return [selectedModel.id]; + } +} // Helper function to validate file size bool validateFileSize(int fileSize, int? maxSizeMB) { @@ -790,9 +878,10 @@ Future regenerateMessage( if ((activeConversation.systemPrompt == null || activeConversation.systemPrompt!.trim().isEmpty) && (userSystemPrompt?.isNotEmpty ?? false)) { - final updated = - activeConversation.copyWith(systemPrompt: userSystemPrompt); - ref.read(activeConversationProvider.notifier).state = updated; + final updated = activeConversation.copyWith( + systemPrompt: userSystemPrompt, + ); + ref.read(activeConversationProvider.notifier).set(updated); activeConversation = updated; } @@ -831,7 +920,8 @@ Future regenerateMessage( } final conversationSystemPrompt = activeConversation.systemPrompt?.trim(); - final effectiveSystemPrompt = (conversationSystemPrompt != null && + final effectiveSystemPrompt = + (conversationSystemPrompt != null && conversationSystemPrompt.isNotEmpty) ? conversationSystemPrompt : userSystemPrompt; @@ -840,10 +930,10 @@ Future regenerateMessage( (m) => (m['role']?.toString().toLowerCase() ?? '') == 'system', ); if (!hasSystemMessage) { - conversationMessages.insert( - 0, - {'role': 'system', 'content': effectiveSystemPrompt}, - ); + conversationMessages.insert(0, { + 'role': 'system', + 'content': effectiveSystemPrompt, + }); } } @@ -974,7 +1064,9 @@ Future regenerateMessage( // Resolve tool servers from user settings (if any) List>? toolServers; final uiSettings = userSettingsData?['ui'] as Map?; - final rawServers = uiSettings != null ? (uiSettings['toolServers'] as List?) : null; + final rawServers = uiSettings != null + ? (uiSettings['toolServers'] as List?) + : null; if (rawServers != null && rawServers.isNotEmpty) { try { toolServers = await _resolveToolServers(rawServers, api); @@ -1151,7 +1243,7 @@ Future _sendMessageInternal( ); // Set as active conversation locally - ref.read(activeConversationProvider.notifier).state = localConversation; + ref.read(activeConversationProvider.notifier).set(localConversation); activeConversation = localConversation; if (!reviewerMode) { @@ -1170,8 +1262,7 @@ Future _sendMessageInternal( ? serverConversation.messages : [userMessage], ); - ref.read(activeConversationProvider.notifier).state = - updatedConversation; + ref.read(activeConversationProvider.notifier).set(updatedConversation); activeConversation = updatedConversation; // Set messages in the messages provider to keep UI in sync @@ -1208,7 +1299,7 @@ Future _sendMessageInternal( activeConversation.systemPrompt!.trim().isEmpty) && (userSystemPrompt?.isNotEmpty ?? false)) { final updated = activeConversation.copyWith(systemPrompt: userSystemPrompt); - ref.read(activeConversationProvider.notifier).state = updated; + ref.read(activeConversationProvider.notifier).set(updated); activeConversation = updated; } @@ -1316,8 +1407,8 @@ Future _sendMessageInternal( } final conversationSystemPrompt = activeConversation?.systemPrompt?.trim(); - final effectiveSystemPrompt = (conversationSystemPrompt != null && - conversationSystemPrompt.isNotEmpty) + final effectiveSystemPrompt = + (conversationSystemPrompt != null && conversationSystemPrompt.isNotEmpty) ? conversationSystemPrompt : userSystemPrompt; if (effectiveSystemPrompt != null && effectiveSystemPrompt.isNotEmpty) { @@ -1325,10 +1416,10 @@ Future _sendMessageInternal( (m) => (m['role']?.toString().toLowerCase() ?? '') == 'system', ); if (!hasSystemMessage) { - conversationMessages.insert( - 0, - {'role': 'system', 'content': effectiveSystemPrompt}, - ); + conversationMessages.insert(0, { + 'role': 'system', + 'content': effectiveSystemPrompt, + }); } } @@ -1478,7 +1569,9 @@ Future _sendMessageInternal( // Resolve tool servers from user settings (if any) List>? toolServers; final uiSettings = userSettingsData?['ui'] as Map?; - final rawServers = uiSettings != null ? (uiSettings['toolServers'] as List?) : null; + final rawServers = uiSettings != null + ? (uiSettings['toolServers'] as List?) + : null; if (rawServers != null && rawServers.isNotEmpty) { try { toolServers = await _resolveToolServers(rawServers, api); @@ -2377,8 +2470,9 @@ Future _sendMessageInternal( updatedAt: DateTime.now(), ); - ref.read(activeConversationProvider.notifier).state = - updatedConversation; + ref + .read(activeConversationProvider.notifier) + .set(updatedConversation); } else { // Keep local messages and only refresh conversations list ref.invalidate(conversationsProvider); @@ -2614,7 +2708,7 @@ Future _checkForTitleInBackground( title: updatedConv.title, updatedAt: DateTime.now(), ); - ref.read(activeConversationProvider.notifier).state = updated; + ref.read(activeConversationProvider.notifier).set(updated); // Refresh the conversations list ref.invalidate(conversationsProvider); @@ -2679,7 +2773,7 @@ Future _saveConversationLocally(dynamic ref) async { } await storage.setString('conversations', jsonEncode(conversations)); - ref.read(activeConversationProvider.notifier).state = updatedConversation; + ref.read(activeConversationProvider.notifier).set(updatedConversation); ref.invalidate(conversationsProvider); } catch (e) { // Handle local storage errors silently @@ -2723,8 +2817,9 @@ Future pinConversation( // Update active conversation if it's the one being pinned final activeConversation = ref.read(activeConversationProvider); if (activeConversation?.id == conversationId) { - ref.read(activeConversationProvider.notifier).state = activeConversation! - .copyWith(pinned: pinned); + ref + .read(activeConversationProvider.notifier) + .set(activeConversation!.copyWith(pinned: pinned)); } } catch (e) { debugPrint('Error ${pinned ? 'pinning' : 'unpinning'} conversation: $e'); @@ -2743,7 +2838,7 @@ Future archiveConversation( // Update local state first if (activeConversation?.id == conversationId && archived) { - ref.read(activeConversationProvider.notifier).state = null; + ref.read(activeConversationProvider.notifier).clear(); ref.read(chatMessagesProvider.notifier).clearMessages(); } @@ -2761,7 +2856,7 @@ Future archiveConversation( // If server operation failed and we archived locally, restore the conversation if (activeConversation?.id == conversationId && archived) { - ref.read(activeConversationProvider.notifier).state = activeConversation; + ref.read(activeConversationProvider.notifier).set(activeConversation); // Messages will be restored through the listener } @@ -2796,7 +2891,7 @@ Future cloneConversation(WidgetRef ref, String conversationId) async { final clonedConversation = await api.cloneConversation(conversationId); // Set the cloned conversation as active - ref.read(activeConversationProvider.notifier).state = clonedConversation; + ref.read(activeConversationProvider.notifier).set(clonedConversation); // Load messages through the listener mechanism // The ChatMessagesNotifier will automatically load messages when activeConversation changes @@ -2841,7 +2936,7 @@ final regenerateLastMessageProvider = Provider Function()>((ref) { final prev = ref.read(imageGenerationEnabledProvider); try { // Force image generation enabled during regeneration - ref.read(imageGenerationEnabledProvider.notifier).state = true; + ref.read(imageGenerationEnabledProvider.notifier).set(true); await regenerateMessage( ref, lastUserMessage.content, @@ -2849,7 +2944,7 @@ final regenerateLastMessageProvider = Provider Function()>((ref) { ); } finally { // restore previous state - ref.read(imageGenerationEnabledProvider.notifier).state = prev; + ref.read(imageGenerationEnabledProvider.notifier).set(prev); } return; } diff --git a/lib/features/chat/providers/text_to_speech_provider.dart b/lib/features/chat/providers/text_to_speech_provider.dart index 8e57ba1..b67b1af 100644 --- a/lib/features/chat/providers/text_to_speech_provider.dart +++ b/lib/features/chat/providers/text_to_speech_provider.dart @@ -49,21 +49,32 @@ class TextToSpeechState { } } -class TextToSpeechController extends StateNotifier { - TextToSpeechController(this._service) : super(const TextToSpeechState()) { - _service.bindHandlers( - onStart: _handleStart, - onComplete: _handleCompletion, - onCancel: _handleCancellation, - onPause: _handlePause, - onContinue: _handleContinue, - onError: _handleError, - ); - } - - final TextToSpeechService _service; +class TextToSpeechController extends Notifier { + late final TextToSpeechService _service; + bool _handlersBound = false; Future? _initializationFuture; + @override + TextToSpeechState build() { + _service = ref.watch(textToSpeechServiceProvider); + if (!_handlersBound) { + _handlersBound = true; + _service.bindHandlers( + onStart: _handleStart, + onComplete: _handleCompletion, + onCancel: _handleCancellation, + onPause: _handlePause, + onContinue: _handleContinue, + onError: _handleError, + ); + + ref.onDispose(() { + unawaited(_service.stop()); + }); + } + return const TextToSpeechState(); + } + Future _ensureInitialized() { final existing = _initializationFuture; if (existing != null) { @@ -78,7 +89,7 @@ class TextToSpeechController extends StateNotifier { final future = _service .initialize() .then((available) { - if (!mounted) { + if (!ref.mounted) { return available; } @@ -90,7 +101,7 @@ class TextToSpeechController extends StateNotifier { return available; }) .catchError((error, _) { - if (!mounted) { + if (!ref.mounted) { return false; } @@ -132,7 +143,7 @@ class TextToSpeechController extends StateNotifier { final available = await _ensureInitialized(); if (!available) { - if (!mounted) { + if (!ref.mounted) { return; } state = state.copyWith( @@ -151,14 +162,14 @@ class TextToSpeechController extends StateNotifier { try { await _service.speak(text); - if (!mounted) { + if (!ref.mounted) { return; } if (state.status == TtsPlaybackStatus.loading) { state = state.copyWith(status: TtsPlaybackStatus.speaking); } } catch (e) { - if (!mounted) { + if (!ref.mounted) { return; } state = state.copyWith( @@ -178,7 +189,7 @@ class TextToSpeechController extends StateNotifier { Future stop() async { await _service.stop(); - if (!mounted) { + if (!ref.mounted) { return; } state = state.copyWith( @@ -189,14 +200,14 @@ class TextToSpeechController extends StateNotifier { } void _handleStart() { - if (!mounted) { + if (!ref.mounted) { return; } state = state.copyWith(status: TtsPlaybackStatus.speaking); } void _handleCompletion() { - if (!mounted) { + if (!ref.mounted) { return; } state = state.copyWith( @@ -206,7 +217,7 @@ class TextToSpeechController extends StateNotifier { } void _handleCancellation() { - if (!mounted) { + if (!ref.mounted) { return; } state = state.copyWith( @@ -216,21 +227,21 @@ class TextToSpeechController extends StateNotifier { } void _handlePause() { - if (!mounted) { + if (!ref.mounted) { return; } state = state.copyWith(status: TtsPlaybackStatus.paused); } void _handleContinue() { - if (!mounted) { + if (!ref.mounted) { return; } state = state.copyWith(status: TtsPlaybackStatus.speaking); } void _handleError(String message) { - if (!mounted) { + if (!ref.mounted) { return; } state = state.copyWith( @@ -239,12 +250,6 @@ class TextToSpeechController extends StateNotifier { clearActiveMessageId: true, ); } - - @override - void dispose() { - unawaited(_service.stop()); - super.dispose(); - } } final textToSpeechServiceProvider = Provider((ref) { @@ -256,7 +261,6 @@ final textToSpeechServiceProvider = Provider((ref) { }); final textToSpeechControllerProvider = - StateNotifierProvider((ref) { - final service = ref.watch(textToSpeechServiceProvider); - return TextToSpeechController(service); - }); + NotifierProvider( + TextToSpeechController.new, + ); diff --git a/lib/features/chat/services/file_attachment_service.dart b/lib/features/chat/services/file_attachment_service.dart index a08a18c..f2cb5cc 100644 --- a/lib/features/chat/services/file_attachment_service.dart +++ b/lib/features/chat/services/file_attachment_service.dart @@ -498,8 +498,9 @@ final fileAttachmentServiceProvider = Provider((ref) { }); // State notifier for managing attached files -class AttachedFilesNotifier extends StateNotifier> { - AttachedFilesNotifier() : super([]); +class AttachedFilesNotifier extends Notifier> { + @override + List build() => []; void addFiles(List files) { final newStates = files @@ -536,6 +537,6 @@ class AttachedFilesNotifier extends StateNotifier> { } final attachedFilesProvider = - StateNotifierProvider>((ref) { - return AttachedFilesNotifier(); - }); + NotifierProvider>( + AttachedFilesNotifier.new, + ); diff --git a/lib/features/chat/services/message_batch_service.dart b/lib/features/chat/services/message_batch_service.dart index 5518fdc..33d7ea9 100644 --- a/lib/features/chat/services/message_batch_service.dart +++ b/lib/features/chat/services/message_batch_service.dart @@ -523,16 +523,41 @@ final messageBatchServiceProvider = Provider((ref) { }); /// Provider for selected messages (for batch operations) -final selectedMessagesProvider = StateProvider>((ref) { - return {}; -}); +final selectedMessagesProvider = + NotifierProvider>( + SelectedMessagesNotifier.new, + ); /// Provider for batch operation mode -final batchModeProvider = StateProvider((ref) { - return false; -}); +final batchModeProvider = NotifierProvider( + BatchModeNotifier.new, +); /// Provider for message filter -final messageFilterProvider = StateProvider((ref) { - return null; -}); +final messageFilterProvider = + NotifierProvider( + MessageFilterNotifier.new, + ); + +class SelectedMessagesNotifier extends Notifier> { + @override + Set build() => {}; + + void set(Set messages) => state = Set.from(messages); + + void clear() => state = {}; +} + +class BatchModeNotifier extends Notifier { + @override + bool build() => false; + + void set(bool value) => state = value; +} + +class MessageFilterNotifier extends Notifier { + @override + MessageFilter? build() => null; + + void set(MessageFilter? filter) => state = filter; +} diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 6863e4a..8db38fb 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -92,7 +92,7 @@ class _ChatPageState extends ConsumerState { void startNewChat() { // Clear current conversation ref.read(chatMessagesProvider.notifier).clearMessages(); - ref.read(activeConversationProvider.notifier).state = null; + ref.read(activeConversationProvider.notifier).clear(); // Scroll to top if (_scrollController.hasClients) { @@ -140,7 +140,7 @@ class _ChatPageState extends ConsumerState { 'Default provider failed, selecting first model directly', ); // Fallback: select the first available model - ref.read(selectedModelProvider.notifier).state = models.first; + ref.read(selectedModelProvider.notifier).set(models.first); DebugLogger.log('Fallback model selected: ${models.first.name}'); } } catch (e) { @@ -220,7 +220,7 @@ class _ChatPageState extends ConsumerState { ); if (!mounted) return; - ref.read(activeConversationProvider.notifier).state = welcomeConv; + ref.read(activeConversationProvider.notifier).set(welcomeConv); debugPrint('Auto-loaded demo conversation'); return; } @@ -296,7 +296,7 @@ class _ChatPageState extends ConsumerState { } if (models.isNotEmpty) { selectedModel = models.first; - ref.read(selectedModelProvider.notifier).state = selectedModel; + ref.read(selectedModelProvider.notifier).set(selectedModel); } } catch (_) { // If models cannot be resolved, bail out without sending @@ -1007,12 +1007,14 @@ class _ChatPageState extends ConsumerState { if (!mounted) return; final current = ref.read(inputFocusTriggerProvider); // Immediate focus bump - ref.read(inputFocusTriggerProvider.notifier).state = current + 1; + ref + .read(inputFocusTriggerProvider.notifier) + .set(current + 1); // Second bump shortly after to overcome route/IME timing Future.delayed(const Duration(milliseconds: 120), () { if (!mounted) return; final cur2 = ref.read(inputFocusTriggerProvider); - ref.read(inputFocusTriggerProvider.notifier).state = cur2 + 1; + ref.read(inputFocusTriggerProvider.notifier).set(cur2 + 1); }); }); _didStartupFocus = true; @@ -1326,9 +1328,8 @@ class _ChatPageState extends ConsumerState { try { final full = await api.getConversation(active.id); ref - .read(activeConversationProvider.notifier) - .state = - full; + .read(activeConversationProvider.notifier) + .set(full); } catch (e) { debugPrint( 'DEBUG: Failed to refresh conversation: $e', @@ -1507,7 +1508,7 @@ class _ChatPageState extends ConsumerState { if (hadFocus) { // Bump focus trigger to restore composer focus + IME final cur = ref.read(inputFocusTriggerProvider); - ref.read(inputFocusTriggerProvider.notifier).state = cur + 1; + ref.read(inputFocusTriggerProvider.notifier).set(cur + 1); } }); } @@ -1789,11 +1790,8 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> { onTap: () { HapticFeedback.selectionClick(); widget.ref - .read( - selectedModelProvider.notifier, - ) - .state = - model; + .read(selectedModelProvider.notifier) + .set(model); Navigator.pop(context); }, ); diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index 48ff7f9..64b4584 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -115,7 +115,7 @@ class _ModernChatInputState extends ConsumerState _controller.text = text; _controller.selection = TextSelection.collapsed(offset: text.length); // Clear after applying so it doesn't re-apply on rebuilds - ref.read(prefilledInputTextProvider.notifier).state = null; + ref.read(prefilledInputTextProvider.notifier).clear(); } }); @@ -131,7 +131,7 @@ class _ModernChatInputState extends ConsumerState final hasFocus = _focusNode.hasFocus; // Publish composer focus state try { - ref.read(composerHasFocusProvider.notifier).state = hasFocus; + ref.read(composerHasFocusProvider.notifier).set(hasFocus); } catch (_) {} }); }); @@ -142,7 +142,7 @@ class _ModernChatInputState extends ConsumerState @override void dispose() { try { - ref.read(composerHasFocusProvider.notifier).state = false; + ref.read(composerHasFocusProvider.notifier).set(false); } catch (_) {} _controller.removeListener(_handleComposerChanged); _controller.dispose(); @@ -583,7 +583,7 @@ class _ModernChatInputState extends ConsumerState offset: incoming.length, ); try { - ref.read(prefilledInputTextProvider.notifier).state = null; + ref.read(prefilledInputTextProvider.notifier).clear(); } catch (_) {} }); }); @@ -658,7 +658,7 @@ class _ModernChatInputState extends ConsumerState : Icons.search; void handleTap() { final notifier = ref.read(webSearchEnabledProvider.notifier); - notifier.state = !webSearchEnabled; + notifier.set(!webSearchEnabled); } quickPills.add( @@ -676,7 +676,7 @@ class _ModernChatInputState extends ConsumerState : Icons.image; void handleTap() { final notifier = ref.read(imageGenerationEnabledProvider.notifier); - notifier.state = !imageGenEnabled; + notifier.set(!imageGenEnabled); } quickPills.add( @@ -709,7 +709,7 @@ class _ModernChatInputState extends ConsumerState } else { current.add(id); } - ref.read(selectedToolIdsProvider.notifier).state = current; + ref.read(selectedToolIdsProvider.notifier).set(current); } quickPills.add( @@ -1459,7 +1459,7 @@ class _ModernChatInputState extends ConsumerState subtitle: l10n.webSearchDescription, value: webSearchEnabled, onChanged: (next) { - modalRef.read(webSearchEnabledProvider.notifier).state = next; + modalRef.read(webSearchEnabledProvider.notifier).set(next); }, ), ); @@ -1479,8 +1479,9 @@ class _ModernChatInputState extends ConsumerState subtitle: l10n.imageGenerationDescription, value: imageGenEnabled, onChanged: (next) { - modalRef.read(imageGenerationEnabledProvider.notifier).state = - next; + modalRef + .read(imageGenerationEnabledProvider.notifier) + .set(next); }, ), ); @@ -1507,8 +1508,9 @@ class _ModernChatInputState extends ConsumerState } else { current.add(tool.id); } - modalRef.read(selectedToolIdsProvider.notifier).state = - current; + modalRef + .read(selectedToolIdsProvider.notifier) + .set(current); }, ); }).toList(); diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index 4c65039..6dd9d74 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -43,10 +43,12 @@ class _ChatsDrawerState extends ConsumerState { bool _draggingHasFolder = false; // UI state providers for sections - static final _showArchivedProvider = StateProvider((ref) => false); - static final _expandedFoldersProvider = StateProvider>( - (ref) => {}, - ); + static final _showArchivedProvider = + NotifierProvider<_ShowArchivedNotifier, bool>(_ShowArchivedNotifier.new); + static final _expandedFoldersProvider = + NotifierProvider<_ExpandedFoldersNotifier, Map>( + _ExpandedFoldersNotifier.new, + ); Future _refreshChats() async { try { @@ -694,7 +696,7 @@ class _ChatsDrawerState extends ConsumerState { onTap: () { final current = {...ref.read(_expandedFoldersProvider)}; current[folderId] = !isExpanded; - ref.read(_expandedFoldersProvider.notifier).state = current; + ref.read(_expandedFoldersProvider.notifier).set(current); }, onLongPress: () { HapticFeedback.selectionClick(); @@ -1065,7 +1067,7 @@ class _ChatsDrawerState extends ConsumerState { ), child: InkWell( borderRadius: BorderRadius.zero, - onTap: () => ref.read(_showArchivedProvider.notifier).state = !show, + onTap: () => ref.read(_showArchivedProvider.notifier).set(!show), overlayColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.pressed)) { return theme.buttonPrimary.withValues( @@ -1163,11 +1165,11 @@ class _ChatsDrawerState extends ConsumerState { final container = ProviderScope.containerOf(context, listen: false); try { // Mark global loading to show skeletons in chat - container.read(chat.isLoadingConversationProvider.notifier).state = true; + container.read(chat.isLoadingConversationProvider.notifier).set(true); _pendingConversationId = id; // Immediately clear current chat to show loading skeleton in the chat view - container.read(activeConversationProvider.notifier).state = null; + container.read(activeConversationProvider.notifier).clear(); container.read(chat.chatMessagesProvider.notifier).clearMessages(); // Close the drawer immediately for faster perceived performance @@ -1185,21 +1187,22 @@ class _ChatsDrawerState extends ConsumerState { final api = container.read(apiServiceProvider); if (api != null) { final full = await api.getConversation(id); - container.read(activeConversationProvider.notifier).state = full; + container.read(activeConversationProvider.notifier).set(full); } else { // Fallback: use the lightweight item to update the active conversation - container - .read(activeConversationProvider.notifier) - .state = (await container.read( - conversationsProvider.future, - )).firstWhere((c) => c.id == id); + container.read(activeConversationProvider.notifier).set( + (await container.read( + conversationsProvider.future, + )) + .firstWhere((c) => c.id == id), + ); } // Clear loading after data is ready - container.read(chat.isLoadingConversationProvider.notifier).state = false; + container.read(chat.isLoadingConversationProvider.notifier).set(false); _pendingConversationId = null; } catch (_) { - container.read(chat.isLoadingConversationProvider.notifier).state = false; + container.read(chat.isLoadingConversationProvider.notifier).set(false); _pendingConversationId = null; } finally { if (mounted) setState(() => _isLoadingConversation = false); @@ -1311,6 +1314,20 @@ class _ChatsDrawerState extends ConsumerState { } } +class _ShowArchivedNotifier extends Notifier { + @override + bool build() => false; + + void set(bool value) => state = value; +} + +class _ExpandedFoldersNotifier extends Notifier> { + @override + Map build() => {}; + + void set(Map value) => state = Map.from(value); +} + class _DragConversationData { final String id; final String title; diff --git a/lib/features/prompts/providers/prompts_providers.dart b/lib/features/prompts/providers/prompts_providers.dart index fa260e6..d16bcee 100644 --- a/lib/features/prompts/providers/prompts_providers.dart +++ b/lib/features/prompts/providers/prompts_providers.dart @@ -9,4 +9,14 @@ final promptsListProvider = FutureProvider>((ref) async { return promptsService.getPrompts(); }); -final activePromptCommandProvider = StateProvider((ref) => null); +final activePromptCommandProvider = + NotifierProvider( + ActivePromptCommandNotifier.new, + ); + +class ActivePromptCommandNotifier extends Notifier { + @override + String? build() => null; + + void set(String? command) => state = command; +} diff --git a/lib/features/tools/providers/tools_providers.dart b/lib/features/tools/providers/tools_providers.dart index afa46ea..e9d2298 100644 --- a/lib/features/tools/providers/tools_providers.dart +++ b/lib/features/tools/providers/tools_providers.dart @@ -8,4 +8,14 @@ final toolsListProvider = FutureProvider>((ref) async { return await toolsService.getTools(); }); -final selectedToolIdsProvider = StateProvider>((ref) => []); \ No newline at end of file +final selectedToolIdsProvider = + NotifierProvider>( + SelectedToolIdsNotifier.new, + ); + +class SelectedToolIdsNotifier extends Notifier> { + @override + List build() => []; + + void set(List ids) => state = List.from(ids); +} diff --git a/lib/shared/services/tasks/task_queue.dart b/lib/shared/services/tasks/task_queue.dart index dda42fd..81cecec 100644 --- a/lib/shared/services/tasks/task_queue.dart +++ b/lib/shared/services/tasks/task_queue.dart @@ -10,18 +10,23 @@ import 'outbound_task.dart'; import 'task_worker.dart'; final taskQueueProvider = - StateNotifierProvider>((ref) { - return TaskQueueNotifier(ref); -}); - -class TaskQueueNotifier extends StateNotifier> { - TaskQueueNotifier(this._ref) : super(const []) { - _load(); - } + NotifierProvider>( + TaskQueueNotifier.new, + ); +class TaskQueueNotifier extends Notifier> { static const _prefsKey = 'outbound_task_queue_v1'; - final Ref _ref; final _uuid = const Uuid(); + bool _bootstrapScheduled = false; + + @override + List build() { + if (!_bootstrapScheduled) { + _bootstrapScheduled = true; + Future.microtask(_load); + } + return const []; + } bool _processing = false; final Set _activeThreads = {}; @@ -29,19 +34,24 @@ class TaskQueueNotifier extends StateNotifier> { Future _load() async { try { - final prefs = _ref.read(sharedPreferencesProvider); + final prefs = ref.read(sharedPreferencesProvider); final jsonStr = prefs.getString(_prefsKey); if (jsonStr == null || jsonStr.isEmpty) return; final raw = (jsonDecode(jsonStr) as List).cast>(); final tasks = raw.map(OutboundTask.fromJson).toList(); // Only restore non-completed tasks state = tasks - .where((t) => t.status == TaskStatus.queued || t.status == TaskStatus.running) - .map((t) => t.copyWith( - status: TaskStatus.queued, - startedAt: null, - completedAt: null, - )) + .where( + (t) => + t.status == TaskStatus.queued || t.status == TaskStatus.running, + ) + .map( + (t) => t.copyWith( + status: TaskStatus.queued, + startedAt: null, + completedAt: null, + ), + ) .toList(); // Kick processing after load _process(); @@ -52,7 +62,7 @@ class TaskQueueNotifier extends StateNotifier> { Future _save() async { try { - final prefs = _ref.read(sharedPreferencesProvider); + final prefs = ref.read(sharedPreferencesProvider); final raw = state.map((t) => t.toJson()).toList(growable: false); await prefs.setString(_prefsKey, jsonEncode(raw)); } catch (e) { @@ -87,10 +97,7 @@ class TaskQueueNotifier extends StateNotifier> { state = [ for (final t in state) if (t.id == id) - t.copyWith( - status: TaskStatus.cancelled, - completedAt: DateTime.now(), - ) + t.copyWith(status: TaskStatus.cancelled, completedAt: DateTime.now()) else t, ]; @@ -102,10 +109,7 @@ class TaskQueueNotifier extends StateNotifier> { for (final t in state) if ((t.maybeConversationId ?? '') == conversationId && (t.status == TaskStatus.queued || t.status == TaskStatus.running)) - t.copyWith( - status: TaskStatus.cancelled, - completedAt: DateTime.now(), - ) + t.copyWith(status: TaskStatus.cancelled, completedAt: DateTime.now()) else t, ]; @@ -177,7 +181,9 @@ class TaskQueueNotifier extends StateNotifier> { // Pump while there is capacity and queued tasks remain while (true) { // Filter runnable tasks - final queued = state.where((t) => t.status == TaskStatus.queued).toList(); + final queued = state + .where((t) => t.status == TaskStatus.queued) + .toList(); if (queued.isEmpty) break; // Respect parallelism and one-per-thread @@ -202,18 +208,23 @@ class TaskQueueNotifier extends StateNotifier> { state = [ for (final t in state) if (t.id == next.id) - next.copyWith(status: TaskStatus.running, startedAt: DateTime.now()) + next.copyWith( + status: TaskStatus.running, + startedAt: DateTime.now(), + ) else t, ]; await _save(); // Launch worker - unawaited(_run(next).whenComplete(() { - _activeThreads.remove(threadKey); - // After a task completes, try to schedule more - _process(); - })); + unawaited( + _run(next).whenComplete(() { + _activeThreads.remove(threadKey); + // After a task completes, try to schedule more + _process(); + }), + ); } } finally { _processing = false; @@ -222,11 +233,14 @@ class TaskQueueNotifier extends StateNotifier> { Future _run(OutboundTask task) async { try { - await TaskWorker(_ref).perform(task); + await TaskWorker(ref).perform(task); state = [ for (final t in state) if (t.id == task.id) - t.copyWith(status: TaskStatus.succeeded, completedAt: DateTime.now()) + t.copyWith( + status: TaskStatus.succeeded, + completedAt: DateTime.now(), + ) else t, ]; diff --git a/lib/shared/services/tasks/task_worker.dart b/lib/shared/services/tasks/task_worker.dart index 8a1b3d2..b27d4c2 100644 --- a/lib/shared/services/tasks/task_worker.dart +++ b/lib/shared/services/tasks/task_worker.dart @@ -49,7 +49,7 @@ class TaskWorker { final api = _ref.read(apiServiceProvider); if (api != null) { final conv = await api.getConversation(task.conversationId!); - _ref.read(activeConversationProvider.notifier).state = conv; + _ref.read(activeConversationProvider.notifier).set(conv); } } catch (_) { // If loading fails, proceed; send flow can create a new conversation @@ -167,7 +167,7 @@ class TaskWorker { (active == null || active.id != task.conversationId)) { try { final conv = await api.getConversation(task.conversationId!); - _ref.read(activeConversationProvider.notifier).state = conv; + _ref.read(activeConversationProvider.notifier).set(conv); } catch (_) {} } } catch (_) {} @@ -225,7 +225,7 @@ class TaskWorker { (active == null || active.id != task.conversationId)) { try { final conv = await api.getConversation(task.conversationId!); - _ref.read(activeConversationProvider.notifier).state = conv; + _ref.read(activeConversationProvider.notifier).set(conv); } catch (_) {} } } catch (_) {} @@ -233,10 +233,10 @@ class TaskWorker { // Temporarily enable image-generation background flow for this send final prev = _ref.read(chat.imageGenerationEnabledProvider); try { - _ref.read(chat.imageGenerationEnabledProvider.notifier).state = true; + _ref.read(chat.imageGenerationEnabledProvider.notifier).set(true); await chat.sendMessageFromService(_ref, task.prompt, null, null); } finally { - _ref.read(chat.imageGenerationEnabledProvider.notifier).state = prev; + _ref.read(chat.imageGenerationEnabledProvider.notifier).set(prev); } } @@ -368,7 +368,7 @@ class TaskWorker { title: title.length > 100 ? '${title.substring(0, 100)}...' : title, updatedAt: DateTime.now(), ); - _ref.read(activeConversationProvider.notifier).state = updated; + _ref.read(activeConversationProvider.notifier).set(updated); // Do not push full messages to server; skip remote update. // Optionally refresh list to reflect server-side title when it’s generated there. _ref.invalidate(conversationsProvider); diff --git a/lib/shared/utils/conversation_context_menu.dart b/lib/shared/utils/conversation_context_menu.dart index e38e3b8..9cdd91b 100644 --- a/lib/shared/utils/conversation_context_menu.dart +++ b/lib/shared/utils/conversation_context_menu.dart @@ -224,9 +224,9 @@ Future _renameConversation( ref.invalidate(conversationsProvider); final active = ref.read(activeConversationProvider); if (active?.id == conversationId) { - ref.read(activeConversationProvider.notifier).state = active!.copyWith( - title: newName, - ); + ref + .read(activeConversationProvider.notifier) + .set(active!.copyWith(title: newName)); } } catch (_) { if (!context.mounted) return; @@ -259,7 +259,7 @@ Future _confirmAndDeleteConversation( HapticFeedback.mediumImpact(); final active = ref.read(activeConversationProvider); if (active?.id == conversationId) { - ref.read(activeConversationProvider.notifier).state = null; + ref.read(activeConversationProvider.notifier).clear(); ref.read(chat.chatMessagesProvider.notifier).clearMessages(); } ref.invalidate(conversationsProvider); diff --git a/pubspec.lock b/pubspec.lock index e969181..4ef45f7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -161,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" clock: dependency: transitive description: @@ -173,10 +181,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" url: "https://pub.dev" source: hosted - version: "4.10.1" + version: "4.11.0" collection: dependency: transitive description: @@ -193,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" cross_file: dependency: transitive description: @@ -285,10 +301,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: e7e16c9d15c36330b94ca0e2ad8cb61f93cd5282d0158c09805aed13b5452f22 + sha256: f2d9f173c2c14635cc0e9b14c143c49ef30b4934e8d1d274d6206fcb0086a06f url: "https://pub.dev" source: hosted - version: "10.3.2" + version: "10.3.3" file_selector_linux: dependency: transitive description: @@ -391,10 +407,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + sha256: ca2480512a8e840291325249f4857e363ffa5d1b77b132e189c9313a9d9fb9e0 url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "3.0.0" flutter_secure_storage: dependency: "direct main" description: @@ -460,10 +476,10 @@ packages: dependency: "direct main" description: name: flutter_tts - sha256: cbb3fd43b946e62398560235469e6113e4fe26c40eab1b7cb5e7c417503fb3a8 + sha256: bdf2fc4483e74450dc9fc6fe6a9b6a5663e108d4d0dad3324a22c8e26bf48af4 url: "https://pub.dev" source: hosted - version: "3.8.5" + version: "4.2.3" flutter_web_plugins: dependency: transitive description: flutter @@ -473,10 +489,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: da32f8ba8cfcd4ec71d9decc8cbf28bd2c31b5283d9887eb51eb4a0659d8110c + sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.3" freezed_annotation: dependency: "direct main" description: @@ -569,10 +585,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e" + sha256: "8dfe08ea7fcf7467dbaf6889e72eebd5e0d6711caae201fdac780eb45232cd02" url: "https://pub.dev" source: hosted - version: "0.8.13+1" + version: "0.8.13+3" image_picker_for_web: dependency: transitive description: @@ -665,10 +681,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "11.0.1" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: @@ -749,6 +765,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" octo_image: dependency: transitive description: @@ -769,10 +793,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d url: "https://pub.dev" source: hosted - version: "8.3.1" + version: "9.0.0" package_info_plus_platform_interface: dependency: transitive description: @@ -865,10 +889,10 @@ packages: dependency: transitive description: name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.5.2" posix: dependency: transitive description: @@ -905,10 +929,10 @@ packages: dependency: transitive description: name: record_android - sha256: "8361a791c9a3fa5c065f0b8b5adb10f12531f8538c86b19474cf7b56ea80d426" + sha256: "854627cd78d8d66190377f98477eee06ca96ab7c9f2e662700daf33dbf7e6673" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" record_ios: dependency: transitive description: @@ -961,10 +985,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + sha256: "135723ec44dfba141bc4696224048a408336e794228a0117439e7ad0a8be6d05" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "3.0.0" rxdart: dependency: transitive description: @@ -1017,10 +1041,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1 + sha256: "3424e9d5c22fd7f7590254ba09465febd6f8827c8b19a44350de4ac31d92d3a6" url: "https://pub.dev" source: hosted - version: "11.1.0" + version: "12.0.0" share_plus_platform_interface: dependency: transitive description: @@ -1093,6 +1117,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" shelf_web_socket: dependency: transitive description: @@ -1126,10 +1166,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "7b19d6ba131c6eb98bfcbf8d56c1a7002eba438af2e7ae6f8398b2b0f4f381e3" + sha256: "800f12fb87434defa13432ab37e33051b43b290a174e15259563b043cda40c46" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.0.0" source_helper: dependency: transitive description: @@ -1138,6 +1178,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.8" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" source_span: dependency: transitive description: @@ -1274,6 +1330,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + test: + dependency: transitive + description: + name: test + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + url: "https://pub.dev" + source: hosted + version: "1.26.2" test_api: dependency: transitive description: @@ -1282,6 +1346,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.6" + test_core: + dependency: transitive + description: + name: test_core + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + url: "https://pub.dev" + source: hosted + version: "0.6.11" timing: dependency: transitive description: @@ -1318,10 +1390,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7" + sha256: "199bc33e746088546a39cc5f36bac5a278c5e53b40cb3196f99e7345fdcfae6b" url: "https://pub.dev" source: hosted - version: "6.3.18" + version: "6.3.22" url_launcher_ios: dependency: transitive description: @@ -1406,18 +1478,18 @@ packages: dependency: "direct main" description: name: wakelock_plus - sha256: a474e314c3e8fb5adef1f9ae2d247e57467ad557fa7483a2b895bc1b421c5678 + sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: e10444072e50dbc4999d7316fd303f7ea53d31c824aa5eb05d7ccbdd98985207 + sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" watcher: dependency: transitive description: @@ -1450,6 +1522,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" win32: dependency: transitive description: @@ -1484,4 +1564,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.9.0 <4.0.0" - flutter: ">=3.29.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7c0ae14..e14c824 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: sdk: flutter # State Management - flutter_riverpod: ^2.6.1 + flutter_riverpod: ^3.0.0 # Network & API dio: ^5.9.0 @@ -39,16 +39,16 @@ dependencies: # Platform Features record: ^6.1.1 stts: ^1.2.5 - flutter_tts: ^3.8.5 + flutter_tts: ^4.2.3 image_picker: ^1.2.0 - file_picker: ^10.3.2 + file_picker: ^10.3.3 path_provider: ^2.1.4 # Utilities path: ^1.9.0 uuid: ^4.5.0 crypto: ^3.0.3 - package_info_plus: ^8.3.1 + package_info_plus: ^9.0.0 url_launcher: ^6.3.0 intl: ^0.20.2 @@ -56,8 +56,8 @@ dependencies: cupertino_icons: ^1.0.8 json_annotation: ^4.9.0 freezed_annotation: ^3.0.0 - wakelock_plus: ^1.2.10 - share_plus: ^11.1.0 + wakelock_plus: ^1.4.0 + share_plus: ^12.0.0 share_handler: ^0.0.19 # Clipboard functionality is available through flutter/services (part of Flutter SDK)