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 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../providers/app_providers.dart';
|
import '../providers/app_providers.dart';
|
||||||
|
import '../services/connectivity_service.dart';
|
||||||
import '../services/navigation_service.dart';
|
import '../services/navigation_service.dart';
|
||||||
import '../utils/debug_logger.dart';
|
import '../utils/debug_logger.dart';
|
||||||
import '../../features/auth/providers/unified_auth_providers.dart';
|
import '../../features/auth/providers/unified_auth_providers.dart';
|
||||||
import '../../features/auth/views/authentication_page.dart';
|
import '../../features/auth/views/authentication_page.dart';
|
||||||
import '../../features/auth/views/connect_signin_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/auth/views/server_connection_page.dart';
|
||||||
import '../../features/chat/views/chat_page.dart';
|
import '../../features/chat/views/chat_page.dart';
|
||||||
import '../../features/navigation/views/splash_launcher_page.dart';
|
import '../../features/navigation/views/splash_launcher_page.dart';
|
||||||
@@ -67,19 +69,34 @@ class RouterNotifier extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (activeServerAsync.hasError) {
|
if (activeServerAsync.hasError) {
|
||||||
return location == Routes.serverConnection
|
return location == Routes.connectionIssue ? null : Routes.connectionIssue;
|
||||||
? null
|
|
||||||
: Routes.serverConnection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final activeServer = activeServerAsync.asData?.value;
|
final activeServer = activeServerAsync.asData?.value;
|
||||||
if (activeServer == null) {
|
final hasActiveServer = activeServer != null;
|
||||||
|
if (!hasActiveServer) {
|
||||||
// Allow auth-related routes while no server configured
|
// Allow auth-related routes while no server configured
|
||||||
if (_isAuthLocation(location)) return null;
|
if (_isAuthLocation(location)) return null;
|
||||||
return Routes.serverConnection;
|
return Routes.serverConnection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (location == Routes.serverConnection) {
|
||||||
|
return Routes.chat;
|
||||||
|
}
|
||||||
|
|
||||||
final authState = ref.read(authNavigationStateProvider);
|
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) {
|
switch (authState) {
|
||||||
case AuthNavigationState.loading:
|
case AuthNavigationState.loading:
|
||||||
// Keep user on auth routes while loading to prevent bounce
|
// Keep user on auth routes while loading to prevent bounce
|
||||||
@@ -87,12 +104,16 @@ class RouterNotifier extends ChangeNotifier {
|
|||||||
// Otherwise keep splash during session establishment
|
// Otherwise keep splash during session establishment
|
||||||
return location == Routes.splash ? null : Routes.splash;
|
return location == Routes.splash ? null : Routes.splash;
|
||||||
case AuthNavigationState.needsLogin:
|
case AuthNavigationState.needsLogin:
|
||||||
|
if (location == Routes.connectionIssue) return null;
|
||||||
|
return null;
|
||||||
case AuthNavigationState.error:
|
case AuthNavigationState.error:
|
||||||
if (_isAuthLocation(location)) return null;
|
if (location == Routes.connectionIssue) return null;
|
||||||
return Routes.serverConnection;
|
return null;
|
||||||
case AuthNavigationState.authenticated:
|
case AuthNavigationState.authenticated:
|
||||||
// Avoid unnecessary redirects if already on a non-auth route
|
// 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 Routes.chat;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -102,7 +123,8 @@ class RouterNotifier extends ChangeNotifier {
|
|||||||
bool _isAuthLocation(String location) {
|
bool _isAuthLocation(String location) {
|
||||||
return location == Routes.serverConnection ||
|
return location == Routes.serverConnection ||
|
||||||
location == Routes.login ||
|
location == Routes.login ||
|
||||||
location == Routes.authentication;
|
location == Routes.authentication ||
|
||||||
|
location == Routes.connectionIssue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -145,6 +167,11 @@ final goRouterProvider = Provider<GoRouter>((ref) {
|
|||||||
name: RouteNames.serverConnection,
|
name: RouteNames.serverConnection,
|
||||||
builder: (context, state) => const ServerConnectionPage(),
|
builder: (context, state) => const ServerConnectionPage(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: Routes.connectionIssue,
|
||||||
|
name: RouteNames.connectionIssue,
|
||||||
|
builder: (context, state) => const ConnectionIssuePage(),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: Routes.authentication,
|
path: Routes.authentication,
|
||||||
name: RouteNames.authentication,
|
name: RouteNames.authentication,
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ class Routes {
|
|||||||
static const String chat = '/chat';
|
static const String chat = '/chat';
|
||||||
static const String login = '/login';
|
static const String login = '/login';
|
||||||
static const String serverConnection = '/server-connection';
|
static const String serverConnection = '/server-connection';
|
||||||
|
static const String connectionIssue = '/connection-issue';
|
||||||
static const String authentication = '/authentication';
|
static const String authentication = '/authentication';
|
||||||
static const String profile = '/profile';
|
static const String profile = '/profile';
|
||||||
static const String appCustomization = '/profile/customization';
|
static const String appCustomization = '/profile/customization';
|
||||||
@@ -106,6 +107,7 @@ class RouteNames {
|
|||||||
static const String chat = 'chat';
|
static const String chat = 'chat';
|
||||||
static const String login = 'login';
|
static const String login = 'login';
|
||||||
static const String serverConnection = 'server-connection';
|
static const String serverConnection = 'server-connection';
|
||||||
|
static const String connectionIssue = 'connection-issue';
|
||||||
static const String authentication = 'authentication';
|
static const String authentication = 'authentication';
|
||||||
static const String profile = 'profile';
|
static const String profile = 'profile';
|
||||||
static const String appCustomization = 'app-customization';
|
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,
|
backgroundColor: context.conduitTheme.surfaceBackground,
|
||||||
// Left navigation drawer with draggable edge open (native, finger-following)
|
// Left navigation drawer with draggable edge open (native, finger-following)
|
||||||
drawerEnableOpenDragGesture: true,
|
drawerEnableOpenDragGesture: true,
|
||||||
drawerDragStartBehavior: DragStartBehavior.down,
|
drawerDragStartBehavior: DragStartBehavior.start,
|
||||||
drawerEdgeDragWidth: MediaQuery.of(context).size.width * 0.5,
|
drawerEdgeDragWidth: MediaQuery.of(context).size.width * 0.75,
|
||||||
drawerScrimColor: Colors.black.withValues(alpha: 0.32),
|
drawerScrimColor: Colors.black.withValues(alpha: 0.32),
|
||||||
drawer: Drawer(
|
drawer: Drawer(
|
||||||
width: (MediaQuery.of(context).size.width * 0.80).clamp(
|
width: (MediaQuery.of(context).size.width * 0.80).clamp(
|
||||||
@@ -1167,7 +1167,13 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
420.0,
|
420.0,
|
||||||
),
|
),
|
||||||
backgroundColor: context.conduitTheme.surfaceBackground,
|
backgroundColor: context.conduitTheme.surfaceBackground,
|
||||||
child: const SafeArea(child: ChatsDrawer()),
|
child: SafeArea(
|
||||||
|
top: true,
|
||||||
|
bottom: true,
|
||||||
|
left: false,
|
||||||
|
right: false,
|
||||||
|
child: const ChatsDrawer(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: context.conduitTheme.surfaceBackground,
|
backgroundColor: context.conduitTheme.surfaceBackground,
|
||||||
|
|||||||
@@ -149,8 +149,8 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
Spacing.md,
|
Spacing.inputPadding,
|
||||||
0,
|
Spacing.sm,
|
||||||
Spacing.md,
|
Spacing.md,
|
||||||
Spacing.sm,
|
Spacing.sm,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,6 +8,18 @@
|
|||||||
"loadingProfile": "Profil wird geladen...",
|
"loadingProfile": "Profil wird geladen...",
|
||||||
"unableToLoadProfile": "Profil konnte nicht geladen werden",
|
"unableToLoadProfile": "Profil konnte nicht geladen werden",
|
||||||
"pleaseCheckConnection": "Bitte überprüfe deine Verbindung und versuche es erneut",
|
"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",
|
"account": "Konto",
|
||||||
"signOut": "Abmelden",
|
"signOut": "Abmelden",
|
||||||
"endYourSession": "Sitzung beenden",
|
"endYourSession": "Sitzung beenden",
|
||||||
@@ -129,7 +141,6 @@
|
|||||||
"invalidImageFormat": "Ungültiges Bildformat",
|
"invalidImageFormat": "Ungültiges Bildformat",
|
||||||
"emptyImageData": "Leere Bilddaten"
|
"emptyImageData": "Leere Bilddaten"
|
||||||
,
|
,
|
||||||
"offlineBanner": "Du bist offline. Einige Funktionen sind eingeschränkt.",
|
|
||||||
"featureRequiresInternet": "Diese Funktion erfordert eine Internetverbindung",
|
"featureRequiresInternet": "Diese Funktion erfordert eine Internetverbindung",
|
||||||
"messagesWillSendWhenOnline": "Nachrichten werden gesendet, sobald du wieder online bist",
|
"messagesWillSendWhenOnline": "Nachrichten werden gesendet, sobald du wieder online bist",
|
||||||
"confirm": "Bestätigen",
|
"confirm": "Bestätigen",
|
||||||
|
|||||||
@@ -15,6 +15,18 @@
|
|||||||
"unableToLoadProfile": "Unable to load profile",
|
"unableToLoadProfile": "Unable to load profile",
|
||||||
"@unableToLoadProfile": {"description": "Error title shown when profile request fails."},
|
"@unableToLoadProfile": {"description": "Error title shown when profile request fails."},
|
||||||
"pleaseCheckConnection": "Please check your connection and try again",
|
"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."},
|
"@pleaseCheckConnection": {"description": "Generic connectivity hint after an error."},
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"@account": {"description": "Section header for account-related options."},
|
"@account": {"description": "Section header for account-related options."},
|
||||||
@@ -282,8 +294,6 @@
|
|||||||
"emptyImageData": "Empty image data",
|
"emptyImageData": "Empty image data",
|
||||||
"@emptyImageData": {"description": "Error when image data buffer is empty."}
|
"@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": "This feature requires an internet connection",
|
||||||
"@featureRequiresInternet": {"description": "Informational text explaining internet requirement."},
|
"@featureRequiresInternet": {"description": "Informational text explaining internet requirement."},
|
||||||
"messagesWillSendWhenOnline": "Messages will be sent when you're back online",
|
"messagesWillSendWhenOnline": "Messages will be sent when you're back online",
|
||||||
|
|||||||
@@ -8,6 +8,18 @@
|
|||||||
"loadingProfile": "Chargement du profil...",
|
"loadingProfile": "Chargement du profil...",
|
||||||
"unableToLoadProfile": "Impossible de charger le profil",
|
"unableToLoadProfile": "Impossible de charger le profil",
|
||||||
"pleaseCheckConnection": "Veuillez vérifier votre connexion et réessayer",
|
"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",
|
"account": "Compte",
|
||||||
"signOut": "Se déconnecter",
|
"signOut": "Se déconnecter",
|
||||||
"endYourSession": "Terminer votre session",
|
"endYourSession": "Terminer votre session",
|
||||||
@@ -129,7 +141,6 @@
|
|||||||
"invalidImageFormat": "Format d'image invalide",
|
"invalidImageFormat": "Format d'image invalide",
|
||||||
"emptyImageData": "Données d'image vides"
|
"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",
|
"featureRequiresInternet": "Cette fonctionnalité nécessite une connexion Internet",
|
||||||
"messagesWillSendWhenOnline": "Les messages seront envoyés lorsque vous serez de nouveau en ligne",
|
"messagesWillSendWhenOnline": "Les messages seront envoyés lorsque vous serez de nouveau en ligne",
|
||||||
"confirm": "Confirmer",
|
"confirm": "Confirmer",
|
||||||
|
|||||||
@@ -8,6 +8,18 @@
|
|||||||
"loadingProfile": "Caricamento profilo...",
|
"loadingProfile": "Caricamento profilo...",
|
||||||
"unableToLoadProfile": "Impossibile caricare il profilo",
|
"unableToLoadProfile": "Impossibile caricare il profilo",
|
||||||
"pleaseCheckConnection": "Controlla la connessione e riprova",
|
"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",
|
"account": "Account",
|
||||||
"signOut": "Esci",
|
"signOut": "Esci",
|
||||||
"endYourSession": "Termina la sessione",
|
"endYourSession": "Termina la sessione",
|
||||||
@@ -129,7 +141,6 @@
|
|||||||
"invalidImageFormat": "Formato immagine non valido",
|
"invalidImageFormat": "Formato immagine non valido",
|
||||||
"emptyImageData": "Dati immagine vuoti"
|
"emptyImageData": "Dati immagine vuoti"
|
||||||
,
|
,
|
||||||
"offlineBanner": "Sei offline. Alcune funzioni potrebbero essere limitate.",
|
|
||||||
"featureRequiresInternet": "Questa funzione richiede una connessione Internet",
|
"featureRequiresInternet": "Questa funzione richiede una connessione Internet",
|
||||||
"messagesWillSendWhenOnline": "I messaggi verranno inviati quando tornerai online",
|
"messagesWillSendWhenOnline": "I messaggi verranno inviati quando tornerai online",
|
||||||
"confirm": "Conferma",
|
"confirm": "Conferma",
|
||||||
|
|||||||
@@ -150,6 +150,24 @@ abstract class AppLocalizations {
|
|||||||
/// **'Please check your connection and try again'**
|
/// **'Please check your connection and try again'**
|
||||||
String get pleaseCheckConnection;
|
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.
|
/// Section header for account-related options.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
@@ -864,12 +882,6 @@ abstract class AppLocalizations {
|
|||||||
/// **'Empty image data'**
|
/// **'Empty image data'**
|
||||||
String get emptyImageData;
|
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.
|
/// Informational text explaining internet requirement.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get pleaseCheckConnection =>
|
String get pleaseCheckConnection =>
|
||||||
'Bitte überprüfe deine Verbindung und versuche es erneut';
|
'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
|
@override
|
||||||
String get account => 'Konto';
|
String get account => 'Konto';
|
||||||
|
|
||||||
@@ -434,10 +445,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get emptyImageData => 'Leere Bilddaten';
|
String get emptyImageData => 'Leere Bilddaten';
|
||||||
|
|
||||||
@override
|
|
||||||
String get offlineBanner =>
|
|
||||||
'Du bist offline. Einige Funktionen sind eingeschränkt.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get featureRequiresInternet =>
|
String get featureRequiresInternet =>
|
||||||
'Diese Funktion erfordert eine Internetverbindung';
|
'Diese Funktion erfordert eine Internetverbindung';
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get pleaseCheckConnection =>
|
String get pleaseCheckConnection =>
|
||||||
'Please check your connection and try again';
|
'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
|
@override
|
||||||
String get account => 'Account';
|
String get account => 'Account';
|
||||||
|
|
||||||
@@ -430,9 +441,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get emptyImageData => 'Empty image data';
|
String get emptyImageData => 'Empty image data';
|
||||||
|
|
||||||
@override
|
|
||||||
String get offlineBanner => 'You\'re offline. Some features may be limited.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get featureRequiresInternet =>
|
String get featureRequiresInternet =>
|
||||||
'This feature requires an internet connection';
|
'This feature requires an internet connection';
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get pleaseCheckConnection =>
|
String get pleaseCheckConnection =>
|
||||||
'Veuillez vérifier votre connexion et réessayer';
|
'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
|
@override
|
||||||
String get account => 'Compte';
|
String get account => 'Compte';
|
||||||
|
|
||||||
@@ -439,10 +450,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get emptyImageData => 'Données d\'image vides';
|
String get emptyImageData => 'Données d\'image vides';
|
||||||
|
|
||||||
@override
|
|
||||||
String get offlineBanner =>
|
|
||||||
'Vous êtes hors ligne. Certaines fonctions peuvent être limitées.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get featureRequiresInternet =>
|
String get featureRequiresInternet =>
|
||||||
'Cette fonctionnalité nécessite une connexion Internet';
|
'Cette fonctionnalité nécessite une connexion Internet';
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get pleaseCheckConnection => 'Controlla la connessione e riprova';
|
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
|
@override
|
||||||
String get account => 'Account';
|
String get account => 'Account';
|
||||||
|
|
||||||
@@ -431,10 +442,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get emptyImageData => 'Dati immagine vuoti';
|
String get emptyImageData => 'Dati immagine vuoti';
|
||||||
|
|
||||||
@override
|
|
||||||
String get offlineBanner =>
|
|
||||||
'Sei offline. Alcune funzioni potrebbero essere limitate.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get featureRequiresInternet =>
|
String get featureRequiresInternet =>
|
||||||
'Questa funzione richiede una connessione Internet';
|
'Questa funzione richiede una connessione Internet';
|
||||||
|
|||||||
@@ -535,13 +535,13 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
|
|||||||
buttonDisabledText: AppTheme.neutral400,
|
buttonDisabledText: AppTheme.neutral400,
|
||||||
|
|
||||||
// Status and feedback colors - Enhanced visibility
|
// Status and feedback colors - Enhanced visibility
|
||||||
success: Color(0xFF22C55E),
|
success: Color(0xFF34D399),
|
||||||
successBackground: Color(0xFF14532D),
|
successBackground: Color(0xFF14532D),
|
||||||
error: Color(0xFFEF4444),
|
error: Color(0xFFFCA5A5),
|
||||||
errorBackground: Color(0xFF7F1D1D),
|
errorBackground: Color(0xFF7F1D1D),
|
||||||
warning: Color(0xFFF59E0B),
|
warning: Color(0xFFFBBF24),
|
||||||
warningBackground: Color(0xFF7C2D12),
|
warningBackground: Color(0xFF451A03),
|
||||||
info: Color(0xFF38BDF8),
|
info: Color(0xFF93C5FD),
|
||||||
infoBackground: Color(0xFF0C4A6E),
|
infoBackground: Color(0xFF0C4A6E),
|
||||||
|
|
||||||
// Navigation and UI element colors - Enhanced contrast
|
// Navigation and UI element colors - Enhanced contrast
|
||||||
@@ -549,7 +549,7 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
|
|||||||
navigationBackground: Color(0xFF0A0D0C),
|
navigationBackground: Color(0xFF0A0D0C),
|
||||||
navigationSelected: AppTheme.brandPrimary,
|
navigationSelected: AppTheme.brandPrimary,
|
||||||
navigationUnselected: AppTheme.neutral300,
|
navigationUnselected: AppTheme.neutral300,
|
||||||
navigationSelectedBackground: AppTheme.brandPrimary,
|
navigationSelectedBackground: Color(0xFF312E81),
|
||||||
|
|
||||||
// Loading and animation colors - Enhanced visibility
|
// Loading and animation colors - Enhanced visibility
|
||||||
shimmerBase: Color(0xFF121514),
|
shimmerBase: Color(0xFF121514),
|
||||||
@@ -660,21 +660,21 @@ class ConduitThemeExtension extends ThemeExtension<ConduitThemeExtension> {
|
|||||||
buttonDisabledText: AppTheme.neutral500,
|
buttonDisabledText: AppTheme.neutral500,
|
||||||
|
|
||||||
// Status and feedback colors - Enhanced visibility
|
// Status and feedback colors - Enhanced visibility
|
||||||
success: Color(0xFF16A34A),
|
success: Color(0xFF166534),
|
||||||
successBackground: Color(0xFFEFFBF3),
|
successBackground: Color(0xFFECFDF3),
|
||||||
error: Color(0xFFDC2626),
|
error: Color(0xFFB91C1C),
|
||||||
errorBackground: Color(0xFFFDECEC),
|
errorBackground: Color(0xFFFEE2E2),
|
||||||
warning: Color(0xFFD97706),
|
warning: Color(0xFF92400E),
|
||||||
warningBackground: Color(0xFFFEF6E7),
|
warningBackground: Color(0xFFFEF3C7),
|
||||||
info: Color(0xFF0284C7),
|
info: Color(0xFF1D4ED8),
|
||||||
infoBackground: Color(0xFFE8F4FD),
|
infoBackground: Color(0xFFDBEAFE),
|
||||||
|
|
||||||
// Navigation and UI element colors - Enhanced contrast
|
// Navigation and UI element colors - Enhanced contrast
|
||||||
dividerColor: AppTheme.neutral100,
|
dividerColor: AppTheme.neutral100,
|
||||||
navigationBackground: AppTheme.neutral50,
|
navigationBackground: AppTheme.neutral50,
|
||||||
navigationSelected: AppTheme.brandPrimary,
|
navigationSelected: AppTheme.brandPrimary,
|
||||||
navigationUnselected: AppTheme.neutral600,
|
navigationUnselected: AppTheme.neutral600,
|
||||||
navigationSelectedBackground: AppTheme.brandPrimary,
|
navigationSelectedBackground: Color(0xFFE0E7FF),
|
||||||
|
|
||||||
// Loading and animation colors - Enhanced visibility
|
// Loading and animation colors - Enhanced visibility
|
||||||
shimmerBase: Color(0xFFF3F4F6),
|
shimmerBase: Color(0xFFF3F4F6),
|
||||||
|
|||||||
@@ -15,13 +15,8 @@ part 'offline_indicator.g.dart';
|
|||||||
|
|
||||||
class OfflineIndicator extends ConsumerWidget {
|
class OfflineIndicator extends ConsumerWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final bool showBanner;
|
|
||||||
|
|
||||||
const OfflineIndicator({
|
const OfflineIndicator({super.key, required this.child});
|
||||||
super.key,
|
|
||||||
required this.child,
|
|
||||||
this.showBanner = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -33,26 +28,22 @@ class OfflineIndicator extends ConsumerWidget {
|
|||||||
orElse: () => false,
|
orElse: () => false,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Stack(
|
final overlay = connectivityStatus.when(
|
||||||
children: [
|
|
||||||
child,
|
|
||||||
if (showBanner)
|
|
||||||
connectivityStatus.when(
|
|
||||||
data: (status) {
|
data: (status) {
|
||||||
if (status == ConnectivityStatus.offline || socketOffline) {
|
if ((status == ConnectivityStatus.offline || socketOffline) &&
|
||||||
return _OfflineBanner();
|
!wasOffline) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
// Announce back-online briefly if we were previously offline
|
|
||||||
if (wasOffline) {
|
if (wasOffline) {
|
||||||
return const _BackOnlineToast();
|
return const _BackOnlineToast();
|
||||||
}
|
}
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
loading: () => 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
|
// Inline offline indicator for specific features
|
||||||
class InlineOfflineIndicator extends ConsumerWidget {
|
class InlineOfflineIndicator extends ConsumerWidget {
|
||||||
final String message;
|
final String message;
|
||||||
@@ -217,16 +149,20 @@ class InlineOfflineIndicator extends ConsumerWidget {
|
|||||||
return const SizedBox.shrink();
|
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(
|
return Container(
|
||||||
margin: const EdgeInsets.all(Spacing.md),
|
margin: const EdgeInsets.all(Spacing.md),
|
||||||
padding: const EdgeInsets.all(Spacing.md),
|
padding: const EdgeInsets.all(Spacing.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color:
|
color: surfaceColor,
|
||||||
backgroundColor ??
|
|
||||||
context.conduitTheme.warning.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: context.conduitTheme.warning.withValues(alpha: 0.3),
|
color: theme.warning.withValues(alpha: borderAlpha),
|
||||||
width: BorderWidth.regular,
|
width: BorderWidth.regular,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -235,7 +171,7 @@ class InlineOfflineIndicator extends ConsumerWidget {
|
|||||||
Icon(
|
Icon(
|
||||||
icon ??
|
icon ??
|
||||||
(Platform.isIOS ? CupertinoIcons.wifi_slash : Icons.wifi_off),
|
(Platform.isIOS ? CupertinoIcons.wifi_slash : Icons.wifi_off),
|
||||||
color: context.conduitTheme.warning,
|
color: theme.warning,
|
||||||
size: Spacing.lg,
|
size: Spacing.lg,
|
||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.xs),
|
const SizedBox(width: Spacing.xs),
|
||||||
@@ -245,7 +181,7 @@ class InlineOfflineIndicator extends ConsumerWidget {
|
|||||||
? message
|
? message
|
||||||
: AppLocalizations.of(context)!.featureRequiresInternet,
|
: AppLocalizations.of(context)!.featureRequiresInternet,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: context.conduitTheme.warning,
|
color: theme.warning,
|
||||||
fontSize: AppTypography.labelLarge,
|
fontSize: AppTypography.labelLarge,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
@@ -299,16 +235,22 @@ class ChatOfflineOverlay extends ConsumerWidget {
|
|||||||
return const SizedBox.shrink();
|
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(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: Spacing.md,
|
horizontal: Spacing.md,
|
||||||
vertical: Spacing.sm,
|
vertical: Spacing.sm,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.conduitTheme.warning.withValues(alpha: 0.2),
|
color: surfaceColor,
|
||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(
|
top: BorderSide(
|
||||||
color: context.conduitTheme.warning.withValues(alpha: 0.5),
|
color: theme.warning.withValues(alpha: borderAlpha),
|
||||||
width: BorderWidth.regular,
|
width: BorderWidth.regular,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -318,14 +260,14 @@ class ChatOfflineOverlay extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Platform.isIOS ? CupertinoIcons.wifi_slash : Icons.wifi_off,
|
Platform.isIOS ? CupertinoIcons.wifi_slash : Icons.wifi_off,
|
||||||
color: context.conduitTheme.warning,
|
color: theme.warning,
|
||||||
size: Spacing.md,
|
size: Spacing.md,
|
||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.sm),
|
const SizedBox(width: Spacing.sm),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context)!.messagesWillSendWhenOnline,
|
AppLocalizations.of(context)!.messagesWillSendWhenOnline,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: context.conduitTheme.warning,
|
color: theme.warning,
|
||||||
fontSize: AppTypography.bodySmall,
|
fontSize: AppTypography.bodySmall,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user