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.
This commit is contained in:
@@ -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<GoRouter>((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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
314
lib/features/auth/views/connection_issue_page.dart
Normal file
314
lib/features/auth/views/connection_issue_page.dart
Normal file
@@ -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<ConnectionIssuePage> createState() =>
|
||||
_ConnectionIssuePageState();
|
||||
}
|
||||
|
||||
class _ConnectionIssuePageState extends ConsumerState<ConnectionIssuePage> {
|
||||
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<void> _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<void> _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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1158,8 +1158,8 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
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<ChatPage> {
|
||||
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,
|
||||
|
||||
@@ -149,8 +149,8 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
Spacing.md,
|
||||
0,
|
||||
Spacing.inputPadding,
|
||||
Spacing.sm,
|
||||
Spacing.md,
|
||||
Spacing.sm,
|
||||
),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -535,13 +535,13 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
|
||||
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<ConduitThemeExtension> {
|
||||
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<ConduitThemeExtension> {
|
||||
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),
|
||||
|
||||
@@ -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(
|
||||
final overlay = connectivityStatus.when(
|
||||
data: (status) {
|
||||
if (status == ConnectivityStatus.offline || socketOffline) {
|
||||
return _OfflineBanner();
|
||||
if ((status == ConnectivityStatus.offline || socketOffline) &&
|
||||
!wasOffline) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
// Announce back-online briefly if we were previously offline
|
||||
if (wasOffline) {
|
||||
return const _BackOnlineToast();
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (_, _) => _OfflineBanner(),
|
||||
),
|
||||
],
|
||||
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,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user