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:
cogwheel0
2025-10-01 23:26:12 +05:30
parent d899ca5f70
commit ebe6cec17c
16 changed files with 523 additions and 148 deletions

View 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;
}
}
}

View File

@@ -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,

View File

@@ -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,
),