import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import '../services/tile_cache_service.dart'; import '../services/validation_service.dart'; import '../models/map_layer.dart'; import '../l10n/app_localizations.dart'; class MapManagementScreen extends StatefulWidget { final TileCacheService tileCacheService; final MapLayer? initialLayer; final LatLngBounds? initialBounds; final int? initialZoom; const MapManagementScreen({ super.key, required this.tileCacheService, this.initialLayer, this.initialBounds, this.initialZoom, }); @override State createState() => _MapManagementScreenState(); } class _MapManagementScreenState extends State { bool _isLoading = false; String? _statusMessage; Map? _cacheStats; // Download parameters late MapLayer _selectedLayer; late TextEditingController _northController; late TextEditingController _southController; late TextEditingController _eastController; late TextEditingController _westController; late int _minZoom; late int _maxZoom; double _downloadProgress = 0.0; bool _isDownloading = false; @override void initState() { super.initState(); // Initialize with provided values or defaults _selectedLayer = widget.initialLayer ?? MapLayer.openStreetMap; if (widget.initialBounds != null) { _northController = TextEditingController( text: widget.initialBounds!.north.toStringAsFixed(4), ); _southController = TextEditingController( text: widget.initialBounds!.south.toStringAsFixed(4), ); _eastController = TextEditingController( text: widget.initialBounds!.east.toStringAsFixed(4), ); _westController = TextEditingController( text: widget.initialBounds!.west.toStringAsFixed(4), ); } else { _northController = TextEditingController(text: '46.1'); _southController = TextEditingController(text: '46.0'); _eastController = TextEditingController(text: '14.6'); _westController = TextEditingController(text: '14.4'); } // Set zoom levels if (widget.initialZoom != null) { _minZoom = (widget.initialZoom! - 2).clamp(1, 19); _maxZoom = (widget.initialZoom! + 2).clamp(1, 19); } else { _minZoom = 10; _maxZoom = 16; } _loadCacheStats(); } @override void dispose() { _northController.dispose(); _southController.dispose(); _eastController.dispose(); _westController.dispose(); super.dispose(); } Future _loadCacheStats() async { if (!mounted) return; setState(() => _isLoading = true); try { final stats = await widget.tileCacheService.getStoreStats(); if (!mounted) return; setState(() { _cacheStats = stats; _isLoading = false; }); } catch (e) { if (!mounted) return; setState(() { _statusMessage = AppLocalizations.of(context)!.errorLoadingStats(e.toString()); _isLoading = false; }); } } Future _downloadRegion() async { final validator = ValidationService(); try { // Parse coordinates final north = double.tryParse(_northController.text); final south = double.tryParse(_southController.text); final east = double.tryParse(_eastController.text); final west = double.tryParse(_westController.text); // Validate bounds final boundsResult = validator.validateBounds( north: north, south: south, east: east, west: west, ); if (!boundsResult.isValid) { _showError(boundsResult.errorMessage!); return; } // Validate zoom levels final minZoomResult = validator.validateZoomLevel(_minZoom); if (!minZoomResult.isValid) { _showError(AppLocalizations.of(context)!.minZoomError(minZoomResult.errorMessage!)); return; } final maxZoomResult = validator.validateZoomLevel(_maxZoom); if (!maxZoomResult.isValid) { _showError(AppLocalizations.of(context)!.maxZoomError(maxZoomResult.errorMessage!)); return; } if (_minZoom > _maxZoom) { _showError(AppLocalizations.of(context)!.minZoomGreaterThanMax); return; } final bounds = LatLngBounds( LatLng(south!, west!), LatLng(north!, east!), ); if (!mounted) return; setState(() { _isDownloading = true; _downloadProgress = 0.0; _statusMessage = AppLocalizations.of(context)!.startingDownload; }); await widget.tileCacheService.downloadRegion( layer: _selectedLayer, bounds: bounds, minZoom: _minZoom, maxZoom: _maxZoom, onProgress: (progress) { print('UI received progress update: $progress%'); if (!mounted) return; setState(() { _downloadProgress = progress; _statusMessage = AppLocalizations.of(context)!.downloadingMapTiles; }); }, ); if (!mounted) return; setState(() { _isDownloading = false; _statusMessage = AppLocalizations.of(context)!.downloadCompletedSuccessfully; }); await _loadCacheStats(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(AppLocalizations.of(context)!.mapDownloadCompleted), backgroundColor: Colors.green, ), ); } } catch (e) { if (!mounted) return; setState(() { _isDownloading = false; _statusMessage = AppLocalizations.of(context)!.downloadFailed(e.toString()); }); _showError(AppLocalizations.of(context)!.downloadFailed(e.toString())); } } Future _cancelDownload() async { try { if (!mounted) return; setState(() => _statusMessage = AppLocalizations.of(context)!.cancellingDownload); await widget.tileCacheService.cancelDownload(); if (!mounted) return; setState(() { _isDownloading = false; _statusMessage = AppLocalizations.of(context)!.downloadCancelled; }); await _loadCacheStats(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(AppLocalizations.of(context)!.cancel), backgroundColor: Colors.orange, ), ); } } catch (e) { if (!mounted) return; setState(() { _isDownloading = false; _statusMessage = AppLocalizations.of(context)!.cancelFailed(e.toString()); }); _showError(AppLocalizations.of(context)!.cancelFailed(e.toString())); } } Future _clearCache() async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: Text(AppLocalizations.of(context)!.clearMapsConfirmTitle), content: Text( AppLocalizations.of(context)!.clearMapsConfirmMessage, ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: Text(AppLocalizations.of(context)!.cancel), ), TextButton( onPressed: () => Navigator.pop(context, true), style: TextButton.styleFrom(foregroundColor: Colors.red), child: Text(AppLocalizations.of(context)!.clear), ), ], ), ); if (confirmed != true) return; if (!mounted) return; setState(() => _isLoading = true); try { await widget.tileCacheService.clearCache(); if (!mounted) return; setState(() => _isLoading = false); await _loadCacheStats(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(AppLocalizations.of(context)!.cacheClearedSuccessfully), backgroundColor: Colors.green, ), ); } } catch (e) { setState(() => _isLoading = false); _showError(AppLocalizations.of(context)!.clearCacheFailed(e.toString())); } } void _showError(String message) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.red, duration: const Duration(seconds: 4), ), ); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.mapManagement), ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Cache Statistics _buildStatisticsCard(), const SizedBox(height: 16), // Download Region _buildDownloadCard(), const SizedBox(height: 16), // Clear Cache _buildActionsCard(), ], ), ), ); } Widget _buildStatisticsCard() { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( AppLocalizations.of(context)!.cacheStatistics, style: Theme.of(context).textTheme.titleLarge, ), IconButton( icon: const Icon(Icons.refresh), onPressed: _loadCacheStats, ), ], ), const SizedBox(height: 16), if (_cacheStats != null) ...[ _buildStatRow( AppLocalizations.of(context)!.totalTiles, '${_cacheStats!['tileCount'] ?? 0}', Icons.grid_on, ), _buildStatRow( AppLocalizations.of(context)!.cacheSize, '${(_cacheStats!['sizeMB'] ?? 0.0).toStringAsFixed(2)} MB', Icons.storage, ), _buildStatRow( AppLocalizations.of(context)!.storeName, _cacheStats!['storeName'] ?? 'Unknown', Icons.folder, ), ] else Text(AppLocalizations.of(context)!.noCacheStatistics), ], ), ), ); } Widget _buildStatRow(String label, String value, IconData icon) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( children: [ Icon(icon, size: 20, color: Colors.grey[600]), const SizedBox(width: 12), Expanded( child: Text(label, style: const TextStyle(fontWeight: FontWeight.w500)), ), Text(value, style: TextStyle(color: Colors.grey[600])), ], ), ); } Widget _buildDownloadCard() { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( AppLocalizations.of(context)!.downloadRegion, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 16), // Map Layer Selection DropdownButtonFormField( value: _selectedLayer, decoration: InputDecoration( labelText: AppLocalizations.of(context)!.mapLayer, border: const OutlineInputBorder(), ), items: MapLayer.allLayers.map((layer) { return DropdownMenuItem( value: layer, child: Text(layer.getLocalizedName(context)), ); }).toList(), onChanged: _isDownloading ? null : (layer) { if (layer != null) { setState(() => _selectedLayer = layer); } }, ), const SizedBox(height: 16), // Coordinates Text( AppLocalizations.of(context)!.regionBounds, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), Row( children: [ Expanded( child: TextField( controller: _northController, decoration: InputDecoration( labelText: AppLocalizations.of(context)!.north, border: const OutlineInputBorder(), hintText: '46.1', ), keyboardType: const TextInputType.numberWithOptions(decimal: true), enabled: !_isDownloading, ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: _southController, decoration: InputDecoration( labelText: AppLocalizations.of(context)!.south, border: const OutlineInputBorder(), hintText: '46.0', ), keyboardType: const TextInputType.numberWithOptions(decimal: true), enabled: !_isDownloading, ), ), ], ), const SizedBox(height: 8), Row( children: [ Expanded( child: TextField( controller: _eastController, decoration: InputDecoration( labelText: AppLocalizations.of(context)!.east, border: const OutlineInputBorder(), hintText: '14.6', ), keyboardType: const TextInputType.numberWithOptions(decimal: true), enabled: !_isDownloading, ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: _westController, decoration: InputDecoration( labelText: AppLocalizations.of(context)!.west, border: const OutlineInputBorder(), hintText: '14.4', ), keyboardType: const TextInputType.numberWithOptions(decimal: true), enabled: !_isDownloading, ), ), ], ), const SizedBox(height: 16), // Zoom Levels Text( AppLocalizations.of(context)!.zoomLevels, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(AppLocalizations.of(context)!.minZoom(_minZoom)), Slider( value: _minZoom.toDouble(), min: 1, max: 19, divisions: 18, label: '$_minZoom', onChanged: _isDownloading ? null : (value) { setState(() { _minZoom = value.toInt(); if (_minZoom > _maxZoom) { _maxZoom = _minZoom; } }); }, ), ], ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(AppLocalizations.of(context)!.maxZoom(_maxZoom)), Slider( value: _maxZoom.toDouble(), min: 1, max: 19, divisions: 18, label: '$_maxZoom', onChanged: _isDownloading ? null : (value) { setState(() { _maxZoom = value.toInt(); if (_maxZoom < _minZoom) { _minZoom = _maxZoom; } }); }, ), ], ), ), ], ), // Download Progress if (_isDownloading) ...[ const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(8), border: Border.all( color: Theme.of(context).colorScheme.primary.withOpacity(0.3), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _statusMessage ?? AppLocalizations.of(context)!.downloadingDots, style: TextStyle( fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), Text( '${_downloadProgress.toStringAsFixed(1)}%', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: Theme.of(context).colorScheme.onPrimaryContainer, ), ), ], ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: _downloadProgress / 100, minHeight: 8, backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.2), valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary, ), ), ), ], ), ), ], const SizedBox(height: 16), // Download/Cancel Button if (_isDownloading) ElevatedButton.icon( onPressed: _cancelDownload, icon: const Icon(Icons.cancel), label: Text(AppLocalizations.of(context)!.cancelDownload), style: ElevatedButton.styleFrom( minimumSize: const Size.fromHeight(48), backgroundColor: Colors.red, foregroundColor: Colors.white, ), ) else ElevatedButton.icon( onPressed: _downloadRegion, icon: const Icon(Icons.download), label: Text(AppLocalizations.of(context)!.downloadRegionButton), style: ElevatedButton.styleFrom( minimumSize: const Size.fromHeight(48), ), ), const SizedBox(height: 8), Text( AppLocalizations.of(context)!.downloadNote, style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), ], ), ), ); } Widget _buildActionsCard() { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( AppLocalizations.of(context)!.cacheManagement, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 16), // Clear Cache Button OutlinedButton.icon( onPressed: _isDownloading ? null : _clearCache, icon: const Icon(Icons.delete_forever), label: Text(AppLocalizations.of(context)!.clearAllMaps), style: OutlinedButton.styleFrom( foregroundColor: Colors.red, minimumSize: const Size.fromHeight(48), ), ), ], ), ), ); } }