refactor: improve app start time

This commit is contained in:
cogwheel0
2025-09-16 20:10:53 +05:30
parent f80930685c
commit ac12eca6b5
13 changed files with 150 additions and 188 deletions

View File

@@ -99,10 +99,9 @@ class AuthStateManager extends StateNotifier<AuthState> {
if (token != null && token.isNotEmpty) { if (token != null && token.isNotEmpty) {
DebugLogger.auth('Found stored token during initialization'); DebugLogger.auth('Found stored token during initialization');
// Validate token before setting authenticated state // Fast path: trust token format to avoid blocking startup on network
final isValid = await _validateToken(token); final formatOk = _isValidTokenFormat(token);
DebugLogger.auth('Token validation result: $isValid'); if (formatOk) {
if (isValid) {
state = state.copyWith( state = state.copyWith(
status: AuthStatus.authenticated, status: AuthStatus.authenticated,
token: token, token: token,
@@ -110,14 +109,23 @@ class AuthStateManager extends StateNotifier<AuthState> {
clearError: true, clearError: true,
); );
// Update API service with token // Update API service with token and load user data in background
_updateApiServiceToken(token); _updateApiServiceToken(token);
// Load user data in background
_loadUserData(); _loadUserData();
// Background server validation; if it fails, invalidate token gracefully
Future.microtask(() async {
try {
final ok = await _validateToken(token);
DebugLogger.auth('Deferred token validation result: $ok');
if (!ok) {
await onTokenInvalidated();
}
} catch (_) {}
});
} else { } else {
// Token is invalid, clear it // Token format invalid; clear and require login
DebugLogger.auth('Token validation failed, deleting token'); DebugLogger.auth('Token format invalid, deleting token');
await storage.deleteAuthToken(); await storage.deleteAuthToken();
state = state.copyWith( state = state.copyWith(
status: AuthStatus.unauthenticated, status: AuthStatus.unauthenticated,

View File

@@ -682,137 +682,104 @@ final defaultModelProvider = FutureProvider<Model?>((ref) async {
if (api == null) return null; if (api == null) return null;
try { try {
// Get all available models first // Respect manual selection if present
if (ref.read(isManualModelSelectionProvider)) {
final current = ref.read(selectedModelProvider);
if (current != null) return current;
}
// 1) Fast path: read stored default model ID directly and select optimistically
try {
final storedDefaultId = await SettingsService.getDefaultModel();
if (storedDefaultId != null && storedDefaultId.isNotEmpty) {
if (!ref.read(isManualModelSelectionProvider)) {
final placeholder = Model(
id: storedDefaultId,
name: storedDefaultId,
supportsStreaming: true,
);
ref.read(selectedModelProvider.notifier).state = placeholder;
}
// Reconcile against real models in background
Future.microtask(() async {
try {
final models = await ref.read(modelsProvider.future);
Model? resolved;
try {
resolved = models.firstWhere((m) => m.id == storedDefaultId);
} catch (_) {
final byName = models
.where((m) => m.name == storedDefaultId)
.toList();
if (byName.length == 1) resolved = byName.first;
}
resolved ??= models.isNotEmpty ? models.first : null;
if (resolved != null && !ref.read(isManualModelSelectionProvider)) {
ref.read(selectedModelProvider.notifier).state = resolved;
foundation.debugPrint(
'DEBUG: Reconciled default model to ${resolved.name}',
);
}
} catch (_) {}
});
return ref.read(selectedModelProvider);
}
} catch (_) {}
// 2) Fast server path: query server default ID without listing all models
try {
final serverDefault = await api.getDefaultModel();
if (serverDefault != null && serverDefault.isNotEmpty) {
if (!ref.read(isManualModelSelectionProvider)) {
final placeholder = Model(
id: serverDefault,
name: serverDefault,
supportsStreaming: true,
);
ref.read(selectedModelProvider.notifier).state = placeholder;
}
// Reconcile against real models in background
Future.microtask(() async {
try {
final models = await ref.read(modelsProvider.future);
Model? resolved;
try {
resolved = models.firstWhere((m) => m.id == serverDefault);
} catch (_) {
final byName = models
.where((m) => m.name == serverDefault)
.toList();
if (byName.length == 1) resolved = byName.first;
}
resolved ??= models.isNotEmpty ? models.first : null;
if (resolved != null && !ref.read(isManualModelSelectionProvider)) {
ref.read(selectedModelProvider.notifier).state = resolved;
foundation.debugPrint(
'DEBUG: Reconciled server default to ${resolved.name}',
);
}
} catch (_) {}
});
return ref.read(selectedModelProvider);
}
} catch (_) {}
// 3) Fallback: fetch models and pick first available
final models = await ref.read(modelsProvider.future); final models = await ref.read(modelsProvider.future);
if (models.isEmpty) { if (models.isEmpty) {
foundation.debugPrint('DEBUG: No models available'); foundation.debugPrint('DEBUG: No models available');
return null; return null;
} }
final selectedModel = models.first;
Model? selectedModel;
// First check user's preferred default model (ID only). If an older
// name-based value is found, migrate it once to the correct ID.
final userSettings = ref.read(appSettingsProvider);
final userDefaultModelId = userSettings.defaultModel;
if (userDefaultModelId != null && userDefaultModelId.isNotEmpty) {
try {
// Exact ID match only
selectedModel = models.firstWhere(
(model) => model.id == userDefaultModelId,
);
foundation.debugPrint(
'DEBUG: Found user default model by ID: ${selectedModel.name}',
);
} catch (e) {
// Attempt a one-time migration if the stored value was a model name
// from older versions. Only migrate on exact, unique name match.
final nameMatches = models
.where((m) => m.name == userDefaultModelId)
.toList();
if (nameMatches.length == 1) {
selectedModel = nameMatches.first;
foundation.debugPrint(
'DEBUG: Migrating user default model name to ID: '
'${nameMatches.first.name} -> ${nameMatches.first.id}',
);
// Persist the migrated ID
await ref
.read(appSettingsProvider.notifier)
.setDefaultModel(nameMatches.first.id);
} else {
foundation.debugPrint(
'DEBUG: User default model "$userDefaultModelId" not found by ID and '
'no unique name match. Ignoring.',
);
selectedModel =
null; // Will fall back to server default or first model
}
}
}
// If no user default or user default not found, try server's default model
if (selectedModel == null) {
try {
final defaultModelId = await api.getDefaultModel();
if (defaultModelId != null && defaultModelId.isNotEmpty) {
// Find the model that matches the default model ID (ID only)
try {
selectedModel = models.firstWhere(
(model) => model.id == defaultModelId,
);
foundation.debugPrint(
'DEBUG: Found server default model by ID: ${selectedModel.name}',
);
} catch (e) {
// If server returned a name instead of ID, attempt exact name match.
final byName = models
.where((m) => m.name == defaultModelId)
.toList();
if (byName.length == 1) {
selectedModel = byName.first;
foundation.debugPrint(
'DEBUG: Server default "$defaultModelId" matched by name; '
'selected ${selectedModel.name} (${selectedModel.id})',
);
} else {
foundation.debugPrint(
'DEBUG: Server default model "$defaultModelId" not found by ID; '
'falling back to first available',
);
selectedModel = models.first;
}
}
} else {
// No server default, use first available model
selectedModel = models.first;
foundation.debugPrint(
'DEBUG: No server default model, using first available: ${selectedModel.name}',
);
}
} catch (apiError) {
foundation.debugPrint(
'DEBUG: Failed to get default model from server: $apiError',
);
// Use first available model as fallback
selectedModel = models.first;
foundation.debugPrint(
'DEBUG: Using first available model as fallback: ${selectedModel.name}',
);
}
}
// Update selection immediately inside provider context
if (!ref.read(isManualModelSelectionProvider)) { if (!ref.read(isManualModelSelectionProvider)) {
ref.read(selectedModelProvider.notifier).state = selectedModel; ref.read(selectedModelProvider.notifier).state = selectedModel;
foundation.debugPrint('DEBUG: Set default model: ${selectedModel.name}'); foundation.debugPrint(
'DEBUG: Set default model (fallback): ${selectedModel.name}',
);
} }
return selectedModel; return selectedModel;
} catch (e) { } catch (e) {
foundation.debugPrint('DEBUG: Error setting default model: $e'); foundation.debugPrint('DEBUG: Error setting default model: $e');
// Final fallback: try to select any available model
try {
final models = await ref.read(modelsProvider.future);
if (models.isNotEmpty) {
final fallbackModel = models.first;
if (!ref.read(isManualModelSelectionProvider)) {
ref.read(selectedModelProvider.notifier).state = fallbackModel;
foundation.debugPrint(
'DEBUG: Fallback to first available model: ${fallbackModel.name}',
);
}
return fallbackModel;
}
} catch (fallbackError) {
foundation.debugPrint(
'DEBUG: Error in fallback model selection: $fallbackError',
);
}
return null; return null;
} }
}); });
@@ -826,7 +793,7 @@ final backgroundModelLoadProvider = Provider<void>((ref) {
// Schedule background loading without blocking // Schedule background loading without blocking
Future.microtask(() async { Future.microtask(() async {
// Wait a bit to ensure auth is complete // Wait a bit to ensure auth is complete
await Future.delayed(const Duration(milliseconds: 1500)); await Future.delayed(const Duration(milliseconds: 200));
foundation.debugPrint('DEBUG: Starting background model loading'); foundation.debugPrint('DEBUG: Starting background model loading');

View File

@@ -279,8 +279,26 @@ class _ChatPageState extends ConsumerState<ChatPage> {
} }
void _handleMessageSend(String text, dynamic selectedModel) async { void _handleMessageSend(String text, dynamic selectedModel) async {
// Resolve model on-demand if none selected yet
if (selectedModel == null) { if (selectedModel == null) {
return; try {
// Prefer already-loaded models
List<Model> models;
final modelsAsync = ref.read(modelsProvider);
if (modelsAsync.hasValue) {
models = modelsAsync.value!;
} else {
models = await ref.read(modelsProvider.future);
}
if (models.isNotEmpty) {
selectedModel = models.first;
ref.read(selectedModelProvider.notifier).state = selectedModel;
}
} catch (_) {
// If models cannot be resolved, bail out without sending
return;
}
if (selectedModel == null) return;
} }
final isOnline = ref.read(isOnlineProvider); final isOnline = ref.read(isOnlineProvider);
@@ -884,16 +902,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
).animate().fadeIn(delay: const Duration(milliseconds: 150)), ).animate().fadeIn(delay: const Duration(milliseconds: 150)),
const SizedBox(height: Spacing.sm),
Text(
l10n.typeBelowToBegin,
style: theme.textTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textSecondary,
fontWeight: FontWeight.w400,
),
textAlign: TextAlign.center,
).animate().fadeIn(delay: const Duration(milliseconds: 300)),
], ],
), ),
), ),
@@ -945,8 +953,8 @@ class _ChatPageState extends ConsumerState<ChatPage> {
}); });
} }
// Focus composer on app startup once, when a model is selected // Focus composer on app startup once
if (!_didStartupFocus && selectedModel != null) { if (!_didStartupFocus) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return; if (!mounted) return;
final current = ref.read(inputFocusTriggerProvider); final current = ref.read(inputFocusTriggerProvider);
@@ -1425,7 +1433,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
}, },
child: ModernChatInput( child: ModernChatInput(
enabled: enabled:
selectedModel != null &&
(isOnline || ref.watch(reviewerModeProvider)), (isOnline || ref.watch(reviewerModeProvider)),
onSendMessage: (text) => onSendMessage: (text) =>
_handleMessageSend(text, selectedModel), _handleMessageSend(text, selectedModel),

View File

@@ -34,6 +34,7 @@
"noFilesYet": "Noch keine Dateien", "noFilesYet": "Noch keine Dateien",
"uploadDocsPrompt": "Lade Dokumente hoch, um sie in deinen Unterhaltungen mit Conduit zu verwenden", "uploadDocsPrompt": "Lade Dokumente hoch, um sie in deinen Unterhaltungen mit Conduit zu verwenden",
"uploadFirstFile": "Erste Datei hochladen", "uploadFirstFile": "Erste Datei hochladen",
"attachments": "Anhänge",
"knowledgeBaseEmpty": "Wissensdatenbank ist leer", "knowledgeBaseEmpty": "Wissensdatenbank ist leer",
"createCollectionsPrompt": "Erstelle Sammlungen verwandter Dokumente zur einfachen Referenz", "createCollectionsPrompt": "Erstelle Sammlungen verwandter Dokumente zur einfachen Referenz",
"chooseSourcePhoto": "Quelle auswählen", "chooseSourcePhoto": "Quelle auswählen",
@@ -91,7 +92,7 @@
"next": "Weiter", "next": "Weiter",
"done": "Fertig", "done": "Fertig",
"onboardStartTitle": "Hallo, {username}", "onboardStartTitle": "Hallo, {username}",
"onboardStartSubtitle": "Wähle ein Modell und tippe los. Tippe jederzeit auf Neuer Chat.", "onboardStartSubtitle": "Wähle ein Modell, um loszulegen. Tippe jederzeit auf Neuer Chat.",
"onboardStartBullet1": "Modellname oben antippen, um zu wechseln", "onboardStartBullet1": "Modellname oben antippen, um zu wechseln",
"onboardStartBullet2": "Mit Neuer Chat den Kontext zurücksetzen", "onboardStartBullet2": "Mit Neuer Chat den Kontext zurücksetzen",
"onboardAttachTitle": "Kontext hinzufügen", "onboardAttachTitle": "Kontext hinzufügen",
@@ -204,7 +205,6 @@
"failedToDeleteFolder": "Ordner konnte nicht gelöscht werden", "failedToDeleteFolder": "Ordner konnte nicht gelöscht werden",
"aboutApp": "Über die App", "aboutApp": "Über die App",
"aboutAppSubtitle": "Conduit Informationen und Links", "aboutAppSubtitle": "Conduit Informationen und Links",
"typeBelowToBegin": "Unten tippen, um zu beginnen",
"web": "Web", "web": "Web",
"imageGen": "Bild-Gen", "imageGen": "Bild-Gen",
"pinned": "Angeheftet", "pinned": "Angeheftet",

View File

@@ -204,7 +204,7 @@
"@done": {"description": "Onboarding: finish the flow."}, "@done": {"description": "Onboarding: finish the flow."},
"onboardStartTitle": "Hello, {username}", "onboardStartTitle": "Hello, {username}",
"@onboardStartTitle": {"description": "Onboarding card: start chatting title.", "placeholders": {"username": {"type": "String", "example": "Alex"}}}, "@onboardStartTitle": {"description": "Onboarding card: start chatting title.", "placeholders": {"username": {"type": "String", "example": "Alex"}}},
"onboardStartSubtitle": "Choose a model, then type below to begin. Tap New Chat anytime.", "onboardStartSubtitle": "Choose a model to get started. Tap New Chat anytime.",
"@onboardStartSubtitle": {"description": "Onboarding card: brief guidance to begin a chat."}, "@onboardStartSubtitle": {"description": "Onboarding card: brief guidance to begin a chat."},
"onboardStartBullet1": "Tap the model name in the top bar to switch models", "onboardStartBullet1": "Tap the model name in the top bar to switch models",
"@onboardStartBullet1": {"description": "Bullet: how to switch models."}, "@onboardStartBullet1": {"description": "Bullet: how to switch models."},
@@ -417,8 +417,6 @@
"@aboutApp": {"description": "Settings tile title to view app information."}, "@aboutApp": {"description": "Settings tile title to view app information."},
"aboutAppSubtitle": "Conduit information and links", "aboutAppSubtitle": "Conduit information and links",
"@aboutAppSubtitle": {"description": "Subtitle/description for the About section."}, "@aboutAppSubtitle": {"description": "Subtitle/description for the About section."},
"typeBelowToBegin": "Type below to begin",
"@typeBelowToBegin": {"description": "Hint shown in empty chat input area."},
"web": "Web", "web": "Web",
"@web": {"description": "Tab/section label for web features."}, "@web": {"description": "Tab/section label for web features."},
"imageGen": "Image Gen", "imageGen": "Image Gen",

View File

@@ -34,6 +34,7 @@
"noFilesYet": "Pas encore de fichiers", "noFilesYet": "Pas encore de fichiers",
"uploadDocsPrompt": "Importez des documents à utiliser dans vos conversations avec Conduit", "uploadDocsPrompt": "Importez des documents à utiliser dans vos conversations avec Conduit",
"uploadFirstFile": "Importer votre premier fichier", "uploadFirstFile": "Importer votre premier fichier",
"attachments": "Pièces jointes",
"knowledgeBaseEmpty": "La base de connaissances est vide", "knowledgeBaseEmpty": "La base de connaissances est vide",
"createCollectionsPrompt": "Créez des collections de documents liés pour une référence facile", "createCollectionsPrompt": "Créez des collections de documents liés pour une référence facile",
"chooseSourcePhoto": "Choisir la source", "chooseSourcePhoto": "Choisir la source",
@@ -91,7 +92,7 @@
"next": "Suivant", "next": "Suivant",
"done": "Terminé", "done": "Terminé",
"onboardStartTitle": "Bonjour, {username}", "onboardStartTitle": "Bonjour, {username}",
"onboardStartSubtitle": "Choisissez un modèle puis commencez à écrire. Touchez Nouveau chat à tout moment.", "onboardStartSubtitle": "Choisissez un modèle pour commencer. Touchez Nouveau chat à tout moment.",
"onboardStartBullet1": "Touchez le nom du modèle en haut pour changer", "onboardStartBullet1": "Touchez le nom du modèle en haut pour changer",
"onboardStartBullet2": "Utilisez Nouveau chat pour réinitialiser le contexte", "onboardStartBullet2": "Utilisez Nouveau chat pour réinitialiser le contexte",
"onboardAttachTitle": "Ajouter du contexte", "onboardAttachTitle": "Ajouter du contexte",
@@ -204,7 +205,6 @@
"failedToDeleteFolder": "Échec de la suppression du dossier", "failedToDeleteFolder": "Échec de la suppression du dossier",
"aboutApp": "À propos de lapplication", "aboutApp": "À propos de lapplication",
"aboutAppSubtitle": "Informations et liens Conduit", "aboutAppSubtitle": "Informations et liens Conduit",
"typeBelowToBegin": "Saisissez cidessous pour commencer",
"web": "Web", "web": "Web",
"imageGen": "Gén. image", "imageGen": "Gén. image",
"pinned": "Épinglé", "pinned": "Épinglé",

View File

@@ -34,6 +34,7 @@
"noFilesYet": "Ancora nessun file", "noFilesYet": "Ancora nessun file",
"uploadDocsPrompt": "Carica documenti da usare nelle conversazioni con Conduit", "uploadDocsPrompt": "Carica documenti da usare nelle conversazioni con Conduit",
"uploadFirstFile": "Carica il tuo primo file", "uploadFirstFile": "Carica il tuo primo file",
"attachments": "Allegati",
"knowledgeBaseEmpty": "La base di conoscenza è vuota", "knowledgeBaseEmpty": "La base di conoscenza è vuota",
"createCollectionsPrompt": "Crea raccolte di documenti correlati per un rapido riferimento", "createCollectionsPrompt": "Crea raccolte di documenti correlati per un rapido riferimento",
"chooseSourcePhoto": "Scegli origine", "chooseSourcePhoto": "Scegli origine",
@@ -91,7 +92,7 @@
"next": "Avanti", "next": "Avanti",
"done": "Fatto", "done": "Fatto",
"onboardStartTitle": "Ciao, {username}", "onboardStartTitle": "Ciao, {username}",
"onboardStartSubtitle": "Scegli un modello e inizia a scrivere. Tocca Nuova chat in qualsiasi momento.", "onboardStartSubtitle": "Scegli un modello per iniziare. Tocca Nuova chat in qualsiasi momento.",
"onboardStartBullet1": "Tocca il nome del modello in alto per cambiare", "onboardStartBullet1": "Tocca il nome del modello in alto per cambiare",
"onboardStartBullet2": "Usa Nuova chat per azzerare il contesto", "onboardStartBullet2": "Usa Nuova chat per azzerare il contesto",
"onboardAttachTitle": "Aggiungi contesto", "onboardAttachTitle": "Aggiungi contesto",
@@ -204,7 +205,6 @@
"failedToDeleteFolder": "Impossibile eliminare la cartella", "failedToDeleteFolder": "Impossibile eliminare la cartella",
"aboutApp": "Informazioni sullapp", "aboutApp": "Informazioni sullapp",
"aboutAppSubtitle": "Informazioni e link di Conduit", "aboutAppSubtitle": "Informazioni e link di Conduit",
"typeBelowToBegin": "Scrivi qui sotto per iniziare",
"web": "Web", "web": "Web",
"imageGen": "Gen. immagini", "imageGen": "Gen. immagini",
"pinned": "Fissati", "pinned": "Fissati",

View File

@@ -657,7 +657,7 @@ abstract class AppLocalizations {
/// Onboarding card: brief guidance to begin a chat. /// Onboarding card: brief guidance to begin a chat.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Choose a model, then type below to begin. Tap New Chat anytime.'** /// **'Choose a model to get started. Tap New Chat anytime.'**
String get onboardStartSubtitle; String get onboardStartSubtitle;
/// Bullet: how to switch models. /// Bullet: how to switch models.
@@ -1218,12 +1218,6 @@ abstract class AppLocalizations {
/// **'Conduit information and links'** /// **'Conduit information and links'**
String get aboutAppSubtitle; String get aboutAppSubtitle;
/// Hint shown in empty chat input area.
///
/// In en, this message translates to:
/// **'Type below to begin'**
String get typeBelowToBegin;
/// Tab/section label for web features. /// Tab/section label for web features.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -125,7 +125,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get uploadFirstFile => 'Erste Datei hochladen'; String get uploadFirstFile => 'Erste Datei hochladen';
@override @override
String get attachments => 'Attachments'; String get attachments => 'Anhänge';
@override @override
String get knowledgeBaseEmpty => 'Wissensdatenbank ist leer'; String get knowledgeBaseEmpty => 'Wissensdatenbank ist leer';
@@ -320,7 +320,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get onboardStartSubtitle => String get onboardStartSubtitle =>
'Wähle ein Modell und tippe los. Tippe jederzeit auf Neuer Chat.'; 'Wähle ein Modell, um loszulegen. Tippe jederzeit auf Neuer Chat.';
@override @override
String get onboardStartBullet1 => 'Modellname oben antippen, um zu wechseln'; String get onboardStartBullet1 => 'Modellname oben antippen, um zu wechseln';
@@ -621,9 +621,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get aboutAppSubtitle => 'Conduit Informationen und Links'; String get aboutAppSubtitle => 'Conduit Informationen und Links';
@override
String get typeBelowToBegin => 'Unten tippen, um zu beginnen';
@override @override
String get web => 'Web'; String get web => 'Web';

View File

@@ -316,7 +316,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get onboardStartSubtitle => String get onboardStartSubtitle =>
'Choose a model, then type below to begin. Tap New Chat anytime.'; 'Choose a model to get started. Tap New Chat anytime.';
@override @override
String get onboardStartBullet1 => String get onboardStartBullet1 =>
@@ -616,9 +616,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get aboutAppSubtitle => 'Conduit information and links'; String get aboutAppSubtitle => 'Conduit information and links';
@override
String get typeBelowToBegin => 'Type below to begin';
@override @override
String get web => 'Web'; String get web => 'Web';

View File

@@ -124,7 +124,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get uploadFirstFile => 'Importer votre premier fichier'; String get uploadFirstFile => 'Importer votre premier fichier';
@override @override
String get attachments => 'Attachments'; String get attachments => 'Pièces jointes';
@override @override
String get knowledgeBaseEmpty => 'La base de connaissances est vide'; String get knowledgeBaseEmpty => 'La base de connaissances est vide';
@@ -321,7 +321,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get onboardStartSubtitle => String get onboardStartSubtitle =>
'Choisissez un modèle puis commencez à écrire. Touchez Nouveau chat à tout moment.'; 'Choisissez un modèle pour commencer. Touchez Nouveau chat à tout moment.';
@override @override
String get onboardStartBullet1 => String get onboardStartBullet1 =>
@@ -626,9 +626,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get aboutAppSubtitle => 'Informations et liens Conduit'; String get aboutAppSubtitle => 'Informations et liens Conduit';
@override
String get typeBelowToBegin => 'Saisissez cidessous pour commencer';
@override @override
String get web => 'Web'; String get web => 'Web';

View File

@@ -123,7 +123,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get uploadFirstFile => 'Carica il tuo primo file'; String get uploadFirstFile => 'Carica il tuo primo file';
@override @override
String get attachments => 'Attachments'; String get attachments => 'Allegati';
@override @override
String get knowledgeBaseEmpty => 'La base di conoscenza è vuota'; String get knowledgeBaseEmpty => 'La base di conoscenza è vuota';
@@ -316,7 +316,7 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get onboardStartSubtitle => String get onboardStartSubtitle =>
'Scegli un modello e inizia a scrivere. Tocca Nuova chat in qualsiasi momento.'; 'Scegli un modello per iniziare. Tocca Nuova chat in qualsiasi momento.';
@override @override
String get onboardStartBullet1 => String get onboardStartBullet1 =>
@@ -619,9 +619,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get aboutAppSubtitle => 'Informazioni e link di Conduit'; String get aboutAppSubtitle => 'Informazioni e link di Conduit';
@override
String get typeBelowToBegin => 'Scrivi qui sotto per iniziare';
@override @override
String get web => 'Web'; String get web => 'Web';

View File

@@ -26,7 +26,9 @@ void main() async {
// Enable edge-to-edge globally (back-compat on pre-Android 15) // Enable edge-to-edge globally (back-compat on pre-Android 15)
// Pairs with Activity's EdgeToEdge.enable and our SafeArea usage. // Pairs with Activity's EdgeToEdge.enable and our SafeArea usage.
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); // Do not block first frame on system UI mode; apply shortly after startup
// ignore: discarded_futures
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
final sharedPrefs = await SharedPreferences.getInstance(); final sharedPrefs = await SharedPreferences.getInstance();
const secureStorage = FlutterSecureStorage( const secureStorage = FlutterSecureStorage(
@@ -65,7 +67,8 @@ class _ConduitAppState extends ConsumerState<ConduitApp> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initializeAppState(); // Defer heavy provider initialization to after first frame to render UI sooner
WidgetsBinding.instance.addPostFrameCallback((_) => _initializeAppState());
} }
Widget _buildInitialLoadingSkeleton(BuildContext context) { Widget _buildInitialLoadingSkeleton(BuildContext context) {
@@ -142,14 +145,11 @@ class _ConduitAppState extends ConsumerState<ConduitApp> {
return MediaQuery( return MediaQuery(
data: MediaQuery.of(context).copyWith( data: MediaQuery.of(context).copyWith(
// Ensure proper text scaling for edge-to-edge // Ensure proper text scaling for edge-to-edge
textScaler: MediaQuery.of(context).textScaler.clamp( textScaler: MediaQuery.of(
minScaleFactor: 0.8, context,
maxScaleFactor: 1.3, ).textScaler.clamp(minScaleFactor: 0.8, maxScaleFactor: 1.3),
),
),
child: OfflineIndicator(
child: child ?? const SizedBox.shrink(),
), ),
child: OfflineIndicator(child: child ?? const SizedBox.shrink()),
); );
}, },
home: _getInitialPageWithReactiveState(), home: _getInitialPageWithReactiveState(),