refactor: improve app start time
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 l’application",
|
"aboutApp": "À propos de l’application",
|
||||||
"aboutAppSubtitle": "Informations et liens Conduit",
|
"aboutAppSubtitle": "Informations et liens Conduit",
|
||||||
"typeBelowToBegin": "Saisissez ci‑dessous pour commencer",
|
|
||||||
"web": "Web",
|
"web": "Web",
|
||||||
"imageGen": "Gén. image",
|
"imageGen": "Gén. image",
|
||||||
"pinned": "Épinglé",
|
"pinned": "Épinglé",
|
||||||
|
|||||||
@@ -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 sull’app",
|
"aboutApp": "Informazioni sull’app",
|
||||||
"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",
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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 ci‑dessous pour commencer';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get web => 'Web';
|
String get web => 'Web';
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user