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