refactor: migrate to go_router navigation

This commit is contained in:
cogwheel0
2025-09-22 14:36:43 +05:30
parent 462bf4cde2
commit 66a28958ed
11 changed files with 468 additions and 410 deletions

View File

@@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/services.dart';
import 'package:uuid/uuid.dart';
import 'package:conduit/l10n/app_localizations.dart';
@@ -12,12 +13,11 @@ import '../../../core/models/server_config.dart';
import '../../../core/providers/app_providers.dart';
import '../../../core/services/api_service.dart';
import '../../../core/services/input_validation_service.dart';
import '../../../core/services/navigation_service.dart';
import '../../../core/widgets/error_boundary.dart';
import '../../../shared/services/brand_service.dart';
import '../../../shared/theme/theme_extensions.dart';
import '../../../shared/widgets/conduit_components.dart';
import '../../chat/views/chat_page.dart';
import 'authentication_page.dart';
class ServerConnectionPage extends ConsumerStatefulWidget {
const ServerConnectionPage({super.key});
@@ -33,7 +33,7 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
final Map<String, String> _customHeaders = {};
final TextEditingController _headerKeyController = TextEditingController();
final TextEditingController _headerValueController = TextEditingController();
String? _connectionError;
bool _isConnecting = false;
bool _showAdvancedSettings = false;
@@ -85,14 +85,10 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
}
await _saveServerConfig(tempConfig);
// Navigate to authentication page
if (mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => AuthenticationPage(serverConfig: tempConfig),
),
);
context.pushNamed(RouteNames.authentication, extra: tempConfig);
}
} catch (e) {
setState(() {
@@ -122,12 +118,12 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
// Clean up the input
String url = input.trim();
// Add protocol if missing
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'http://$url';
}
// Remove trailing slash
if (url.endsWith('/')) {
url = url.substring(0, url.length - 1);
@@ -171,7 +167,7 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
bool _isValidIPAddress(String ip) {
final parts = ip.split('.');
if (parts.length != 4) return false;
for (final part in parts) {
final num = int.tryParse(part);
if (num == null || num < 0 || num > 255) return false;
@@ -190,7 +186,7 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
String _formatConnectionError(String error) {
// Clean up the error message
String cleanError = error.replaceFirst('Exception: ', '');
// Handle specific error types
if (error.contains('SocketException')) {
return AppLocalizations.of(context)!.weCouldntReachServer;
@@ -208,10 +204,12 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
return cleanError;
} else if (error.contains('Invalid IP address format')) {
return cleanError;
} else if (error.contains('This does not appear to be an Open-WebUI server')) {
} else if (error.contains(
'This does not appear to be an Open-WebUI server',
)) {
return AppLocalizations.of(context)!.serverNotOpenWebUI;
}
return AppLocalizations.of(context)!.couldNotConnectGeneric;
}
@@ -232,9 +230,9 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
children: [
// Header with progress indicator
_buildHeader(),
const SizedBox(height: Spacing.extraLarge),
// Main content
Expanded(
child: SingleChildScrollView(
@@ -269,7 +267,7 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
),
),
),
// Bottom action button
_buildConnectButton(),
],
@@ -340,8 +338,12 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
context.conduitTheme.buttonPrimary.withValues(alpha: 0.12),
context.conduitTheme.buttonPrimary.withValues(alpha: 0.06),
context.conduitTheme.buttonPrimary.withValues(
alpha: 0.12,
),
context.conduitTheme.buttonPrimary.withValues(
alpha: 0.06,
),
Colors.transparent,
],
stops: const [0.0, 0.7, 1.0],
@@ -361,7 +363,9 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
bottom: 0,
child: ConduitBadge(
text: AppLocalizations.of(context)!.demoBadge,
backgroundColor: context.conduitTheme.warning.withValues(alpha: 0.15),
backgroundColor: context.conduitTheme.warning.withValues(
alpha: 0.15,
),
textColor: context.conduitTheme.warning,
isCompact: true,
),
@@ -448,11 +452,7 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
text: AppLocalizations.of(context)!.enterDemo,
icon: Platform.isIOS ? CupertinoIcons.play_fill : Icons.play_arrow,
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const ChatPage(),
),
);
context.go(Routes.chat);
},
isSecondary: true,
isFullWidth: true,
@@ -475,7 +475,8 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
controller: _urlController,
validator: InputValidationService.combine([
InputValidationService.validateRequired,
(value) => InputValidationService.validateUrl(value, required: true),
(value) =>
InputValidationService.validateUrl(value, required: true),
]),
keyboardType: TextInputType.url,
semanticLabel: AppLocalizations.of(context)!.enterServerUrlSemantic,
@@ -492,7 +493,7 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
delay: AnimationDuration.microInteraction,
curve: Curves.easeOutCubic,
),
if (_connectionError != null) ...[
const SizedBox(height: Spacing.md),
_buildErrorMessage(_connectionError!),
@@ -516,7 +517,9 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
child: Column(
children: [
InkWell(
onTap: () => setState(() => _showAdvancedSettings = !_showAdvancedSettings),
onTap: () => setState(
() => _showAdvancedSettings = !_showAdvancedSettings,
),
borderRadius: BorderRadius.circular(AppBorderRadius.button),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: Spacing.sm),
@@ -552,7 +555,9 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
duration: AnimationDuration.microInteraction,
turns: _showAdvancedSettings ? 0.5 : 0,
child: Icon(
Platform.isIOS ? CupertinoIcons.chevron_down : Icons.expand_more,
Platform.isIOS
? CupertinoIcons.chevron_down
: Icons.expand_more,
color: context.conduitTheme.iconSecondary,
),
),
@@ -563,7 +568,9 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
AnimatedSize(
duration: AnimationDuration.microInteraction,
curve: Curves.easeInOutCubic,
child: _showAdvancedSettings ? _buildAdvancedSettingsContent() : const SizedBox.shrink(),
child: _showAdvancedSettings
? _buildAdvancedSettingsContent()
: const SizedBox.shrink(),
),
],
),
@@ -591,8 +598,8 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
Text(
'${_customHeaders.length}/10',
style: context.conduitTheme.bodySmall?.copyWith(
color: _customHeaders.length >= 10
? context.conduitTheme.error
color: _customHeaders.length >= 10
? context.conduitTheme.error
: context.conduitTheme.textSecondary,
),
),
@@ -638,14 +645,14 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
ConduitIconButton(
icon: Platform.isIOS ? CupertinoIcons.plus : Icons.add,
onPressed: _customHeaders.length >= 10 ? null : _addCustomHeader,
tooltip: _customHeaders.length >= 10
? AppLocalizations.of(context)!.maximumHeadersReached
tooltip: _customHeaders.length >= 10
? AppLocalizations.of(context)!.maximumHeadersReached
: AppLocalizations.of(context)!.addHeader,
backgroundColor: _customHeaders.length >= 10
? context.conduitTheme.surfaceContainer
backgroundColor: _customHeaders.length >= 10
? context.conduitTheme.surfaceContainer
: context.conduitTheme.buttonPrimary,
iconColor: _customHeaders.length >= 10
? context.conduitTheme.textDisabled
iconColor: _customHeaders.length >= 10
? context.conduitTheme.textDisabled
: context.conduitTheme.buttonPrimaryText,
),
],
@@ -679,8 +686,8 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
fit: FlexFit.loose,
child: ConduitBadge(
text: entry.key,
backgroundColor:
context.conduitTheme.buttonPrimary.withValues(alpha: 0.1),
backgroundColor: context.conduitTheme.buttonPrimary
.withValues(alpha: 0.1),
textColor: context.conduitTheme.buttonPrimary,
isCompact: true,
maxLines: 1,
@@ -704,7 +711,9 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close,
onPressed: () => _removeCustomHeader(entry.key),
tooltip: AppLocalizations.of(context)!.removeHeader,
backgroundColor: context.conduitTheme.error.withValues(alpha: 0.1),
backgroundColor: context.conduitTheme.error.withValues(
alpha: 0.1,
),
iconColor: context.conduitTheme.error,
isCompact: true,
),
@@ -718,20 +727,23 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
Widget _buildConnectButton() {
return Padding(
padding: const EdgeInsets.only(top: Spacing.lg),
child: ConduitButton(
text: _isConnecting
? AppLocalizations.of(context)!.connecting
: AppLocalizations.of(context)!.connectToServerButton,
icon: _isConnecting
? null
: (Platform.isIOS ? CupertinoIcons.arrow_right : Icons.arrow_forward),
onPressed: _isConnecting ? null : _connectToServer,
isLoading: _isConnecting,
isFullWidth: true,
).animate().fadeIn(
duration: AnimationDuration.pageTransition,
delay: AnimationDuration.fast,
),
child:
ConduitButton(
text: _isConnecting
? AppLocalizations.of(context)!.connecting
: AppLocalizations.of(context)!.connectToServerButton,
icon: _isConnecting
? null
: (Platform.isIOS
? CupertinoIcons.arrow_right
: Icons.arrow_forward),
onPressed: _isConnecting ? null : _connectToServer,
isLoading: _isConnecting,
isFullWidth: true,
).animate().fadeIn(
duration: AnimationDuration.pageTransition,
delay: AnimationDuration.fast,
),
);
}
@@ -749,8 +761,8 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
child: Row(
children: [
Icon(
Platform.isIOS
? CupertinoIcons.exclamationmark_circle_fill
Platform.isIOS
? CupertinoIcons.exclamationmark_circle_fill
: Icons.error_outline,
color: context.conduitTheme.error,
size: IconSize.medium,
@@ -776,35 +788,35 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
void _addCustomHeader() {
final key = _headerKeyController.text.trim();
final value = _headerValueController.text.trim();
if (key.isEmpty || value.isEmpty) return;
// Validate header name
final keyValidation = _validateHeaderKey(key);
if (keyValidation != null) {
_showHeaderError(keyValidation);
return;
}
// Validate header value
final valueValidation = _validateHeaderValue(value);
if (valueValidation != null) {
_showHeaderError(valueValidation);
return;
}
// Check for duplicates
if (_customHeaders.containsKey(key)) {
_showHeaderError(AppLocalizations.of(context)!.headerAlreadyExists(key));
return;
}
// Check header count limit
if (_customHeaders.length >= 10) {
_showHeaderError(AppLocalizations.of(context)!.maxHeadersReachedDetail);
return;
}
setState(() {
_customHeaders[key] = value;
_headerKeyController.clear();
@@ -817,31 +829,42 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
// RFC 7230 compliant header name validation
if (key.isEmpty) return AppLocalizations.of(context)!.headerNameEmpty;
if (key.length > 64) return AppLocalizations.of(context)!.headerNameTooLong;
// Check for valid characters (RFC 7230: token characters)
if (!RegExp(r'^[a-zA-Z0-9!#$&\-^_`|~]+$').hasMatch(key)) {
return AppLocalizations.of(context)!.headerNameInvalidChars;
}
// Check for reserved headers that should not be overridden
final lowerKey = key.toLowerCase();
final reservedHeaders = {
'authorization', 'content-type', 'content-length', 'host',
'user-agent', 'accept', 'accept-encoding', 'connection',
'transfer-encoding', 'upgrade', 'via', 'warning'
'authorization',
'content-type',
'content-length',
'host',
'user-agent',
'accept',
'accept-encoding',
'connection',
'transfer-encoding',
'upgrade',
'via',
'warning',
};
if (reservedHeaders.contains(lowerKey)) {
return AppLocalizations.of(context)!.headerNameReserved(key);
}
return null;
}
String? _validateHeaderValue(String value) {
if (value.isEmpty) return AppLocalizations.of(context)!.headerValueEmpty;
if (value.length > 1024) return AppLocalizations.of(context)!.headerValueTooLong;
if (value.length > 1024) {
return AppLocalizations.of(context)!.headerValueTooLong;
}
// Check for valid characters (no control characters except tab)
for (int i = 0; i < value.length; i++) {
final char = value.codeUnitAt(i);
@@ -850,14 +873,14 @@ class _ServerConnectionPageState extends ConsumerState<ServerConnectionPage> {
return AppLocalizations.of(context)!.headerValueInvalidChars;
}
}
// Check for security-sensitive patterns
if (value.toLowerCase().contains('script') ||
value.contains('<') ||
value.contains('>')) {
return AppLocalizations.of(context)!.headerValueUnsafe;
}
return null;
}