import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:uuid/uuid.dart'; import 'package:worldhopper/l10n/app_localizations.dart'; import 'package:worldhopper/models/server.dart'; import 'package:worldhopper/helpers/snackbar_helper.dart'; import 'package:worldhopper/providers/server_provider.dart'; import 'package:worldhopper/providers/server_software_provider.dart'; import 'package:worldhopper/services/server_software/server_software_type.dart'; /// Screen for adding a new server class AddServerScreen extends ConsumerStatefulWidget { const AddServerScreen({super.key}); @override ConsumerState createState() => _AddServerScreenState(); } class _AddServerScreenState extends ConsumerState { final _formKey = GlobalKey(); final _nameController = TextEditingController(); final _urlController = TextEditingController(); final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); final _urlFocusNode = FocusNode(); bool _requiresAuth = false; bool _obscurePassword = true; bool _isLoading = false; bool _isTesting = false; ServerSoftwareType _selectedServerType = ServerSoftwareType.generic; bool _serverTypeManuallySet = false; @override void initState() { super.initState(); _urlFocusNode.addListener(_onUrlFocusChange); } @override void dispose() { _nameController.dispose(); _urlController.dispose(); _usernameController.dispose(); _passwordController.dispose(); _urlFocusNode.removeListener(_onUrlFocusChange); _urlFocusNode.dispose(); super.dispose(); } void _onUrlFocusChange() { if (!_urlFocusNode.hasFocus && !_serverTypeManuallySet) { _autoDetectServerType(); } } void _autoDetectServerType() { final url = _urlController.text.trim(); if (url.isEmpty) return; final uri = Uri.tryParse(url); if (uri == null || !uri.hasScheme || !uri.hasAuthority) return; final tempServer = Server( id: '', name: '', url: url, createdAt: DateTime.now(), ); final softwareService = ref.read(serverSoftwareServiceProvider); final detected = softwareService.detect(tempServer); if (detected != _selectedServerType) { setState(() { _selectedServerType = detected; }); } } String _serverTypeLabel(ServerSoftwareType type, AppLocalizations l10n) { return switch (type) { ServerSoftwareType.generic => l10n.serverTypeGeneric, ServerSoftwareType.kavita => l10n.serverTypeKavita, }; } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); return Scaffold( appBar: AppBar( title: Text(l10n.addServerTitle), ), body: Form( key: _formKey, child: ListView( padding: const EdgeInsets.all(16), children: [ // Name field TextFormField( controller: _nameController, decoration: InputDecoration( labelText: l10n.addServerNameLabel, hintText: l10n.addServerNameHint, prefixIcon: const Icon(Icons.label), ), textInputAction: TextInputAction.next, validator: (value) { if (value == null || value.trim().isEmpty) { return l10n.addServerValidationNameRequired; } return null; }, ), const SizedBox(height: 16), // URL field TextFormField( controller: _urlController, focusNode: _urlFocusNode, decoration: InputDecoration( labelText: l10n.addServerUrlLabel, hintText: _selectedServerType == ServerSoftwareType.kavita ? l10n.addServerUrlHintKavita : l10n.addServerUrlHint, prefixIcon: const Icon(Icons.link), ), keyboardType: TextInputType.url, textInputAction: TextInputAction.next, validator: (value) { if (value == null || value.trim().isEmpty) { return l10n.addServerValidationUrlRequired; } final uri = Uri.tryParse(value.trim()); if (uri == null || !uri.hasScheme || !uri.hasAuthority) { return l10n.addServerValidationUrlInvalid; } if (uri.scheme != 'http' && uri.scheme != 'https') { return l10n.addServerValidationUrlScheme; } return null; }, ), const SizedBox(height: 16), // Server Type dropdown DropdownButtonFormField( initialValue: _selectedServerType, decoration: InputDecoration( labelText: l10n.addServerServerType, helperText: l10n.addServerServerTypeSubtitle, prefixIcon: const Icon(Icons.dns), ), items: ServerSoftwareType.values.map((type) { return DropdownMenuItem( value: type, child: Text(_serverTypeLabel(type, l10n)), ); }).toList(), onChanged: (value) { if (value != null) { setState(() { _selectedServerType = value; _serverTypeManuallySet = true; }); } }, ), const SizedBox(height: 24), // Authentication toggle SwitchListTile( title: Text(l10n.addServerAuth), subtitle: Text(l10n.addServerAuthSubtitle), value: _requiresAuth, onChanged: (value) { setState(() { _requiresAuth = value; if (!value) { _usernameController.clear(); _passwordController.clear(); } }); }, ), const SizedBox(height: 16), // Authentication fields if (_requiresAuth) ...[ TextFormField( controller: _usernameController, decoration: InputDecoration( labelText: l10n.addServerUsername, prefixIcon: const Icon(Icons.person), ), textInputAction: TextInputAction.next, validator: (value) { if (_requiresAuth && (value == null || value.trim().isEmpty)) { return l10n.addServerValidationUsernameRequired; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: _passwordController, decoration: InputDecoration( labelText: l10n.addServerPassword, prefixIcon: const Icon(Icons.lock), suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility : Icons.visibility_off, ), onPressed: () { setState(() { _obscurePassword = !_obscurePassword; }); }, ), ), obscureText: _obscurePassword, textInputAction: TextInputAction.done, validator: (value) { if (_requiresAuth && (value == null || value.isEmpty)) { return l10n.addServerValidationPasswordRequired; } return null; }, onFieldSubmitted: (_) => _saveServer(), ), const SizedBox(height: 24), ], // Test connection button OutlinedButton.icon( onPressed: _isLoading || _isTesting ? null : _testConnection, icon: _isTesting ? const SizedBox( height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.wifi_find), label: Text( _isTesting ? l10n.testConnectionTesting : l10n.testConnectionButton, ), ), const SizedBox(height: 16), // Help text Card( color: Theme.of(context).colorScheme.surfaceContainerHighest, child: Padding( padding: const EdgeInsets.all(16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.info_outline, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 12), Expanded( child: Text( l10n.addServerHelp, style: Theme.of(context).textTheme.bodySmall, ), ), ], ), ), ), const SizedBox(height: 32), // Save button FilledButton( onPressed: _isLoading ? null : _saveServer, child: _isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : Text(l10n.addServerButton), ), ], ), ), ); } Future _testConnection() async { // Validate URL at minimum final url = _urlController.text.trim(); if (url.isEmpty) { final l10n = AppLocalizations.of(context); context.showWarningSnackBar(l10n.addServerValidationUrlRequired); return; } final uri = Uri.tryParse(url); if (uri == null || !uri.hasScheme || !uri.hasAuthority) { final l10n = AppLocalizations.of(context); context.showWarningSnackBar(l10n.addServerValidationUrlInvalid); return; } setState(() => _isTesting = true); try { final l10n = AppLocalizations.of(context); final tempServer = Server( id: 'test', name: 'Test', url: url, username: _requiresAuth ? _usernameController.text.trim() : null, password: _requiresAuth ? _passwordController.text : null, createdAt: DateTime.now(), softwareType: _selectedServerType, ); final softwareService = ref.read(serverSoftwareServiceProvider); final software = softwareService.getImplementation(tempServer); await software.testConnection(tempServer); if (!mounted) return; context.showSuccessSnackBar(l10n.testConnectionSuccess); } catch (e) { if (mounted) { final l10n = AppLocalizations.of(context); debugPrint('testConnection failed: $e'); context.showErrorSnackBar(l10n.testConnectionError(e.toString())); } } finally { if (mounted) setState(() => _isTesting = false); } } Future _saveServer() async { if (!_formKey.currentState!.validate()) { return; } setState(() { _isLoading = true; }); try { final l10n = AppLocalizations.of(context); final url = _urlController.text.trim(); // Check if server already exists final exists = await ref.read(serverNotifierProvider.notifier).serverExists(url); if (exists) { if (mounted) { context.showWarningSnackBar(l10n.addServerDuplicate); } setState(() { _isLoading = false; }); return; } // Create new server final server = Server( id: const Uuid().v4(), name: _nameController.text.trim(), url: url, username: _requiresAuth ? _usernameController.text.trim() : null, password: _requiresAuth ? _passwordController.text : null, createdAt: DateTime.now(), softwareType: _selectedServerType, ); // Save server await ref.read(serverNotifierProvider.notifier).addServer(server); if (mounted) { context.showSuccessSnackBar(l10n.addServerSuccess(server.name)); context.pop(); } } catch (e) { if (mounted) { final l10n = AppLocalizations.of(context); context.showErrorSnackBar(l10n.addServerError(e.toString())); } } finally { if (mounted) { setState(() { _isLoading = false; }); } } } }