From ebe6cec17c8bef9483a3f78505986d3b1ded6c35 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Wed, 1 Oct 2025 23:26:12 +0530 Subject: [PATCH] feat: enhance routing and connectivity handling - Added a new route for connection issues, allowing users to navigate to a dedicated page when the server is unreachable. - Updated the RouterNotifier to manage navigation based on server connectivity status and authentication state. - Improved the handling of offline scenarios by integrating connectivity checks into the routing logic. - Enhanced localization support for connection issue messages in multiple languages. - Refactored the OfflineIndicator widget to streamline the display of connectivity status without unnecessary complexity. --- lib/core/router/app_router.dart | 43 ++- lib/core/services/navigation_service.dart | 2 + .../auth/views/connection_issue_page.dart | 314 ++++++++++++++++++ lib/features/chat/views/chat_page.dart | 12 +- .../navigation/widgets/chats_drawer.dart | 4 +- lib/l10n/app_de.arb | 13 +- lib/l10n/app_en.arb | 14 +- lib/l10n/app_fr.arb | 13 +- lib/l10n/app_it.arb | 13 +- lib/l10n/app_localizations.dart | 24 +- lib/l10n/app_localizations_de.dart | 15 +- lib/l10n/app_localizations_en.dart | 14 +- lib/l10n/app_localizations_fr.dart | 15 +- lib/l10n/app_localizations_it.dart | 15 +- lib/shared/theme/theme_extensions.dart | 30 +- lib/shared/widgets/offline_indicator.dart | 130 ++------ 16 files changed, 523 insertions(+), 148 deletions(-) create mode 100644 lib/features/auth/views/connection_issue_page.dart diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index a20858e..673fed8 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -4,11 +4,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../providers/app_providers.dart'; +import '../services/connectivity_service.dart'; import '../services/navigation_service.dart'; import '../utils/debug_logger.dart'; import '../../features/auth/providers/unified_auth_providers.dart'; import '../../features/auth/views/authentication_page.dart'; import '../../features/auth/views/connect_signin_page.dart'; +import '../../features/auth/views/connection_issue_page.dart'; import '../../features/auth/views/server_connection_page.dart'; import '../../features/chat/views/chat_page.dart'; import '../../features/navigation/views/splash_launcher_page.dart'; @@ -67,19 +69,34 @@ class RouterNotifier extends ChangeNotifier { } if (activeServerAsync.hasError) { - return location == Routes.serverConnection - ? null - : Routes.serverConnection; + return location == Routes.connectionIssue ? null : Routes.connectionIssue; } final activeServer = activeServerAsync.asData?.value; - if (activeServer == null) { + final hasActiveServer = activeServer != null; + if (!hasActiveServer) { // Allow auth-related routes while no server configured if (_isAuthLocation(location)) return null; return Routes.serverConnection; } + if (location == Routes.serverConnection) { + return Routes.chat; + } + final authState = ref.read(authNavigationStateProvider); + final connectivityAsync = ref.read(connectivityStatusProvider); + final connectivity = connectivityAsync.asData?.value; + + final shouldShowConnectionIssue = + !reviewerMode && + connectivity == ConnectivityStatus.offline && + authState != AuthNavigationState.needsLogin; + + if (shouldShowConnectionIssue) { + return location == Routes.connectionIssue ? null : Routes.connectionIssue; + } + switch (authState) { case AuthNavigationState.loading: // Keep user on auth routes while loading to prevent bounce @@ -87,12 +104,16 @@ class RouterNotifier extends ChangeNotifier { // Otherwise keep splash during session establishment return location == Routes.splash ? null : Routes.splash; case AuthNavigationState.needsLogin: + if (location == Routes.connectionIssue) return null; + return null; case AuthNavigationState.error: - if (_isAuthLocation(location)) return null; - return Routes.serverConnection; + if (location == Routes.connectionIssue) return null; + return null; case AuthNavigationState.authenticated: // Avoid unnecessary redirects if already on a non-auth route - if (_isAuthLocation(location) || location == Routes.splash) { + if (_isAuthLocation(location) || + location == Routes.splash || + location == Routes.connectionIssue) { return Routes.chat; } return null; @@ -102,7 +123,8 @@ class RouterNotifier extends ChangeNotifier { bool _isAuthLocation(String location) { return location == Routes.serverConnection || location == Routes.login || - location == Routes.authentication; + location == Routes.authentication || + location == Routes.connectionIssue; } @override @@ -145,6 +167,11 @@ final goRouterProvider = Provider((ref) { name: RouteNames.serverConnection, builder: (context, state) => const ServerConnectionPage(), ), + GoRoute( + path: Routes.connectionIssue, + name: RouteNames.connectionIssue, + builder: (context, state) => const ConnectionIssuePage(), + ), GoRoute( path: Routes.authentication, name: RouteNames.authentication, diff --git a/lib/core/services/navigation_service.dart b/lib/core/services/navigation_service.dart index a43aa05..78798ef 100644 --- a/lib/core/services/navigation_service.dart +++ b/lib/core/services/navigation_service.dart @@ -95,6 +95,7 @@ class Routes { static const String chat = '/chat'; static const String login = '/login'; static const String serverConnection = '/server-connection'; + static const String connectionIssue = '/connection-issue'; static const String authentication = '/authentication'; static const String profile = '/profile'; static const String appCustomization = '/profile/customization'; @@ -106,6 +107,7 @@ class RouteNames { static const String chat = 'chat'; static const String login = 'login'; static const String serverConnection = 'server-connection'; + static const String connectionIssue = 'connection-issue'; static const String authentication = 'authentication'; static const String profile = 'profile'; static const String appCustomization = 'app-customization'; diff --git a/lib/features/auth/views/connection_issue_page.dart b/lib/features/auth/views/connection_issue_page.dart new file mode 100644 index 0000000..6a20c09 --- /dev/null +++ b/lib/features/auth/views/connection_issue_page.dart @@ -0,0 +1,314 @@ +import 'dart:io' show Platform; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../core/models/server_config.dart'; +import '../../../core/providers/app_providers.dart'; +import '../../../core/services/connectivity_service.dart'; +import '../../../core/services/navigation_service.dart'; +import '../../../core/widgets/error_boundary.dart'; +import '../../../l10n/app_localizations.dart'; +import '../../../shared/theme/theme_extensions.dart'; +import '../../../shared/widgets/conduit_components.dart'; +import '../providers/unified_auth_providers.dart'; + +class ConnectionIssuePage extends ConsumerStatefulWidget { + const ConnectionIssuePage({super.key}); + + @override + ConsumerState createState() => + _ConnectionIssuePageState(); +} + +class _ConnectionIssuePageState extends ConsumerState { + bool _isRetrying = false; + bool _isLoggingOut = false; + String? _statusMessage; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final connectivityAsync = ref.watch(connectivityStatusProvider); + final connectivity = connectivityAsync.asData?.value; + final activeServerAsync = ref.watch(activeServerProvider); + final activeServer = activeServerAsync.asData?.value; + + return ErrorBoundary( + child: Scaffold( + backgroundColor: context.conduitTheme.surfaceBackground, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.pagePadding, + vertical: Spacing.lg, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Align( + alignment: Alignment.centerLeft, + child: ConduitIconButton( + icon: Platform.isIOS + ? CupertinoIcons.gear_alt_fill + : Icons.settings_ethernet, + onPressed: () => context.go(Routes.serverConnection), + tooltip: l10n.backToServerSetup, + ), + ), + Expanded( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildHeader(context, l10n, connectivity), + if (activeServer != null) ...[ + const SizedBox(height: Spacing.sm), + _buildServerDetails(context, activeServer), + ], + const SizedBox(height: Spacing.md), + Text( + l10n.connectionIssueSubtitle, + textAlign: TextAlign.center, + style: context.conduitTheme.bodyMedium?.copyWith( + color: context.conduitTheme.textSecondary, + height: 1.45, + ), + ), + ], + ), + ), + ), + ), + _buildActions(context, l10n), + if (_statusMessage != null) ...[ + const SizedBox(height: Spacing.sm), + _buildStatusMessage(context, _statusMessage!), + ], + ], + ), + ), + ), + ), + ); + } + + Widget _buildHeader( + BuildContext context, + AppLocalizations l10n, + ConnectivityStatus? connectivity, + ) { + final iconColor = context.conduitTheme.error; + final statusText = _statusLabel(connectivity, l10n); + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 72, + height: 72, + decoration: BoxDecoration( + color: context.conduitTheme.surfaceContainerHighest, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.08), + blurRadius: 18, + offset: const Offset(0, 12), + ), + ], + ), + child: Icon( + Platform.isIOS + ? CupertinoIcons.wifi_exclamationmark + : Icons.wifi_off_rounded, + color: iconColor, + size: 34, + ), + ), + const SizedBox(height: Spacing.md), + Text( + l10n.connectionIssueTitle, + textAlign: TextAlign.center, + style: context.conduitTheme.headingMedium?.copyWith( + fontWeight: FontWeight.w700, + color: context.conduitTheme.textPrimary, + ), + ), + if (statusText != null) ...[ + const SizedBox(height: Spacing.xs), + Text( + statusText, + textAlign: TextAlign.center, + style: context.conduitTheme.bodySmall?.copyWith( + color: context.conduitTheme.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + ], + ], + ); + } + + Widget _buildServerDetails(BuildContext context, ServerConfig server) { + final host = _resolveHost(server); + + return Column( + children: [ + Text( + host, + textAlign: TextAlign.center, + style: context.conduitTheme.bodyMedium?.copyWith( + color: context.conduitTheme.textPrimary, + fontFamily: 'monospace', + ), + ), + const SizedBox(height: Spacing.xs), + Text( + server.url, + textAlign: TextAlign.center, + style: context.conduitTheme.bodySmall?.copyWith( + color: context.conduitTheme.textSecondary, + ), + ), + ], + ); + } + + Widget _buildActions(BuildContext context, AppLocalizations l10n) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: Spacing.md), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ConduitButton( + text: l10n.retry, + onPressed: _isRetrying || _isLoggingOut ? null : () => _retry(l10n), + isLoading: _isRetrying, + icon: Platform.isIOS + ? CupertinoIcons.refresh + : Icons.refresh_rounded, + isFullWidth: true, + ), + const SizedBox(height: Spacing.sm), + ConduitButton( + text: l10n.signOut, + onPressed: _isRetrying || _isLoggingOut + ? null + : () => _logout(l10n), + isLoading: _isLoggingOut, + isSecondary: true, + icon: Platform.isIOS + ? CupertinoIcons.arrow_turn_up_left + : Icons.logout, + isFullWidth: true, + isCompact: true, + ), + ], + ), + ); + } + + Widget _buildStatusMessage(BuildContext context, String message) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: Spacing.md), + child: Text( + message, + textAlign: TextAlign.center, + style: context.conduitTheme.bodySmall?.copyWith( + color: context.conduitTheme.textSecondary, + ), + ), + ); + } + + Future _retry(AppLocalizations l10n) async { + setState(() { + _isRetrying = true; + _statusMessage = null; + }); + + try { + final service = ref.read(connectivityServiceProvider); + final isOnline = await service.checkConnectivity(); + + if (!mounted) return; + + if (isOnline) { + await ref.read(authActionsProvider).refresh(); + } else { + setState(() { + _statusMessage = l10n.stillOfflineMessage; + }); + } + } catch (_) { + if (!mounted) return; + setState(() { + _statusMessage = l10n.couldNotConnectGeneric; + }); + } finally { + if (mounted) { + setState(() { + _isRetrying = false; + }); + } + } + } + + Future _logout(AppLocalizations l10n) async { + setState(() { + _isLoggingOut = true; + _statusMessage = null; + }); + + try { + await ref.read(authActionsProvider).logout(); + } catch (_) { + if (!mounted) return; + setState(() { + _statusMessage = l10n.couldNotConnectGeneric; + }); + } finally { + if (mounted) { + setState(() { + _isLoggingOut = false; + }); + } + } + } + + String _resolveHost(ServerConfig? config) { + final url = config?.url; + if (url == null || url.isEmpty) { + return 'Open WebUI'; + } + + try { + final uri = Uri.parse(url); + if (uri.host.isNotEmpty) { + return uri.host; + } + return url; + } catch (_) { + return url; + } + } + + String? _statusLabel(ConnectivityStatus? status, AppLocalizations l10n) { + switch (status) { + case ConnectivityStatus.online: + return l10n.connectedToServer; + case ConnectivityStatus.offline: + return l10n.pleaseCheckConnection; + case ConnectivityStatus.checking: + case null: + return null; + } + } +} diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index b5ae4d8..9795a93 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -1158,8 +1158,8 @@ class _ChatPageState extends ConsumerState { backgroundColor: context.conduitTheme.surfaceBackground, // Left navigation drawer with draggable edge open (native, finger-following) drawerEnableOpenDragGesture: true, - drawerDragStartBehavior: DragStartBehavior.down, - drawerEdgeDragWidth: MediaQuery.of(context).size.width * 0.5, + drawerDragStartBehavior: DragStartBehavior.start, + drawerEdgeDragWidth: MediaQuery.of(context).size.width * 0.75, drawerScrimColor: Colors.black.withValues(alpha: 0.32), drawer: Drawer( width: (MediaQuery.of(context).size.width * 0.80).clamp( @@ -1167,7 +1167,13 @@ class _ChatPageState extends ConsumerState { 420.0, ), backgroundColor: context.conduitTheme.surfaceBackground, - child: const SafeArea(child: ChatsDrawer()), + child: SafeArea( + top: true, + bottom: true, + left: false, + right: false, + child: const ChatsDrawer(), + ), ), appBar: AppBar( backgroundColor: context.conduitTheme.surfaceBackground, diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index 1ab25bf..38c7c8d 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -149,8 +149,8 @@ class _ChatsDrawerState extends ConsumerState { children: [ Padding( padding: const EdgeInsets.fromLTRB( - Spacing.md, - 0, + Spacing.inputPadding, + Spacing.sm, Spacing.md, Spacing.sm, ), diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 8ed6895..d005786 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -8,6 +8,18 @@ "loadingProfile": "Profil wird geladen...", "unableToLoadProfile": "Profil konnte nicht geladen werden", "pleaseCheckConnection": "Bitte überprüfe deine Verbindung und versuche es erneut", + "connectionIssueTitle": "Server nicht erreichbar", + "@connectionIssueTitle": { + "description": "Titel, wenn der konfigurierte Server nicht erreichbar ist" + }, + "connectionIssueSubtitle": "Verbindung wiederherstellen oder abmelden, um einen anderen Server zu wählen.", + "@connectionIssueSubtitle": { + "description": "Untertitel mit den verfügbaren Aktionen, wenn der Server nicht erreichbar ist" + }, + "stillOfflineMessage": "Der Server ist weiterhin nicht erreichbar. Prüfe deine Verbindung und versuche es erneut.", + "@stillOfflineMessage": { + "description": "Statusnachricht nach einem erneuten Versuch ohne wiederhergestellte Verbindung" + }, "account": "Konto", "signOut": "Abmelden", "endYourSession": "Sitzung beenden", @@ -129,7 +141,6 @@ "invalidImageFormat": "Ungültiges Bildformat", "emptyImageData": "Leere Bilddaten" , - "offlineBanner": "Du bist offline. Einige Funktionen sind eingeschränkt.", "featureRequiresInternet": "Diese Funktion erfordert eine Internetverbindung", "messagesWillSendWhenOnline": "Nachrichten werden gesendet, sobald du wieder online bist", "confirm": "Bestätigen", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 80a1768..53ee8e4 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -15,6 +15,18 @@ "unableToLoadProfile": "Unable to load profile", "@unableToLoadProfile": {"description": "Error title shown when profile request fails."}, "pleaseCheckConnection": "Please check your connection and try again", + "connectionIssueTitle": "Can't reach your server", + "@connectionIssueTitle": { + "description": "Title shown when the configured server is unreachable" + }, + "connectionIssueSubtitle": "Reconnect to continue or sign out to choose a different server.", + "@connectionIssueSubtitle": { + "description": "Subtitle explaining available actions when the server cannot be reached" + }, + "stillOfflineMessage": "We still can't reach the server. Double-check your connection and try again.", + "@stillOfflineMessage": { + "description": "Status message after a retry when connectivity has not been restored" + }, "@pleaseCheckConnection": {"description": "Generic connectivity hint after an error."}, "account": "Account", "@account": {"description": "Section header for account-related options."}, @@ -282,8 +294,6 @@ "emptyImageData": "Empty image data", "@emptyImageData": {"description": "Error when image data buffer is empty."} , - "offlineBanner": "You're offline. Some features may be limited.", - "@offlineBanner": {"description": "Banner warning when device is offline."}, "featureRequiresInternet": "This feature requires an internet connection", "@featureRequiresInternet": {"description": "Informational text explaining internet requirement."}, "messagesWillSendWhenOnline": "Messages will be sent when you're back online", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index f49fb7d..760c18f 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -8,6 +8,18 @@ "loadingProfile": "Chargement du profil...", "unableToLoadProfile": "Impossible de charger le profil", "pleaseCheckConnection": "Veuillez vérifier votre connexion et réessayer", + "connectionIssueTitle": "Impossible d'atteindre votre serveur", + "@connectionIssueTitle": { + "description": "Titre affiché lorsque le serveur configuré est injoignable" + }, + "connectionIssueSubtitle": "Reconnectez-vous pour continuer ou déconnectez-vous pour choisir un autre serveur.", + "@connectionIssueSubtitle": { + "description": "Sous-titre expliquant les actions possibles quand le serveur est injoignable" + }, + "stillOfflineMessage": "Nous ne pouvons toujours pas joindre le serveur. Vérifiez votre connexion et réessayez.", + "@stillOfflineMessage": { + "description": "Message d'état après une tentative de reconnexion sans succès" + }, "account": "Compte", "signOut": "Se déconnecter", "endYourSession": "Terminer votre session", @@ -129,7 +141,6 @@ "invalidImageFormat": "Format d'image invalide", "emptyImageData": "Données d'image vides" , - "offlineBanner": "Vous êtes hors ligne. Certaines fonctions peuvent être limitées.", "featureRequiresInternet": "Cette fonctionnalité nécessite une connexion Internet", "messagesWillSendWhenOnline": "Les messages seront envoyés lorsque vous serez de nouveau en ligne", "confirm": "Confirmer", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 7d25ba8..b7c4167 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -8,6 +8,18 @@ "loadingProfile": "Caricamento profilo...", "unableToLoadProfile": "Impossibile caricare il profilo", "pleaseCheckConnection": "Controlla la connessione e riprova", + "connectionIssueTitle": "Impossibile raggiungere il server", + "@connectionIssueTitle": { + "description": "Titolo mostrato quando il server configurato non è raggiungibile" + }, + "connectionIssueSubtitle": "Riconnettiti per continuare oppure esci per scegliere un server diverso.", + "@connectionIssueSubtitle": { + "description": "Sottotitolo che spiega le azioni disponibili quando il server non è raggiungibile" + }, + "stillOfflineMessage": "Non riusciamo ancora a raggiungere il server. Controlla la connessione e riprova.", + "@stillOfflineMessage": { + "description": "Messaggio di stato dopo un tentativo di riconnessione senza successo" + }, "account": "Account", "signOut": "Esci", "endYourSession": "Termina la sessione", @@ -129,7 +141,6 @@ "invalidImageFormat": "Formato immagine non valido", "emptyImageData": "Dati immagine vuoti" , - "offlineBanner": "Sei offline. Alcune funzioni potrebbero essere limitate.", "featureRequiresInternet": "Questa funzione richiede una connessione Internet", "messagesWillSendWhenOnline": "I messaggi verranno inviati quando tornerai online", "confirm": "Conferma", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index b933b04..1d682a6 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -150,6 +150,24 @@ abstract class AppLocalizations { /// **'Please check your connection and try again'** String get pleaseCheckConnection; + /// Title shown when the configured server is unreachable + /// + /// In en, this message translates to: + /// **'Can\'t reach your server'** + String get connectionIssueTitle; + + /// Subtitle explaining available actions when the server cannot be reached + /// + /// In en, this message translates to: + /// **'Reconnect to continue or sign out to choose a different server.'** + String get connectionIssueSubtitle; + + /// Status message after a retry when connectivity has not been restored + /// + /// In en, this message translates to: + /// **'We still can\'t reach the server. Double-check your connection and try again.'** + String get stillOfflineMessage; + /// Section header for account-related options. /// /// In en, this message translates to: @@ -864,12 +882,6 @@ abstract class AppLocalizations { /// **'Empty image data'** String get emptyImageData; - /// Banner warning when device is offline. - /// - /// In en, this message translates to: - /// **'You\'re offline. Some features may be limited.'** - String get offlineBanner; - /// Informational text explaining internet requirement. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 90829e5..a33df3e 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -33,6 +33,17 @@ class AppLocalizationsDe extends AppLocalizations { String get pleaseCheckConnection => 'Bitte überprüfe deine Verbindung und versuche es erneut'; + @override + String get connectionIssueTitle => 'Server nicht erreichbar'; + + @override + String get connectionIssueSubtitle => + 'Verbindung wiederherstellen oder abmelden, um einen anderen Server zu wählen.'; + + @override + String get stillOfflineMessage => + 'Der Server ist weiterhin nicht erreichbar. Prüfe deine Verbindung und versuche es erneut.'; + @override String get account => 'Konto'; @@ -434,10 +445,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get emptyImageData => 'Leere Bilddaten'; - @override - String get offlineBanner => - 'Du bist offline. Einige Funktionen sind eingeschränkt.'; - @override String get featureRequiresInternet => 'Diese Funktion erfordert eine Internetverbindung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 99cf853..0581993 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -33,6 +33,17 @@ class AppLocalizationsEn extends AppLocalizations { String get pleaseCheckConnection => 'Please check your connection and try again'; + @override + String get connectionIssueTitle => 'Can\'t reach your server'; + + @override + String get connectionIssueSubtitle => + 'Reconnect to continue or sign out to choose a different server.'; + + @override + String get stillOfflineMessage => + 'We still can\'t reach the server. Double-check your connection and try again.'; + @override String get account => 'Account'; @@ -430,9 +441,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get emptyImageData => 'Empty image data'; - @override - String get offlineBanner => 'You\'re offline. Some features may be limited.'; - @override String get featureRequiresInternet => 'This feature requires an internet connection'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 1f92cbf..f4429fc 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -33,6 +33,17 @@ class AppLocalizationsFr extends AppLocalizations { String get pleaseCheckConnection => 'Veuillez vérifier votre connexion et réessayer'; + @override + String get connectionIssueTitle => 'Impossible d\'atteindre votre serveur'; + + @override + String get connectionIssueSubtitle => + 'Reconnectez-vous pour continuer ou déconnectez-vous pour choisir un autre serveur.'; + + @override + String get stillOfflineMessage => + 'Nous ne pouvons toujours pas joindre le serveur. Vérifiez votre connexion et réessayez.'; + @override String get account => 'Compte'; @@ -439,10 +450,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get emptyImageData => 'Données d\'image vides'; - @override - String get offlineBanner => - 'Vous êtes hors ligne. Certaines fonctions peuvent être limitées.'; - @override String get featureRequiresInternet => 'Cette fonctionnalité nécessite une connexion Internet'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 7376547..6f86b07 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -32,6 +32,17 @@ class AppLocalizationsIt extends AppLocalizations { @override String get pleaseCheckConnection => 'Controlla la connessione e riprova'; + @override + String get connectionIssueTitle => 'Impossibile raggiungere il server'; + + @override + String get connectionIssueSubtitle => + 'Riconnettiti per continuare oppure esci per scegliere un server diverso.'; + + @override + String get stillOfflineMessage => + 'Non riusciamo ancora a raggiungere il server. Controlla la connessione e riprova.'; + @override String get account => 'Account'; @@ -431,10 +442,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get emptyImageData => 'Dati immagine vuoti'; - @override - String get offlineBanner => - 'Sei offline. Alcune funzioni potrebbero essere limitate.'; - @override String get featureRequiresInternet => 'Questa funzione richiede una connessione Internet'; diff --git a/lib/shared/theme/theme_extensions.dart b/lib/shared/theme/theme_extensions.dart index ef16045..ba57212 100644 --- a/lib/shared/theme/theme_extensions.dart +++ b/lib/shared/theme/theme_extensions.dart @@ -535,13 +535,13 @@ class ConduitThemeExtension extends ThemeExtension { buttonDisabledText: AppTheme.neutral400, // Status and feedback colors - Enhanced visibility - success: Color(0xFF22C55E), + success: Color(0xFF34D399), successBackground: Color(0xFF14532D), - error: Color(0xFFEF4444), + error: Color(0xFFFCA5A5), errorBackground: Color(0xFF7F1D1D), - warning: Color(0xFFF59E0B), - warningBackground: Color(0xFF7C2D12), - info: Color(0xFF38BDF8), + warning: Color(0xFFFBBF24), + warningBackground: Color(0xFF451A03), + info: Color(0xFF93C5FD), infoBackground: Color(0xFF0C4A6E), // Navigation and UI element colors - Enhanced contrast @@ -549,7 +549,7 @@ class ConduitThemeExtension extends ThemeExtension { navigationBackground: Color(0xFF0A0D0C), navigationSelected: AppTheme.brandPrimary, navigationUnselected: AppTheme.neutral300, - navigationSelectedBackground: AppTheme.brandPrimary, + navigationSelectedBackground: Color(0xFF312E81), // Loading and animation colors - Enhanced visibility shimmerBase: Color(0xFF121514), @@ -660,21 +660,21 @@ class ConduitThemeExtension extends ThemeExtension { buttonDisabledText: AppTheme.neutral500, // Status and feedback colors - Enhanced visibility - success: Color(0xFF16A34A), - successBackground: Color(0xFFEFFBF3), - error: Color(0xFFDC2626), - errorBackground: Color(0xFFFDECEC), - warning: Color(0xFFD97706), - warningBackground: Color(0xFFFEF6E7), - info: Color(0xFF0284C7), - infoBackground: Color(0xFFE8F4FD), + success: Color(0xFF166534), + successBackground: Color(0xFFECFDF3), + error: Color(0xFFB91C1C), + errorBackground: Color(0xFFFEE2E2), + warning: Color(0xFF92400E), + warningBackground: Color(0xFFFEF3C7), + info: Color(0xFF1D4ED8), + infoBackground: Color(0xFFDBEAFE), // Navigation and UI element colors - Enhanced contrast dividerColor: AppTheme.neutral100, navigationBackground: AppTheme.neutral50, navigationSelected: AppTheme.brandPrimary, navigationUnselected: AppTheme.neutral600, - navigationSelectedBackground: AppTheme.brandPrimary, + navigationSelectedBackground: Color(0xFFE0E7FF), // Loading and animation colors - Enhanced visibility shimmerBase: Color(0xFFF3F4F6), diff --git a/lib/shared/widgets/offline_indicator.dart b/lib/shared/widgets/offline_indicator.dart index ff82b8f..b2a716b 100644 --- a/lib/shared/widgets/offline_indicator.dart +++ b/lib/shared/widgets/offline_indicator.dart @@ -15,13 +15,8 @@ part 'offline_indicator.g.dart'; class OfflineIndicator extends ConsumerWidget { final Widget child; - final bool showBanner; - const OfflineIndicator({ - super.key, - required this.child, - this.showBanner = true, - }); + const OfflineIndicator({super.key, required this.child}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -33,26 +28,22 @@ class OfflineIndicator extends ConsumerWidget { orElse: () => false, ); - return Stack( - children: [ - child, - if (showBanner) - connectivityStatus.when( - data: (status) { - if (status == ConnectivityStatus.offline || socketOffline) { - return _OfflineBanner(); - } - // Announce back-online briefly if we were previously offline - if (wasOffline) { - return const _BackOnlineToast(); - } - return const SizedBox.shrink(); - }, - loading: () => const SizedBox.shrink(), - error: (_, _) => _OfflineBanner(), - ), - ], + final overlay = connectivityStatus.when( + data: (status) { + if ((status == ConnectivityStatus.offline || socketOffline) && + !wasOffline) { + return const SizedBox.shrink(); + } + if (wasOffline) { + return const _BackOnlineToast(); + } + return const SizedBox.shrink(); + }, + loading: () => const SizedBox.shrink(), + error: (unusedError, unusedStackTrace) => const SizedBox.shrink(), ); + + return Stack(children: [child, overlay]); } } @@ -137,65 +128,6 @@ class _BackOnlineToast extends StatelessWidget { } } -class _OfflineBanner extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Positioned( - top: 0, - left: 0, - right: 0, - child: SafeArea( - bottom: false, - child: - Semantics( - container: true, - liveRegion: true, - label: AppLocalizations.of(context)!.offlineBanner, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.xs, - ), - decoration: BoxDecoration( - color: context.conduitTheme.warning, - boxShadow: ConduitShadows.low, - ), - child: Row( - children: [ - Icon( - Platform.isIOS - ? CupertinoIcons.wifi_slash - : Icons.wifi_off, - color: context.conduitTheme.textInverse, - size: AppTypography.headlineMedium, - ), - const SizedBox(width: Spacing.xs), - Expanded( - child: Text( - AppLocalizations.of(context)!.offlineBanner, - style: TextStyle( - color: context.conduitTheme.textInverse, - fontSize: AppTypography.labelLarge, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ), - ) - .animate(onPlay: (controller) => controller.forward()) - .slideY( - begin: -1, - end: 0, - duration: const Duration(milliseconds: 300), - curve: Curves.easeOutCubic, - ), - ), - ); - } -} - // Inline offline indicator for specific features class InlineOfflineIndicator extends ConsumerWidget { final String message; @@ -217,16 +149,20 @@ class InlineOfflineIndicator extends ConsumerWidget { return const SizedBox.shrink(); } + final theme = context.conduitTheme; + final surfaceColor = backgroundColor ?? theme.warningBackground; + final borderAlpha = Theme.of(context).brightness == Brightness.dark + ? 0.45 + : 0.3; + return Container( margin: const EdgeInsets.all(Spacing.md), padding: const EdgeInsets.all(Spacing.md), decoration: BoxDecoration( - color: - backgroundColor ?? - context.conduitTheme.warning.withValues(alpha: 0.1), + color: surfaceColor, borderRadius: BorderRadius.circular(AppBorderRadius.md), border: Border.all( - color: context.conduitTheme.warning.withValues(alpha: 0.3), + color: theme.warning.withValues(alpha: borderAlpha), width: BorderWidth.regular, ), ), @@ -235,7 +171,7 @@ class InlineOfflineIndicator extends ConsumerWidget { Icon( icon ?? (Platform.isIOS ? CupertinoIcons.wifi_slash : Icons.wifi_off), - color: context.conduitTheme.warning, + color: theme.warning, size: Spacing.lg, ), const SizedBox(width: Spacing.xs), @@ -245,7 +181,7 @@ class InlineOfflineIndicator extends ConsumerWidget { ? message : AppLocalizations.of(context)!.featureRequiresInternet, style: TextStyle( - color: context.conduitTheme.warning, + color: theme.warning, fontSize: AppTypography.labelLarge, fontWeight: FontWeight.w500, ), @@ -299,16 +235,22 @@ class ChatOfflineOverlay extends ConsumerWidget { return const SizedBox.shrink(); } + final theme = context.conduitTheme; + final surfaceColor = theme.warningBackground; + final borderAlpha = Theme.of(context).brightness == Brightness.dark + ? 0.5 + : 0.35; + return Container( padding: const EdgeInsets.symmetric( horizontal: Spacing.md, vertical: Spacing.sm, ), decoration: BoxDecoration( - color: context.conduitTheme.warning.withValues(alpha: 0.2), + color: surfaceColor, border: Border( top: BorderSide( - color: context.conduitTheme.warning.withValues(alpha: 0.5), + color: theme.warning.withValues(alpha: borderAlpha), width: BorderWidth.regular, ), ), @@ -318,14 +260,14 @@ class ChatOfflineOverlay extends ConsumerWidget { children: [ Icon( Platform.isIOS ? CupertinoIcons.wifi_slash : Icons.wifi_off, - color: context.conduitTheme.warning, + color: theme.warning, size: Spacing.md, ), const SizedBox(width: Spacing.sm), Text( AppLocalizations.of(context)!.messagesWillSendWhenOnline, style: TextStyle( - color: context.conduitTheme.warning, + color: theme.warning, fontSize: AppTypography.bodySmall, fontWeight: FontWeight.w500, ),