import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:worldhopper/l10n/app_localizations.dart'; import 'package:worldhopper/helpers/snackbar_helper.dart'; import 'package:worldhopper/models/opds_server.dart'; import 'package:worldhopper/providers/server_provider.dart'; import 'package:worldhopper/services/koreader_sync_service.dart'; /// Screen for editing an existing OPDS server class EditServerScreen extends ConsumerStatefulWidget { final String serverId; const EditServerScreen({ super.key, required this.serverId, }); @override ConsumerState createState() => _EditServerScreenState(); } class _EditServerScreenState extends ConsumerState { final _formKey = GlobalKey(); final _nameController = TextEditingController(); final _urlController = TextEditingController(); final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); bool _requiresAuth = false; bool _obscurePassword = true; bool _isLoading = false; OPDSServer? _originalServer; // KOReader sync fields bool _koreaderSyncEnabled = false; final _koreaderSyncUrlController = TextEditingController(); final _koreaderSyncUsernameController = TextEditingController(); final _koreaderSyncPasswordController = TextEditingController(); bool _obscureKoreaderPassword = true; bool _isTestingKoreaderConnection = false; @override void dispose() { _nameController.dispose(); _urlController.dispose(); _usernameController.dispose(); _passwordController.dispose(); _koreaderSyncUrlController.dispose(); _koreaderSyncUsernameController.dispose(); _koreaderSyncPasswordController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final serverAsync = ref.watch(serverProvider(widget.serverId)); return Scaffold( appBar: AppBar( title: Text(l10n.editServerTitle), ), body: serverAsync.when( data: (server) { if (server == null) { return Center( child: Text(l10n.editServerNotFound), ); } // Initialize form fields if not already done if (_originalServer == null) { _originalServer = server; _nameController.text = server.name; _urlController.text = server.url; _requiresAuth = server.requiresAuth; if (server.username != null) { _usernameController.text = server.username!; } if (server.password != null) { _passwordController.text = server.password!; } // Initialize KOReader sync fields _koreaderSyncEnabled = server.koreaderSyncEnabled; if (server.koreaderSyncUrl != null) { _koreaderSyncUrlController.text = server.koreaderSyncUrl!; } if (server.koreaderSyncUsername != null) { _koreaderSyncUsernameController.text = server.koreaderSyncUsername!; } if (server.koreaderSyncPassword != null) { _koreaderSyncPasswordController.text = server.koreaderSyncPassword!; } } return Form( key: _formKey, child: ListView( padding: const EdgeInsets.all(16), children: [ // Name field TextFormField( controller: _nameController, decoration: InputDecoration( labelText: l10n.addServerNameLabel, 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, decoration: InputDecoration( labelText: l10n.addServerUrlLabel, 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: 24), // Authentication toggle SwitchListTile( title: Text(l10n.addServerRequiresAuth), subtitle: Text(l10n.addServerRequiresAuthSubtitle), value: _requiresAuth, onChanged: (value) { setState(() { _requiresAuth = value; if (!value) { _usernameController.clear(); _passwordController.clear(); } }); }, ), const SizedBox(height: 16), // Authentication fields (conditionally shown) 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; }, ), const SizedBox(height: 24), ], // KOReader Sync toggle SwitchListTile( title: Text(l10n.koreaderSyncToggle), subtitle: Text(l10n.koreaderSyncToggleSubtitle), value: _koreaderSyncEnabled, onChanged: (value) { setState(() { _koreaderSyncEnabled = value; if (!value) { _koreaderSyncUrlController.clear(); _koreaderSyncUsernameController.clear(); _koreaderSyncPasswordController.clear(); } }); }, ), const SizedBox(height: 16), // KOReader sync fields (conditionally shown) if (_koreaderSyncEnabled) ...[ TextFormField( controller: _koreaderSyncUrlController, decoration: InputDecoration( labelText: l10n.koreaderSyncUrl, hintText: l10n.koreaderSyncUrlHint, prefixIcon: const Icon(Icons.sync), ), keyboardType: TextInputType.url, textInputAction: TextInputAction.next, validator: (value) { if (_koreaderSyncEnabled && (value == null || value.trim().isEmpty)) { return l10n.koreaderSyncValidationUrlRequired; } if (value != null && value.trim().isNotEmpty) { final uri = Uri.tryParse(value.trim()); if (uri == null || !uri.hasScheme || !uri.hasAuthority) { return l10n.koreaderSyncValidationUrlInvalid; } } return null; }, ), const SizedBox(height: 16), TextFormField( controller: _koreaderSyncUsernameController, decoration: InputDecoration( labelText: l10n.koreaderSyncUsername, prefixIcon: const Icon(Icons.person_outline), ), textInputAction: TextInputAction.next, validator: (value) { if (_koreaderSyncEnabled && (value == null || value.trim().isEmpty)) { return l10n.koreaderSyncValidationUsernameRequired; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: _koreaderSyncPasswordController, decoration: InputDecoration( labelText: l10n.koreaderSyncPassword, prefixIcon: const Icon(Icons.lock_outline), suffixIcon: IconButton( icon: Icon( _obscureKoreaderPassword ? Icons.visibility : Icons.visibility_off, ), onPressed: () { setState(() { _obscureKoreaderPassword = !_obscureKoreaderPassword; }); }, ), ), obscureText: _obscureKoreaderPassword, textInputAction: TextInputAction.done, validator: (value) { if (_koreaderSyncEnabled && (value == null || value.isEmpty)) { return l10n.koreaderSyncValidationPasswordRequired; } return null; }, ), const SizedBox(height: 16), // Test connection button OutlinedButton.icon( onPressed: _isTestingKoreaderConnection ? null : _testKoreaderConnection, icon: _isTestingKoreaderConnection ? const SizedBox( height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.wifi_tethering), label: Text(l10n.koreaderSyncTestConnection), ), const SizedBox(height: 16), // KOReader sync 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.koreaderSyncHelp, style: Theme.of(context).textTheme.bodySmall, ), ), ], ), ), ), const SizedBox(height: 24), ], // Server metadata Card( color: Theme.of(context).colorScheme.surfaceContainerHighest, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.editServerInfo, style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), _buildInfoRow( l10n.editServerAdded, _formatDateTime(server.createdAt), ), if (server.lastSyncedAt != null) _buildInfoRow( l10n.editServerLastSynced, _formatDateTime(server.lastSyncedAt!), ), ], ), ), ), 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.editServerSaveButton), ), ], ), ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 48, color: Colors.red), const SizedBox(height: 16), Text(l10n.editServerErrorLoading(error.toString())), ], ), ), ), ); } Widget _buildInfoRow(String label, String value) { return Padding( padding: const EdgeInsets.only(top: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context) .colorScheme .onSurface .withValues(alpha: 0.6), ), ), Text( value, style: Theme.of(context).textTheme.bodySmall, ), ], ), ); } String _formatDateTime(DateTime dateTime) { return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ' '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}'; } Future _testKoreaderConnection() async { final url = _koreaderSyncUrlController.text.trim(); final username = _koreaderSyncUsernameController.text.trim(); final password = _koreaderSyncPasswordController.text; if (url.isEmpty || username.isEmpty || password.isEmpty) { return; } setState(() { _isTestingKoreaderConnection = true; }); try { final l10n = AppLocalizations.of(context); final testServer = OPDSServer( id: 'test', name: 'test', url: '', createdAt: DateTime.now(), koreaderSyncEnabled: true, koreaderSyncUrl: url, koreaderSyncUsername: username, koreaderSyncPassword: password, ); final syncService = KoreaderSyncService(testServer); // Test authentication (also verifies server is reachable) final authenticated = await syncService.authenticate(); if (mounted) { if (authenticated) { context.showSuccessSnackBar(l10n.koreaderSyncTestSuccess(username)); } else { context .showErrorSnackBar(l10n.koreaderSyncTestFailed('Unauthorized')); } } } catch (e) { if (mounted) { final l10n = AppLocalizations.of(context); context.showErrorSnackBar(l10n.koreaderSyncTestFailed(e.toString())); } } finally { if (mounted) { setState(() { _isTestingKoreaderConnection = false; }); } } } Future _saveServer() async { if (!_formKey.currentState!.validate()) { return; } setState(() { _isLoading = true; }); try { final l10n = AppLocalizations.of(context); // Create updated server final updatedServer = _originalServer!.copyWith( name: _nameController.text.trim(), url: _urlController.text.trim(), username: _requiresAuth ? _usernameController.text.trim() : null, password: _requiresAuth ? _passwordController.text : null, koreaderSyncEnabled: _koreaderSyncEnabled, koreaderSyncUrl: _koreaderSyncEnabled ? _koreaderSyncUrlController.text.trim() : null, koreaderSyncUsername: _koreaderSyncEnabled ? _koreaderSyncUsernameController.text.trim() : null, koreaderSyncPassword: _koreaderSyncEnabled ? _koreaderSyncPasswordController.text : null, ); // Save server await ref .read(serverNotifierProvider.notifier) .updateServer(updatedServer); if (mounted) { context.showSuccessSnackBar(l10n.editServerSuccess(updatedServer.name)); context.pop(); } } catch (e) { if (mounted) { final l10n = AppLocalizations.of(context); context.showErrorSnackBar(l10n.editServerError(e.toString())); } } finally { if (mounted) { setState(() { _isLoading = false; }); } } } }