refactor: migrate to riverpod 3

This commit is contained in:
cogwheel0
2025-09-21 22:31:44 +05:30
parent 37e5633c5c
commit 462bf4cde2
20 changed files with 834 additions and 453 deletions

View File

@@ -78,23 +78,28 @@ enum AuthStatus {
}
/// Unified auth state manager - single source of truth for all auth operations
class AuthStateManager extends StateNotifier<AuthState> {
AuthStateManager(this._ref)
: super(const AuthState(status: AuthStatus.initial)) {
_initialize();
}
final Ref _ref;
class AuthStateManager extends Notifier<AuthState> {
final AuthCacheManager _cacheManager = AuthCacheManager();
// Prevent overlapping silent-login attempts from multiple triggers
Future<bool>? _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<void> _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<AuthState> {
// 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<AuthState> {
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<AuthState> {
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<AuthState> {
}
// 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<AuthState> {
}) 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<AuthState> {
);
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<AuthState> {
// 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<AuthState> {
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<AuthState> {
}
// 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<AuthState> {
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<AuthState> {
}
// 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<AuthState> {
/// Load complete user data from server
Future<void> _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<AuthState> {
/// 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<AuthState> {
// 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<AuthState> {
}
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<AuthState> {
}
/// Provider for the unified auth state manager
final authStateManagerProvider =
StateNotifierProvider<AuthStateManager, AuthState>((ref) {
return AuthStateManager(ref);
});
final authStateManagerProvider = NotifierProvider<AuthStateManager, AuthState>(
AuthStateManager.new,
);
/// Computed providers for common auth state queries
final isAuthenticatedProvider = Provider<bool>((ref) {

View File

@@ -63,28 +63,24 @@ final optimizedStorageServiceProvider = Provider<OptimizedStorageService>((
});
// Theme provider
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>((
ref,
) {
final storage = ref.watch(optimizedStorageServiceProvider);
return ThemeModeNotifier(storage);
});
final themeModeProvider = NotifierProvider<ThemeModeNotifier, ThemeMode>(
ThemeModeNotifier.new,
);
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
final OptimizedStorageService _storage;
class ThemeModeNotifier extends Notifier<ThemeMode> {
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<ThemeMode> {
}
// Locale provider
final localeProvider = StateNotifierProvider<LocaleNotifier, Locale?>((ref) {
final storage = ref.watch(optimizedStorageServiceProvider);
return LocaleNotifier(storage);
});
final localeProvider = NotifierProvider<LocaleNotifier, Locale?>(
LocaleNotifier.new,
);
class LocaleNotifier extends StateNotifier<Locale?> {
final OptimizedStorageService _storage;
class LocaleNotifier extends Notifier<Locale?> {
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<void> setLocale(Locale? locale) async {
@@ -325,17 +317,38 @@ final modelsProvider = FutureProvider<List<Model>>((ref) async {
}
});
final selectedModelProvider = StateProvider<Model?>((ref) => null);
final selectedModelProvider = NotifierProvider<SelectedModelNotifier, Model?>(
SelectedModelNotifier.new,
);
// Track if the current model selection is manual (user-selected) or automatic (default)
final isManualModelSelectionProvider = StateProvider<bool>((ref) => false);
final isManualModelSelectionProvider =
NotifierProvider<IsManualModelSelectionNotifier, bool>(
IsManualModelSelectionNotifier.new,
);
class SelectedModelNotifier extends Notifier<Model?> {
@override
Model? build() => null;
void set(Model? model) => state = model;
void clear() => state = null;
}
class IsManualModelSelectionNotifier extends Notifier<bool> {
@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<void>((ref) {
ref.listen<AppSettings>(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<void>((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<void>((ref) {
});
// Cache timestamp for conversations to prevent rapid re-fetches
final _conversationsCacheTimestamp = StateProvider<DateTime?>((ref) => null);
final _conversationsCacheTimestamp =
NotifierProvider<_ConversationsCacheTimestampNotifier, DateTime?>(
_ConversationsCacheTimestampNotifier.new,
);
class _ConversationsCacheTimestampNotifier extends Notifier<DateTime?> {
@override
DateTime? build() => null;
void set(DateTime? timestamp) => state = timestamp;
}
// Conversation providers - Now using correct OpenWebUI API with caching
final conversationsProvider = FutureProvider<List<Conversation>>((ref) async {
@@ -585,7 +608,7 @@ final conversationsProvider = FutureProvider<List<Conversation>>((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<List<Conversation>>((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<List<Conversation>>((ref) async {
}
});
final activeConversationProvider = StateProvider<Conversation?>((ref) => null);
final activeConversationProvider =
NotifierProvider<ActiveConversationNotifier, Conversation?>(
ActiveConversationNotifier.new,
);
class ActiveConversationNotifier extends Notifier<Conversation?> {
@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<Conversation, String>((
@@ -662,7 +697,7 @@ final defaultModelProvider = FutureProvider<Model?>((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<Model?>((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<Model?>((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<Model?>((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<Model?>((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<Model?>((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<void>((ref) {
});
// Search query provider
final searchQueryProvider = StateProvider<String>((ref) => '');
final searchQueryProvider = NotifierProvider<SearchQueryNotifier, String>(
SearchQueryNotifier.new,
);
class SearchQueryNotifier extends Notifier<String> {
@override
String build() => '';
void set(String query) => state = query;
}
// Server-side search provider for chats
final serverSearchProvider = FutureProvider.family<List<Conversation>, String>((
@@ -1002,17 +1046,29 @@ final archivedConversationsProvider = Provider<List<Conversation>>((ref) {
});
// Reviewer mode provider (persisted)
final reviewerModeProvider = StateNotifierProvider<ReviewerModeNotifier, bool>(
(ref) => ReviewerModeNotifier(ref.watch(optimizedStorageServiceProvider)),
final reviewerModeProvider = NotifierProvider<ReviewerModeNotifier, bool>(
ReviewerModeNotifier.new,
);
class ReviewerModeNotifier extends StateNotifier<bool> {
final OptimizedStorageService _storage;
ReviewerModeNotifier(this._storage) : super(false) {
_load();
class ReviewerModeNotifier extends Notifier<bool> {
late final OptimizedStorageService _storage;
bool _initialized = false;
@override
bool build() {
_storage = ref.watch(optimizedStorageServiceProvider);
if (!_initialized) {
_initialized = true;
Future.microtask(_load);
}
return false;
}
Future<void> _load() async {
final enabled = await _storage.getReviewerMode();
if (!ref.mounted) {
return;
}
state = enabled;
}

View File

@@ -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<DateTime?>(
(ref) => null,
);
class _ConversationWarmupStatusNotifier
extends Notifier<_ConversationWarmupStatus> {
@override
_ConversationWarmupStatus build() => _ConversationWarmupStatus.idle;
void set(_ConversationWarmupStatus status) => state = status;
}
class _ConversationWarmupLastAttemptNotifier extends Notifier<DateTime?> {
@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<void>((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<void>((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);
});

View File

@@ -208,12 +208,29 @@ class AnimationService {
enum PageTransitionType { fade, slide, scale }
/// Provider for reduced motion preference
final reducedMotionProvider = StateProvider<bool>((ref) => false);
final reducedMotionProvider = NotifierProvider<ReducedMotionNotifier, bool>(
ReducedMotionNotifier.new,
);
/// Provider for animation performance settings
final animationPerformanceProvider = StateProvider<AnimationPerformance>((ref) {
return AnimationPerformance.adaptive;
});
final animationPerformanceProvider =
NotifierProvider<AnimationPerformanceNotifier, AnimationPerformance>(
AnimationPerformanceNotifier.new,
);
class ReducedMotionNotifier extends Notifier<bool> {
@override
bool build() => false;
void set(bool value) => state = value;
}
class AnimationPerformanceNotifier extends Notifier<AnimationPerformance> {
@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<AnimationSettingsNotifier, AnimationSettings>(
(ref) => AnimationSettingsNotifier(),
NotifierProvider<AnimationSettingsNotifier, AnimationSettings>(
AnimationSettingsNotifier.new,
);
class AnimationSettings {
@@ -253,8 +270,9 @@ class AnimationSettings {
}
}
class AnimationSettingsNotifier extends StateNotifier<AnimationSettings> {
AnimationSettingsNotifier() : super(const AnimationSettings());
class AnimationSettingsNotifier extends Notifier<AnimationSettings> {
@override
AnimationSettings build() => const AnimationSettings();
void setReduceMotion(bool reduce) {
state = state.copyWith(reduceMotion: reduce);

View File

@@ -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<String> a, List<String> b) {
}
/// Provider for app settings
final appSettingsProvider =
StateNotifierProvider<AppSettingsNotifier, AppSettings>(
(ref) => AppSettingsNotifier(),
);
final appSettingsProvider = NotifierProvider<AppSettingsNotifier, AppSettings>(
AppSettingsNotifier.new,
);
class AppSettingsNotifier extends StateNotifier<AppSettings> {
AppSettingsNotifier() : super(const AppSettings()) {
_loadSettings();
class AppSettingsNotifier extends Notifier<AppSettings> {
bool _initialized = false;
@override
AppSettings build() {
if (!_initialized) {
_initialized = true;
Future.microtask(_loadSettings);
}
return const AppSettings();
}
Future<void> _loadSettings() async {
final settings = await SettingsService.loadSettings();
if (!ref.mounted) {
return;
}
state = settings;
}

View File

@@ -25,7 +25,17 @@ class SharedPayload {
}
/// Holds a pending shared payload until the app is ready (e.g., authed + model loaded)
final pendingSharedPayloadProvider = StateProvider<SharedPayload?>((_) => null);
final pendingSharedPayloadProvider =
NotifierProvider<PendingSharedPayloadNotifier, SharedPayload?>(
PendingSharedPayloadNotifier.new,
);
class PendingSharedPayloadNotifier extends Notifier<SharedPayload?> {
@override
SharedPayload? build() => null;
void set(SharedPayload? payload) => state = payload;
}
/// Initializes listening to OS share intents and handles them
final shareReceiverInitializerProvider = Provider<void>((ref) {
@@ -45,7 +55,7 @@ final shareReceiverInitializerProvider = Provider<void>((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<void>((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<void>((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<void> _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.