import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import '../providers/settings_provider.dart'; import '../providers/simple_sync_provider.dart'; import '../services/database_sync_service.dart'; class SimpleSyncSettingsScreen extends StatefulWidget { const SimpleSyncSettingsScreen({super.key}); @override State createState() => _SimpleSyncSettingsScreenState(); } class _SimpleSyncSettingsScreenState extends State { final _formKey = GlobalKey(); final _serverUrlController = TextEditingController(); final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); final _remotePathController = TextEditingController(); String _previewUrl = ''; @override void initState() { super.initState(); _serverUrlController.addListener(_updatePreviewUrl); _usernameController.addListener(_updatePreviewUrl); _loadSavedConfiguration(); } void _loadSavedConfiguration() { WidgetsBinding.instance.addPostFrameCallback((_) { final syncProvider = context.read(); if (syncProvider.serverUrl != null) { _serverUrlController.text = _extractHostnameFromUrl(syncProvider.serverUrl!); } if (syncProvider.username != null) { _usernameController.text = syncProvider.username!; } if (syncProvider.password != null) { _passwordController.text = syncProvider.password!; } if (syncProvider.remotePath != null) { _remotePathController.text = syncProvider.remotePath!; } _updatePreviewUrl(); }); } String _extractHostnameFromUrl(String fullUrl) { try { final uri = Uri.parse(fullUrl); return uri.host; } catch (e) { return fullUrl; // Return as-is if parsing fails } } void _updatePreviewUrl() { setState(() { if (_serverUrlController.text.isNotEmpty && _usernameController.text.isNotEmpty) { _previewUrl = _constructWebDAVUrl(_serverUrlController.text, _usernameController.text); } else { _previewUrl = ''; } }); } @override void dispose() { _serverUrlController.removeListener(_updatePreviewUrl); _usernameController.removeListener(_updatePreviewUrl); _serverUrlController.dispose(); _usernameController.dispose(); _passwordController.dispose(); _remotePathController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final syncProvider = context.watch(); return Scaffold( appBar: AppBar( title: const Text('Database Sync Settings'), backgroundColor: Theme.of(context).colorScheme.inversePrimary, actions: [ IconButton( tooltip: 'Save Configuration', onPressed: syncProvider.isSyncing ? null : _configureSync, icon: const Icon(Icons.save), ), ], ), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildStatusCard(syncProvider), const SizedBox(height: 20), _buildConfigurationSection(syncProvider), const SizedBox(height: 20), _buildActionButtons(), ], ), ), ), ); } Widget _buildStatusCard(SimpleSyncProvider syncProvider) { IconData icon; Color color; String statusText = syncProvider.getStatusText(); switch (syncProvider.status) { case SyncStatus.idle: icon = syncProvider.isAutoSync ? Icons.sync_alt : Icons.sync; color = Colors.blue; break; case SyncStatus.downloading: case SyncStatus.merging: case SyncStatus.uploading: icon = syncProvider.isAutoSync ? Icons.sync_alt : Icons.sync; color = syncProvider.isAutoSync ? Colors.deepOrange : Colors.orange; break; case SyncStatus.completed: icon = syncProvider.isAutoSync ? Icons.check_circle_outline : Icons.check_circle; color = Colors.green; break; case SyncStatus.error: icon = Icons.error; color = Colors.red; break; } return Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Row( children: [ Icon(icon, color: color, size: 24), const SizedBox(width: 12), Expanded( child: Text( statusText, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: color, ), ), ), // Sync action inside the status card if (syncProvider.isSyncing) ...[ const SizedBox(width: 12), SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2.2, valueColor: AlwaysStoppedAnimation(color), ), ), ] else ...[ IconButton( tooltip: 'Sync Database', onPressed: (!syncProvider.isConfigured || syncProvider.isSyncing) ? null : _syncDatabase, icon: const Icon(Icons.sync), color: Theme.of(context).colorScheme.primary, ), ], ], ), _buildAutoSyncStatusIndicator(syncProvider), if (syncProvider.lastSyncTime != null) ...[ const SizedBox(height: 8), Text( 'Last sync: ${_formatDateTime(syncProvider.lastSyncTime!)}', style: Theme.of(context).textTheme.bodySmall, ), ], // Show auto-sync specific errors if (syncProvider.isAutoSyncDisabledDueToErrors) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.red.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.withValues(alpha: 0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.warning, color: Colors.red, size: 20), const SizedBox(width: 8), Expanded( child: Text( 'Auto-sync disabled due to repeated failures', style: Theme.of(context).textTheme.titleSmall?.copyWith( color: Colors.red, fontWeight: FontWeight.w600, ), ), ), ], ), if (syncProvider.autoSyncLastError != null) ...[ const SizedBox(height: 8), Text( syncProvider.autoSyncLastError!, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.red[700], ), ), ], const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton.icon( onPressed: () => syncProvider.resetAutoSyncErrors(), icon: const Icon(Icons.refresh, size: 16), label: const Text('Reset & Re-enable'), style: TextButton.styleFrom( foregroundColor: Colors.red, ), ), ], ), ], ), ), ] else if (syncProvider.lastError != null) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.red.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(4), ), child: Row( children: [ Expanded( child: Text( _getErrorMessage(syncProvider), style: const TextStyle(color: Colors.red), ), ), IconButton( icon: const Icon(Icons.close, color: Colors.red), onPressed: () => syncProvider.clearError(), ), ], ), ), ], ], ), ), ); } Widget _buildConfigurationSection(SimpleSyncProvider syncProvider) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Card( child: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Sync Configuration', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), _buildAutoSyncSection(), ], ), ), ), const SizedBox(height: 12), Card( child: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'WebDAV Settings', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), TextFormField( controller: _serverUrlController, decoration: const InputDecoration( labelText: 'Server URL', hintText: 'your-nextcloud.com', helperText: 'Enter just the hostname. We\'ll auto-detect the full WebDAV path.', border: OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter a server URL'; } return null; }, ), const SizedBox(height: 8), TextFormField( controller: _usernameController, decoration: const InputDecoration( labelText: 'Username', border: OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter a username'; } return null; }, ), if (_previewUrl.isNotEmpty) ...[ const SizedBox(height: 6), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(8), border: Border.all( color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.5), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'WebDAV URL Preview:', style: Theme.of(context).textTheme.labelMedium, ), const SizedBox(height: 4), SelectableText( _previewUrl, style: Theme.of(context).textTheme.bodySmall?.copyWith( fontFamily: 'monospace', color: Theme.of(context).colorScheme.primary, ), ), const SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ ElevatedButton.icon( onPressed: syncProvider.isSyncing ? null : _testConnection, icon: const Icon(Icons.link), label: const Text('Test'), style: ElevatedButton.styleFrom( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), elevation: 0, ), ), ], ), ], ), ), ], const SizedBox(height: 8), TextFormField( controller: _passwordController, decoration: const InputDecoration( labelText: 'Password', border: OutlineInputBorder(), ), obscureText: true, validator: (value) { if (value == null || value.isEmpty) { return 'Please enter a password'; } return null; }, ), const SizedBox(height: 8), TextFormField( controller: _remotePathController, decoration: const InputDecoration( labelText: 'Remote Path (optional)', hintText: 'Supplements/', border: OutlineInputBorder(), ), ), ], ), ), ), ], ); } Widget _buildActionButtons() { // Buttons have been moved into the AppBar / cards. Keep a small spacer here for layout. return const SizedBox.shrink(); } Widget _buildAutoSyncSection() { return Consumer( builder: (context, settingsProvider, child) { return Consumer( builder: (context, syncProvider, child) { return Container( padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(12), border: Border.all( color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.5), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SwitchListTile( title: Row( children: [ const Text( 'Auto-sync', style: TextStyle(fontWeight: FontWeight.w600), ), const SizedBox(width: 8), _buildAutoSyncStatusBadge(settingsProvider, syncProvider), ], ), subtitle: Text( settingsProvider.autoSyncEnabled ? 'Automatically sync when you make changes' : 'Sync manually using the sync button', style: Theme.of(context).textTheme.bodySmall, ), value: settingsProvider.autoSyncEnabled, onChanged: (bool value) async { await settingsProvider.setAutoSyncEnabled(value); }, contentPadding: EdgeInsets.zero, ), if (settingsProvider.autoSyncEnabled) ...[ const SizedBox(height: 8), Padding( padding: const EdgeInsets.only(left: 16.0), child: Text( 'Changes are debounced for ${settingsProvider.autoSyncDebounceSeconds} seconds to prevent excessive syncing.', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, fontStyle: FontStyle.italic, ), ), ), const SizedBox(height: 12), Padding( padding: const EdgeInsets.only(left: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Debounce timeout', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 8), SegmentedButton( segments: const [ ButtonSegment(value: 1, label: Text('1s')), ButtonSegment(value: 5, label: Text('5s')), ButtonSegment(value: 15, label: Text('15s')), ButtonSegment(value: 30, label: Text('30s')), ], selected: {settingsProvider.autoSyncDebounceSeconds}, onSelectionChanged: (values) { settingsProvider.setAutoSyncDebounceSeconds(values.first); }, ), ], ), ), ], const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon( Icons.info_outline, size: 16, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 8), Expanded( child: Text( 'Auto-sync triggers when you add, update, or delete supplements and intakes. Configure your WebDAV settings below to enable syncing.', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), ), ], ), ), ], ), ); }, ); }, ); } Future _testConnection() async { if (!_formKey.currentState!.validate()) return; final syncProvider = context.read(); try { // Construct the full WebDAV URL from the simple hostname final fullWebDAVUrl = _constructWebDAVUrl( _serverUrlController.text.trim(), _usernameController.text.trim(), ); // Configure temporarily for testing await syncProvider.configure( serverUrl: fullWebDAVUrl, username: _usernameController.text.trim(), password: _passwordController.text.trim(), remotePath: _remotePathController.text.trim().isEmpty ? 'Supplements' : _remotePathController.text.trim(), ); final success = await syncProvider.testConnection(); if (mounted) { final sonner = ShadSonner.of(context); final id = DateTime.now().millisecondsSinceEpoch; sonner.show( ShadToast( id: id, title: Text(success ? 'Connection successful!' : 'Connection failed. Check your settings.'), action: ShadButton( size: ShadButtonSize.sm, child: const Text('Dismiss'), onPressed: () => sonner.hide(id), ), ), ); } } catch (e) { if (mounted) { final sonner = ShadSonner.of(context); final id = DateTime.now().millisecondsSinceEpoch; sonner.show( ShadToast( id: id, title: const Text('Connection test failed'), description: Text('$e'), action: ShadButton( size: ShadButtonSize.sm, child: const Text('Dismiss'), onPressed: () => sonner.hide(id), ), ), ); } } } Future _configureSync() async { if (!_formKey.currentState!.validate()) return; final syncProvider = context.read(); try { // Construct the full WebDAV URL from the simple hostname final fullWebDAVUrl = _constructWebDAVUrl( _serverUrlController.text.trim(), _usernameController.text.trim(), ); await syncProvider.configure( serverUrl: fullWebDAVUrl, username: _usernameController.text.trim(), password: _passwordController.text.trim(), remotePath: _remotePathController.text.trim().isEmpty ? 'Supplements' : _remotePathController.text.trim(), ); if (mounted) { final sonner = ShadSonner.of(context); final id = DateTime.now().millisecondsSinceEpoch; sonner.show( ShadToast( id: id, title: const Text('Configuration saved successfully!'), action: ShadButton( size: ShadButtonSize.sm, child: const Text('Dismiss'), onPressed: () => sonner.hide(id), ), ), ); } } catch (e) { if (mounted) { final sonner = ShadSonner.of(context); final id = DateTime.now().millisecondsSinceEpoch; sonner.show( ShadToast( id: id, title: const Text('Failed to save configuration'), description: Text('$e'), action: ShadButton( size: ShadButtonSize.sm, child: const Text('Dismiss'), onPressed: () => sonner.hide(id), ), ), ); } } } Future _syncDatabase() async { final syncProvider = context.read(); try { await syncProvider.syncDatabase(isAutoSync: false); if (mounted) { final sonner = ShadSonner.of(context); final id = DateTime.now().millisecondsSinceEpoch; sonner.show( ShadToast( id: id, title: const Text('Manual sync completed!'), action: ShadButton( size: ShadButtonSize.sm, child: const Text('Dismiss'), onPressed: () => sonner.hide(id), ), ), ); } } catch (e) { if (mounted) { final sonner = ShadSonner.of(context); final id = DateTime.now().millisecondsSinceEpoch; sonner.show( ShadToast( id: id, title: const Text('Manual sync failed'), description: Text('$e'), action: ShadButton( size: ShadButtonSize.sm, child: const Text('Dismiss'), onPressed: () => sonner.hide(id), ), ), ); } } } String _constructWebDAVUrl(String serverUrl, String username) { // Remove any protocol prefix if present String cleanUrl = serverUrl.trim(); if (cleanUrl.startsWith('http://')) { cleanUrl = cleanUrl.substring(7); } else if (cleanUrl.startsWith('https://')) { cleanUrl = cleanUrl.substring(8); } // Remove trailing slash if present if (cleanUrl.endsWith('/')) { cleanUrl = cleanUrl.substring(0, cleanUrl.length - 1); } // For Nextcloud instances, construct the standard WebDAV path // Default to HTTPS for security return 'https://$cleanUrl/remote.php/dav/files/$username/'; } Widget _buildAutoSyncStatusIndicator(SimpleSyncProvider syncProvider) { return Consumer( builder: (context, settingsProvider, child) { // Only show auto-sync status if auto-sync is enabled if (!settingsProvider.autoSyncEnabled) { return const SizedBox.shrink(); } // Check if auto-sync service has pending sync final autoSyncService = syncProvider.autoSyncService; if (autoSyncService == null) { return const SizedBox.shrink(); } // Show pending auto-sync indicator if (autoSyncService.hasPendingSync && !syncProvider.isSyncing) { return Container( margin: const EdgeInsets.only(top: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.blue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.blue.withValues(alpha: 0.3)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 12, height: 12, child: CircularProgressIndicator( strokeWidth: 1.5, valueColor: const AlwaysStoppedAnimation(Colors.blue), ), ), const SizedBox(width: 8), Text( 'Auto-sync pending...', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.blue, fontWeight: FontWeight.w500, ), ), ], ), ); } // Show auto-sync active indicator (when sync is running and it's auto-triggered) if (syncProvider.isSyncing && syncProvider.isAutoSync) { return Container( margin: const EdgeInsets.only(top: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.deepOrange.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.deepOrange.withValues(alpha: 0.3)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.sync_alt, size: 12, color: Colors.deepOrange, ), const SizedBox(width: 8), Text( 'Auto-sync active', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.deepOrange, fontWeight: FontWeight.w500, ), ), ], ), ); } return const SizedBox.shrink(); }, ); } Widget _buildAutoSyncStatusBadge(SettingsProvider settingsProvider, SimpleSyncProvider syncProvider) { if (!settingsProvider.autoSyncEnabled) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.grey.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), ), child: Text( 'OFF', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Colors.grey[600], fontWeight: FontWeight.w600, ), ), ); } // Check if auto-sync is disabled due to errors if (syncProvider.isAutoSyncDisabledDueToErrors) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.red.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), ), child: Text( 'ERROR', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Colors.red[700], fontWeight: FontWeight.w600, ), ), ); } // Check if sync is configured if (!syncProvider.isConfigured) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.orange.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), ), child: Text( 'NOT CONFIGURED', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Colors.orange[700], fontWeight: FontWeight.w600, ), ), ); } // Check if there are recent failures but not disabled yet if (syncProvider.autoSyncConsecutiveFailures > 0) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.orange.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), ), child: Text( 'RETRYING', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Colors.orange[700], fontWeight: FontWeight.w600, ), ), ); } return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: Colors.green.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), ), child: Text( 'ACTIVE', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Colors.green[700], fontWeight: FontWeight.w600, ), ), ); } String _getErrorMessage(SimpleSyncProvider syncProvider) { final error = syncProvider.lastError ?? 'Unknown error'; // Add context for auto-sync errors if (syncProvider.isAutoSync) { return 'Auto-sync error: $error'; } return error; } String _formatDateTime(DateTime dateTime) { return '${dateTime.day}/${dateTime.month}/${dateTime.year} ${dateTime.hour}:${dateTime.minute.toString().padLeft(2, '0')}'; } }