refactor: migrate to riverpod 3
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user