From 1f826ae4c26aa289137dbde2a38c5aa201fa3d83 Mon Sep 17 00:00:00 2001 From: Janez T Date: Sun, 8 Mar 2026 09:19:13 +0100 Subject: [PATCH] Set simple mode default --- lib/providers/app_provider.dart | 35 +- lib/screens/map_tab.dart | 1827 +++++++++++------------- lib/screens/messages_tab.dart | 15 +- lib/screens/settings_screen.dart | 13 - lib/screens/welcome_wizard_screen.dart | 137 +- lib/widgets/contacts/contact_tile.dart | 207 +-- lib/widgets/map/drawing_layer.dart | 89 +- lib/widgets/map/trail_controls.dart | 513 +++---- 8 files changed, 1133 insertions(+), 1703 deletions(-) diff --git a/lib/providers/app_provider.dart b/lib/providers/app_provider.dart index f736f25..f0ac730 100644 --- a/lib/providers/app_provider.dart +++ b/lib/providers/app_provider.dart @@ -43,8 +43,7 @@ class AppProvider with ChangeNotifier { bool _isInitialized = false; bool get isInitialized => _isInitialized; - bool _isSimpleMode = true; - bool get isSimpleMode => _isSimpleMode; + bool get isSimpleMode => true; bool _isMapEnabled = true; bool get isMapEnabled => _isMapEnabled; @@ -94,7 +93,6 @@ class AppProvider with ChangeNotifier { }) { _setupCallbacks(); _initializeLocationTracking(); - _loadSimpleMode(); _loadMapEnabled(); _loadContactsEnabled(); _loadSensorsEnabled(); @@ -223,29 +221,6 @@ class AppProvider with ChangeNotifier { return contact?.advName; } - /// Load simple mode setting from shared preferences - Future _loadSimpleMode() async { - try { - final prefs = await SharedPreferences.getInstance(); - _isSimpleMode = prefs.getBool('simple_mode') ?? true; - notifyListeners(); - } catch (e) { - debugPrint('Error loading simple mode setting: $e'); - } - } - - /// Toggle simple mode on/off - Future toggleSimpleMode(bool enabled) async { - try { - _isSimpleMode = enabled; - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool('simple_mode', enabled); - notifyListeners(); - } catch (e) { - debugPrint('Error saving simple mode setting: $e'); - } - } - /// Load map enabled setting from shared preferences Future _loadMapEnabled() async { try { @@ -1214,10 +1189,8 @@ class AppProvider with ChangeNotifier { // Sync channels to get channel names // In simple mode: only sync first 5 channels for faster startup // In normal mode: sync all channels (up to device max) - final channelsToSync = _isSimpleMode ? 5 : null; - debugPrint( - '📻 [AppProvider] Syncing channels${_isSimpleMode ? ' (simple mode: max 5)' : ''}...', - ); + const channelsToSync = 5; + debugPrint('📻 [AppProvider] Syncing channels (simple mode: max 5)...'); await connectionProvider.syncChannels(maxChannels: channelsToSync); debugPrint('✅ [AppProvider] Channel sync complete'); @@ -2220,7 +2193,7 @@ class AppProvider with ChangeNotifier { await connectionProvider.getContacts(); // Sync channels (respect simple mode settings) - final channelsToSync = _isSimpleMode ? 5 : null; + const channelsToSync = 5; await connectionProvider.syncChannels(maxChannels: channelsToSync); // Messages are automatically synced via PUSH_CODE_MSG_WAITING events diff --git a/lib/screens/map_tab.dart b/lib/screens/map_tab.dart index 3bba388..7a9eaa1 100644 --- a/lib/screens/map_tab.dart +++ b/lib/screens/map_tab.dart @@ -27,7 +27,6 @@ import '../widgets/map_debug_info.dart'; import '../widgets/map/compass_widget.dart'; import '../widgets/map/detailed_compass_dialog.dart'; import '../widgets/map/drawing_layer.dart'; -import '../widgets/map/drawing_toolbar.dart'; import '../widgets/map/location_trail_layer.dart'; import '../widgets/map/trail_controls.dart'; import '../widgets/map/map_message_overlay.dart'; @@ -708,78 +707,6 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { ); } - void _showOptionsMenu(BuildContext context) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => StatefulBuilder( - builder: (context, setModalState) => Container( - padding: const EdgeInsets.symmetric(vertical: 16), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - child: Row( - children: [ - const Icon(Icons.settings), - const SizedBox(width: 12), - Text( - AppLocalizations.of(context)!.mapOptions, - style: Theme.of(context).textTheme.titleMedium - ?.copyWith(fontWeight: FontWeight.bold), - ), - ], - ), - ), - const Divider(), - // Map Debug Info toggle - SwitchListTile( - secondary: const Icon(Icons.developer_mode), - title: Text(AppLocalizations.of(context)!.showMapDebugInfo), - subtitle: Text( - AppLocalizations.of(context)!.displayZoomLevelBounds, - ), - value: _showMapDebugInfo, - onChanged: (value) { - setState(() { - _showMapDebugInfo = value; - }); - setModalState(() {}); - _saveSettings(); - }, - ), - const Divider(), - // Fullscreen mode toggle - SwitchListTile( - secondary: const Icon(Icons.fullscreen), - title: Text(AppLocalizations.of(context)!.fullscreenMode), - subtitle: Text( - AppLocalizations.of(context)!.hideUiFullMapView, - ), - value: _isFullscreen, - onChanged: (value) { - setState(() { - _isFullscreen = value; - }); - setModalState(() {}); - _saveSettings(); - // Notify parent about fullscreen change - widget.onFullscreenChanged?.call(value); - }, - ), - ], - ), - ), - ), - ), - ); - } - void _showDetailedCompass( BuildContext context, List contacts, @@ -1190,8 +1117,6 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); // Required for AutomaticKeepAliveClientMixin - final appProvider = context.watch(); - final isSimpleMode = appProvider.isSimpleMode; return Consumer3( builder: (context, contactsProvider, messagesProvider, drawingProvider, child) { @@ -1207,879 +1132,835 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { children: [ // Map widget Listener( - onPointerMove: (PointerMoveEvent event) { - // Track pointer movement for mobile drag (onPointerHover doesn't work on mobile) - if (_isDraggingPin) { - final latLng = _mapController.camera - .screenOffsetToLatLng(event.localPosition); + onPointerMove: (PointerMoveEvent event) { + // Track pointer movement for mobile drag (onPointerHover doesn't work on mobile) + if (_isDraggingPin) { + final latLng = _mapController.camera.screenOffsetToLatLng( + event.localPosition, + ); + setState(() { + _droppedPinLocation = latLng; + }); + } + }, + child: FlutterMap( + mapController: _mapController, + options: MapOptions( + // Use the layer's CRS if it has one (for WMS layers), otherwise default to EPSG:3857 + crs: _currentLayer.crs ?? const Epsg3857(), + // Use saved position if available, otherwise use calculated center + initialCenter: _savedMapCenter ?? center, + initialZoom: _savedMapZoom ?? _defaultZoom, + minZoom: 0, // Allow full zoom out to see world view + maxZoom: + _currentLayer.maxZoom, // Respect current layer's maximum + interactionOptions: InteractionOptions( + flags: _isDraggingPin + ? InteractiveFlag + .none // Disable map interaction while dragging pin + : InteractiveFlag.all, + ), + onMapEvent: (event) { + // Save map position when user stops panning/zooming + if (event is MapEventMoveEnd || + event is MapEventScrollWheelZoom) { + _saveMapPosition(); + } + // Trigger rebuild on rotation change to show/hide reset button + if (event is MapEventRotateEnd || + event is MapEventRotateStart) { + setState(() {}); + } + }, + onLongPress: (tapPosition, point) { + // Handle measurement mode - set measurement points only, no SAR marker + if (drawingProvider.drawingMode == DrawingMode.measure) { + if (drawingProvider.measurementPoint1 == null) { + // Set first measurement point + drawingProvider.setMeasurementPoint1(point); + } else if (drawingProvider.measurementPoint2 == null) { + // Set second measurement point + drawingProvider.setMeasurementPoint2(point); + } else { + // Clear and start new measurement + drawingProvider.clearMeasurement(); + drawingProvider.setMeasurementPoint1(point); + } + // Don't drop SAR marker pin in measurement mode + return; + } + + // Skip if in other drawing modes + if (drawingProvider.isDrawing) return; + + // Drop a pin at long press location for SAR marker creation + if (_droppedPinLocation == null) { + setState(() { + _droppedPinLocation = point; + }); + } + }, + onPointerDown: (event, point) { + // Check if pointer is near the pin to start dragging + if (_droppedPinLocation != null) { + final distance = _calculateDistanceInMeters( + _droppedPinLocation!.latitude, + _droppedPinLocation!.longitude, + point.latitude, + point.longitude, + ); + // If within ~50m of pin, start dragging + if (distance <= 50) { setState(() { - _droppedPinLocation = latLng; + _isDraggingPin = true; }); } - }, - child: FlutterMap( - mapController: _mapController, - options: MapOptions( - // Use the layer's CRS if it has one (for WMS layers), otherwise default to EPSG:3857 - crs: _currentLayer.crs ?? const Epsg3857(), - // Use saved position if available, otherwise use calculated center - initialCenter: _savedMapCenter ?? center, - initialZoom: _savedMapZoom ?? _defaultZoom, - minZoom: 0, // Allow full zoom out to see world view - maxZoom: _currentLayer - .maxZoom, // Respect current layer's maximum - interactionOptions: InteractionOptions( - flags: _isDraggingPin - ? InteractiveFlag - .none // Disable map interaction while dragging pin - : InteractiveFlag.all, + } + }, + onPointerHover: (event, point) { + // Update rectangle preview while dragging + if (drawingProvider.drawingMode == DrawingMode.rectangle && + drawingProvider.rectangleStartPoint != null) { + drawingProvider.updateRectangleEndPoint(point); + return; + } + + // Update pin location while dragging + if (_isDraggingPin) { + setState(() { + _droppedPinLocation = point; + }); + } + }, + onPointerUp: (event, point) { + // Stop dragging on pointer release + if (_isDraggingPin) { + setState(() { + _isDraggingPin = false; + }); + } + }, + onTap: (tapPosition, point) { + // Handle drawing mode taps + if (drawingProvider.drawingMode == DrawingMode.line) { + if (drawingProvider.currentLinePoints.isEmpty) { + // Start new line + drawingProvider.startLine(point); + } else { + // Add point to current line + drawingProvider.addLinePoint(point); + } + return; + } else if (drawingProvider.drawingMode == + DrawingMode.rectangle) { + if (drawingProvider.rectangleStartPoint == null) { + // Start rectangle + drawingProvider.startRectangle(point); + } else { + // Complete rectangle + drawingProvider.completeRectangle(point); + } + return; + } + + // Clear dropped pin if tapping elsewhere (not on the pin itself) + if (_droppedPinLocation != null && !_isDraggingPin) { + // Check if tap is far from the pin + final distance = _calculateDistanceInMeters( + _droppedPinLocation!.latitude, + _droppedPinLocation!.longitude, + point.latitude, + point.longitude, + ); + // If tap is more than ~50m away, clear pin + if (distance > 50) { + setState(() { + _droppedPinLocation = null; + }); + } + } + }, + ), + children: [ + // Render raster or WMS tile layer based on layer type + if (_currentLayer.isWms && + _currentLayer.wmsBaseUrl != null && + _currentLayer.crs != null) + // WMS Base Layer (e.g., Slovenian Aerial Imagery) + flutter_map.TileLayer( + wmsOptions: WMSTileLayerOptions( + baseUrl: _currentLayer.wmsBaseUrl!, + layers: _currentLayer.wmsLayers ?? [], + styles: _currentLayer.wmsStyles ?? [], + format: _currentLayer.wmsFormat ?? 'image/jpeg', + transparent: _currentLayer.wmsTransparent ?? false, + crs: _currentLayer.crs!, + ), + // Use cached tile provider for offline support + tileProvider: _tileProvider, + userAgentPackageName: 'com.meshcore.sar', + maxZoom: _currentLayer.maxZoom, + errorTileCallback: (tile, error, stackTrace) { + debugPrint( + '🔴 WMS Base Layer tile error at ${tile.coordinates}: $error', + ); + }, + ) + else if (!_currentLayer.isWms) + flutter_map.TileLayer( + urlTemplate: _currentLayer.urlTemplate, + tileProvider: _tileProvider, + userAgentPackageName: 'com.meshcore.sar', + maxZoom: _currentLayer.maxZoom, + ), + // WMS Overlays (rendered after base layer, before polylines) + // Note: These overlays only work with EPSG:3794 CRS (Slovenian coordinate system) + // Cadastral parcels overlay + Consumer( + builder: (context, mapProvider, _) { + // Only show if enabled and map is using Slovenian CRS + if (!mapProvider.showCadastralOverlay || + _currentLayer.crs == null) { + return const SizedBox.shrink(); + } + return flutter_map.TileLayer( + wmsOptions: WMSTileLayerOptions( + baseUrl: + 'https://prostor.zgs.gov.si/geowebcache/service/wms?', + layers: const ['pregledovalnik:kn_parcele'], + styles: const ['parcele'], + format: 'image/png', + transparent: true, + crs: slovenianCrs, ), - onMapEvent: (event) { - // Save map position when user stops panning/zooming - if (event is MapEventMoveEnd || - event is MapEventScrollWheelZoom) { - _saveMapPosition(); - } - // Trigger rebuild on rotation change to show/hide reset button - if (event is MapEventRotateEnd || - event is MapEventRotateStart) { - setState(() {}); + tileProvider: _tileProvider, + userAgentPackageName: 'com.meshcore.sar', + maxZoom: 19, + errorTileCallback: (tile, error, stackTrace) { + debugPrint( + '🔴 Cadastral overlay tile error at ${tile.coordinates}: $error', + ); + if (stackTrace != null) { + debugPrint(' StackTrace: $stackTrace'); } }, - onLongPress: (tapPosition, point) { - // Handle measurement mode - set measurement points only, no SAR marker - if (drawingProvider.drawingMode == - DrawingMode.measure) { - if (drawingProvider.measurementPoint1 == null) { - // Set first measurement point - drawingProvider.setMeasurementPoint1(point); - } else if (drawingProvider.measurementPoint2 == - null) { - // Set second measurement point - drawingProvider.setMeasurementPoint2(point); - } else { - // Clear and start new measurement - drawingProvider.clearMeasurement(); - drawingProvider.setMeasurementPoint1(point); - } - // Don't drop SAR marker pin in measurement mode - return; + ); + }, + ), + // Forest roads overlay + Consumer( + builder: (context, mapProvider, _) { + // Only show if enabled and map is using Slovenian CRS + if (!mapProvider.showForestRoadsOverlay || + _currentLayer.crs == null) { + return const SizedBox.shrink(); + } + return flutter_map.TileLayer( + wmsOptions: WMSTileLayerOptions( + baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?', + layers: const ['pregledovalnik:gozdne_ceste'], + styles: const ['gozdne_ceste'], + format: 'image/png', + transparent: true, + crs: slovenianCrs, + ), + tileProvider: _tileProvider, + userAgentPackageName: 'com.meshcore.sar', + maxZoom: 19, + errorTileCallback: (tile, error, stackTrace) { + debugPrint( + '🔴 Forest roads overlay tile error at ${tile.coordinates}: $error', + ); + if (stackTrace != null) { + debugPrint(' StackTrace: $stackTrace'); } + }, + ); + }, + ), + // Hiking trails overlay + Consumer( + builder: (context, mapProvider, _) { + if (!mapProvider.showHikingTrailsOverlay || + _currentLayer.crs == null) { + return const SizedBox.shrink(); + } + return flutter_map.TileLayer( + wmsOptions: WMSTileLayerOptions( + baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?', + layers: const [ + 'pregledovalnik:KGI_LINIJE_PLANINSKE_POTI_G', + ], + format: 'image/png', + transparent: true, + crs: slovenianCrs, + ), + tileProvider: _tileProvider, + userAgentPackageName: 'com.meshcore.sar', + maxZoom: 19, + errorTileCallback: (tile, error, stackTrace) { + debugPrint( + '🔴 Hiking trails overlay tile error at ${tile.coordinates}: $error', + ); + }, + ); + }, + ), + // Main roads overlay + Consumer( + builder: (context, mapProvider, _) { + if (!mapProvider.showMainRoadsOverlay || + _currentLayer.crs == null) { + return const SizedBox.shrink(); + } + return flutter_map.TileLayer( + wmsOptions: WMSTileLayerOptions( + baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?', + layers: const ['pregledovalnik:KGI_LINIJE_CESTE_G'], + format: 'image/png', + transparent: true, + crs: slovenianCrs, + ), + tileProvider: _tileProvider, + userAgentPackageName: 'com.meshcore.sar', + maxZoom: 19, + errorTileCallback: (tile, error, stackTrace) { + debugPrint( + '🔴 Main roads overlay tile error at ${tile.coordinates}: $error', + ); + }, + ); + }, + ), + // House numbers overlay + Consumer( + builder: (context, mapProvider, _) { + if (!mapProvider.showHouseNumbersOverlay || + _currentLayer.crs == null) { + return const SizedBox.shrink(); + } + return flutter_map.TileLayer( + wmsOptions: WMSTileLayerOptions( + baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?', + layers: const ['pregledovalnik:NEP_HISNE_STEVILKE'], + format: 'image/png', + transparent: true, + crs: slovenianCrs, + ), + tileProvider: _tileProvider, + userAgentPackageName: 'com.meshcore.sar', + maxZoom: 19, + errorTileCallback: (tile, error, stackTrace) { + debugPrint( + '🔴 House numbers overlay tile error at ${tile.coordinates}: $error', + ); + }, + ); + }, + ), + // Fire hazard zones overlay + Consumer( + builder: (context, mapProvider, _) { + if (!mapProvider.showFireHazardZonesOverlay || + _currentLayer.crs == null) { + return const SizedBox.shrink(); + } + return flutter_map.TileLayer( + wmsOptions: WMSTileLayerOptions( + baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?', + layers: const ['pregledovalnik:pozarna_ogrozenost'], + format: 'image/png', + transparent: true, + crs: slovenianCrs, + ), + tileProvider: _tileProvider, + userAgentPackageName: 'com.meshcore.sar', + maxZoom: 19, + errorTileCallback: (tile, error, stackTrace) { + debugPrint( + '🔴 Fire hazard zones overlay tile error at ${tile.coordinates}: $error', + ); + }, + ); + }, + ), + // Historical fires overlay + Consumer( + builder: (context, mapProvider, _) { + if (!mapProvider.showHistoricalFiresOverlay || + _currentLayer.crs == null) { + return const SizedBox.shrink(); + } + return flutter_map.TileLayer( + wmsOptions: WMSTileLayerOptions( + baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?', + layers: const ['pregledovalnik:gozdni_pozari'], + format: 'image/png', + transparent: true, + crs: slovenianCrs, + ), + tileProvider: _tileProvider, + userAgentPackageName: 'com.meshcore.sar', + maxZoom: 19, + errorTileCallback: (tile, error, stackTrace) { + debugPrint( + '🔴 Historical fires overlay tile error at ${tile.coordinates}: $error', + ); + }, + ); + }, + ), + // Firebreaks overlay + Consumer( + builder: (context, mapProvider, _) { + if (!mapProvider.showFirebreaksOverlay || + _currentLayer.crs == null) { + return const SizedBox.shrink(); + } + return flutter_map.TileLayer( + wmsOptions: WMSTileLayerOptions( + baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?', + layers: const ['pregledovalnik:protipozarne_preseke'], + format: 'image/png', + transparent: true, + crs: slovenianCrs, + ), + tileProvider: _tileProvider, + userAgentPackageName: 'com.meshcore.sar', + maxZoom: 19, + errorTileCallback: (tile, error, stackTrace) { + debugPrint( + '🔴 Firebreaks overlay tile error at ${tile.coordinates}: $error', + ); + }, + ); + }, + ), + // Kras fire zones overlay + Consumer( + builder: (context, mapProvider, _) { + if (!mapProvider.showKrasFireZonesOverlay || + _currentLayer.crs == null) { + return const SizedBox.shrink(); + } + return flutter_map.TileLayer( + wmsOptions: WMSTileLayerOptions( + baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?', + layers: const ['pregledovalnik:pozarisce_kras'], + format: 'image/png', + transparent: true, + crs: slovenianCrs, + ), + tileProvider: _tileProvider, + userAgentPackageName: 'com.meshcore.sar', + maxZoom: 19, + errorTileCallback: (tile, error, stackTrace) { + debugPrint( + '🔴 Kras fire zones overlay tile error at ${tile.coordinates}: $error', + ); + }, + ); + }, + ), + // Place names overlay + Consumer( + builder: (context, mapProvider, _) { + if (!mapProvider.showPlaceNamesOverlay || + _currentLayer.crs == null) { + return const SizedBox.shrink(); + } + return flutter_map.TileLayer( + wmsOptions: WMSTileLayerOptions( + baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?', + layers: const ['pregledovalnik:zemljepisna_imena'], + format: 'image/png', + transparent: true, + crs: slovenianCrs, + ), + tileProvider: _tileProvider, + userAgentPackageName: 'com.meshcore.sar', + maxZoom: 19, + errorTileCallback: (tile, error, stackTrace) { + debugPrint( + '🔴 Place names overlay tile error at ${tile.coordinates}: $error', + ); + }, + ); + }, + ), + // Municipality borders overlay + Consumer( + builder: (context, mapProvider, _) { + if (!mapProvider.showMunicipalityBordersOverlay || + _currentLayer.crs == null) { + return const SizedBox.shrink(); + } + return flutter_map.TileLayer( + wmsOptions: WMSTileLayerOptions( + baseUrl: 'https://prostor.zgs.gov.si/geoserver/wms?', + layers: const ['pregledovalnik:NEP_RPE_OBCINE'], + styles: const ['obcine'], + format: 'image/png', + transparent: true, + crs: slovenianCrs, + ), + tileProvider: _tileProvider, + userAgentPackageName: 'com.meshcore.sar', + maxZoom: 19, + errorTileCallback: (tile, error, stackTrace) { + debugPrint( + '🔴 Municipality borders overlay tile error at ${tile.coordinates}: $error', + ); + }, + ); + }, + ), + // Imported trail layer (rendered at bottom for reference) + Consumer( + builder: (context, mapProvider, _) { + if (mapProvider.importedTrail == null || + mapProvider.importedTrail!.points.length < 2) { + return const SizedBox.shrink(); + } - // Skip if in other drawing modes - if (drawingProvider.isDrawing) return; + return PolylineLayer( + polylines: [ + Polyline( + points: mapProvider.importedTrail!.latLngPoints, + color: Colors.green.withValues(alpha: 0.7), + strokeWidth: 3.0, + borderColor: Colors.white.withValues(alpha: 0.4), + borderStrokeWidth: 1.0, + // DOTTED pattern to distinguish from other trails + pattern: StrokePattern.dotted(spacingFactor: 2), + ), + ], + ); + }, + ), + // Contact trail polylines (rendered before user trail and markers) + Consumer( + builder: (context, mapProvider, _) { + // Determine which contacts to show trails for + final contactsToShow = mapProvider.showAllContactTrails + ? contactsWithLocation // Show all when master toggle is ON + : contactsWithLocation.where( + (contact) => mapProvider.isContactPathVisible( + contact.publicKeyHex, + ), + ); // Individual toggles - // Drop a pin at long press location for SAR marker creation - if (_droppedPinLocation == null) { - setState(() { - _droppedPinLocation = point; - }); - } - }, - onPointerDown: (event, point) { - // Check if pointer is near the pin to start dragging - if (_droppedPinLocation != null) { - final distance = _calculateDistanceInMeters( - _droppedPinLocation!.latitude, - _droppedPinLocation!.longitude, - point.latitude, - point.longitude, - ); - // If within ~50m of pin, start dragging - if (distance <= 50) { - setState(() { - _isDraggingPin = true; - }); - } - } - }, - onPointerHover: (event, point) { - // Update rectangle preview while dragging - if (drawingProvider.drawingMode == - DrawingMode.rectangle && - drawingProvider.rectangleStartPoint != null) { - drawingProvider.updateRectangleEndPoint(point); - return; - } + return PolylineLayer( + polylines: contactsToShow + .where( + (contact) => contact.advertHistory.length >= 2, + ) + .map((contact) { + // Use TrailColorService for consistent, emoji-based colors + final color = TrailColorService.getTrailColor( + contact, + ); - // Update pin location while dragging - if (_isDraggingPin) { - setState(() { - _droppedPinLocation = point; - }); - } - }, - onPointerUp: (event, point) { - // Stop dragging on pointer release - if (_isDraggingPin) { - setState(() { - _isDraggingPin = false; - }); - } - }, - onTap: (tapPosition, point) { - // Handle drawing mode taps - if (drawingProvider.drawingMode == DrawingMode.line) { - if (drawingProvider.currentLinePoints.isEmpty) { - // Start new line - drawingProvider.startLine(point); - } else { - // Add point to current line - drawingProvider.addLinePoint(point); - } - return; - } else if (drawingProvider.drawingMode == - DrawingMode.rectangle) { - if (drawingProvider.rectangleStartPoint == null) { - // Start rectangle - drawingProvider.startRectangle(point); - } else { - // Complete rectangle - drawingProvider.completeRectangle(point); - } - return; - } - - // Clear dropped pin if tapping elsewhere (not on the pin itself) - if (_droppedPinLocation != null && !_isDraggingPin) { - // Check if tap is far from the pin - final distance = _calculateDistanceInMeters( - _droppedPinLocation!.latitude, - _droppedPinLocation!.longitude, - point.latitude, - point.longitude, - ); - // If tap is more than ~50m away, clear pin - if (distance > 50) { - setState(() { - _droppedPinLocation = null; - }); - } - } + return Polyline( + points: contact.advertHistory + .map((advert) => advert.location) + .toList(), + color: color.withValues( + alpha: 0.95, + ), // More opaque for better visibility + strokeWidth: + 4.5, // Thicker for better visibility on all map backgrounds + borderColor: Colors.white.withValues( + alpha: 0.6, + ), // Stronger border contrast + borderStrokeWidth: 2.0, // Wider border + // DASHED pattern to distinguish from solid user trail + pattern: StrokePattern.dashed(segments: [8, 4]), + ); + }) + .toList(), + ); + }, + ), + // Location trail layer (rendered after paths, before drawings) + const LocationTrailLayer(), + // Measurement line layer (rendered before drawings) + if (drawingProvider.measurementPoint1 != null && + drawingProvider.measurementPoint2 != null) + PolylineLayer( + polylines: [ + Polyline( + points: [ + drawingProvider.measurementPoint1!, + drawingProvider.measurementPoint2!, + ], + color: Colors.yellow.withValues(alpha: 0.8), + strokeWidth: 3.0, + borderColor: Colors.black.withValues(alpha: 0.5), + borderStrokeWidth: 1.0, + pattern: StrokePattern.dashed(segments: [10, 5]), + ), + ], + ), + // Drawing layer (rendered after paths, before markers) + DrawingLayer( + drawings: drawingProvider.drawings, + previewDrawing: drawingProvider.getPreviewDrawing(), + ), + MarkerLayer( + markers: [ + // Contact markers + ..._markerService.generateContactMarkers( + contacts: contactsWithLocation, + context: context, + mapRotation: _getMapRotation(), + userPosition: _locationService.currentPosition, + onTap: (contact) { + _showDetailedCompassWithContact( + context, + contactsProvider.contactsWithLocation, + messagesProvider.sarMarkers, + contact, + ); }, ), - children: [ - // Render raster or WMS tile layer based on layer type - if (_currentLayer.isWms && - _currentLayer.wmsBaseUrl != null && - _currentLayer.crs != null) - // WMS Base Layer (e.g., Slovenian Aerial Imagery) - flutter_map.TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: _currentLayer.wmsBaseUrl!, - layers: _currentLayer.wmsLayers ?? [], - styles: _currentLayer.wmsStyles ?? [], - format: _currentLayer.wmsFormat ?? 'image/jpeg', - transparent: - _currentLayer.wmsTransparent ?? false, - crs: _currentLayer.crs!, - ), - // Use cached tile provider for offline support - tileProvider: _tileProvider, - userAgentPackageName: 'com.meshcore.sar', - maxZoom: _currentLayer.maxZoom, - errorTileCallback: (tile, error, stackTrace) { - debugPrint( - '🔴 WMS Base Layer tile error at ${tile.coordinates}: $error', - ); - }, - ) - else if (!_currentLayer.isWms) - flutter_map.TileLayer( - urlTemplate: _currentLayer.urlTemplate, - tileProvider: _tileProvider, - userAgentPackageName: 'com.meshcore.sar', - maxZoom: _currentLayer.maxZoom, - ), - // WMS Overlays (rendered after base layer, before polylines) - // Note: These overlays only work with EPSG:3794 CRS (Slovenian coordinate system) - // Cadastral parcels overlay - Consumer( - builder: (context, mapProvider, _) { - // Only show if enabled and map is using Slovenian CRS - if (!mapProvider.showCadastralOverlay || - _currentLayer.crs == null) { - return const SizedBox.shrink(); - } - return flutter_map.TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: - 'https://prostor.zgs.gov.si/geowebcache/service/wms?', - layers: const ['pregledovalnik:kn_parcele'], - styles: const ['parcele'], - format: 'image/png', - transparent: true, - crs: slovenianCrs, - ), - tileProvider: _tileProvider, - userAgentPackageName: 'com.meshcore.sar', - maxZoom: 19, - errorTileCallback: (tile, error, stackTrace) { - debugPrint( - '🔴 Cadastral overlay tile error at ${tile.coordinates}: $error', - ); - if (stackTrace != null) { - debugPrint(' StackTrace: $stackTrace'); - } - }, - ); - }, - ), - // Forest roads overlay - Consumer( - builder: (context, mapProvider, _) { - // Only show if enabled and map is using Slovenian CRS - if (!mapProvider.showForestRoadsOverlay || - _currentLayer.crs == null) { - return const SizedBox.shrink(); - } - return flutter_map.TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: - 'https://prostor.zgs.gov.si/geoserver/wms?', - layers: const ['pregledovalnik:gozdne_ceste'], - styles: const ['gozdne_ceste'], - format: 'image/png', - transparent: true, - crs: slovenianCrs, - ), - tileProvider: _tileProvider, - userAgentPackageName: 'com.meshcore.sar', - maxZoom: 19, - errorTileCallback: (tile, error, stackTrace) { - debugPrint( - '🔴 Forest roads overlay tile error at ${tile.coordinates}: $error', - ); - if (stackTrace != null) { - debugPrint(' StackTrace: $stackTrace'); - } - }, - ); - }, - ), - // Hiking trails overlay - Consumer( - builder: (context, mapProvider, _) { - if (!mapProvider.showHikingTrailsOverlay || - _currentLayer.crs == null) { - return const SizedBox.shrink(); - } - return flutter_map.TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: - 'https://prostor.zgs.gov.si/geoserver/wms?', - layers: const [ - 'pregledovalnik:KGI_LINIJE_PLANINSKE_POTI_G', - ], - format: 'image/png', - transparent: true, - crs: slovenianCrs, - ), - tileProvider: _tileProvider, - userAgentPackageName: 'com.meshcore.sar', - maxZoom: 19, - errorTileCallback: (tile, error, stackTrace) { - debugPrint( - '🔴 Hiking trails overlay tile error at ${tile.coordinates}: $error', - ); - }, - ); - }, - ), - // Main roads overlay - Consumer( - builder: (context, mapProvider, _) { - if (!mapProvider.showMainRoadsOverlay || - _currentLayer.crs == null) { - return const SizedBox.shrink(); - } - return flutter_map.TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: - 'https://prostor.zgs.gov.si/geoserver/wms?', - layers: const [ - 'pregledovalnik:KGI_LINIJE_CESTE_G', - ], - format: 'image/png', - transparent: true, - crs: slovenianCrs, - ), - tileProvider: _tileProvider, - userAgentPackageName: 'com.meshcore.sar', - maxZoom: 19, - errorTileCallback: (tile, error, stackTrace) { - debugPrint( - '🔴 Main roads overlay tile error at ${tile.coordinates}: $error', - ); - }, - ); - }, - ), - // House numbers overlay - Consumer( - builder: (context, mapProvider, _) { - if (!mapProvider.showHouseNumbersOverlay || - _currentLayer.crs == null) { - return const SizedBox.shrink(); - } - return flutter_map.TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: - 'https://prostor.zgs.gov.si/geoserver/wms?', - layers: const [ - 'pregledovalnik:NEP_HISNE_STEVILKE', - ], - format: 'image/png', - transparent: true, - crs: slovenianCrs, - ), - tileProvider: _tileProvider, - userAgentPackageName: 'com.meshcore.sar', - maxZoom: 19, - errorTileCallback: (tile, error, stackTrace) { - debugPrint( - '🔴 House numbers overlay tile error at ${tile.coordinates}: $error', - ); - }, - ); - }, - ), - // Fire hazard zones overlay - Consumer( - builder: (context, mapProvider, _) { - if (!mapProvider.showFireHazardZonesOverlay || - _currentLayer.crs == null) { - return const SizedBox.shrink(); - } - return flutter_map.TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: - 'https://prostor.zgs.gov.si/geoserver/wms?', - layers: const [ - 'pregledovalnik:pozarna_ogrozenost', - ], - format: 'image/png', - transparent: true, - crs: slovenianCrs, - ), - tileProvider: _tileProvider, - userAgentPackageName: 'com.meshcore.sar', - maxZoom: 19, - errorTileCallback: (tile, error, stackTrace) { - debugPrint( - '🔴 Fire hazard zones overlay tile error at ${tile.coordinates}: $error', - ); - }, - ); - }, - ), - // Historical fires overlay - Consumer( - builder: (context, mapProvider, _) { - if (!mapProvider.showHistoricalFiresOverlay || - _currentLayer.crs == null) { - return const SizedBox.shrink(); - } - return flutter_map.TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: - 'https://prostor.zgs.gov.si/geoserver/wms?', - layers: const ['pregledovalnik:gozdni_pozari'], - format: 'image/png', - transparent: true, - crs: slovenianCrs, - ), - tileProvider: _tileProvider, - userAgentPackageName: 'com.meshcore.sar', - maxZoom: 19, - errorTileCallback: (tile, error, stackTrace) { - debugPrint( - '🔴 Historical fires overlay tile error at ${tile.coordinates}: $error', - ); - }, - ); - }, - ), - // Firebreaks overlay - Consumer( - builder: (context, mapProvider, _) { - if (!mapProvider.showFirebreaksOverlay || - _currentLayer.crs == null) { - return const SizedBox.shrink(); - } - return flutter_map.TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: - 'https://prostor.zgs.gov.si/geoserver/wms?', - layers: const [ - 'pregledovalnik:protipozarne_preseke', - ], - format: 'image/png', - transparent: true, - crs: slovenianCrs, - ), - tileProvider: _tileProvider, - userAgentPackageName: 'com.meshcore.sar', - maxZoom: 19, - errorTileCallback: (tile, error, stackTrace) { - debugPrint( - '🔴 Firebreaks overlay tile error at ${tile.coordinates}: $error', - ); - }, - ); - }, - ), - // Kras fire zones overlay - Consumer( - builder: (context, mapProvider, _) { - if (!mapProvider.showKrasFireZonesOverlay || - _currentLayer.crs == null) { - return const SizedBox.shrink(); - } - return flutter_map.TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: - 'https://prostor.zgs.gov.si/geoserver/wms?', - layers: const ['pregledovalnik:pozarisce_kras'], - format: 'image/png', - transparent: true, - crs: slovenianCrs, - ), - tileProvider: _tileProvider, - userAgentPackageName: 'com.meshcore.sar', - maxZoom: 19, - errorTileCallback: (tile, error, stackTrace) { - debugPrint( - '🔴 Kras fire zones overlay tile error at ${tile.coordinates}: $error', - ); - }, - ); - }, - ), - // Place names overlay - Consumer( - builder: (context, mapProvider, _) { - if (!mapProvider.showPlaceNamesOverlay || - _currentLayer.crs == null) { - return const SizedBox.shrink(); - } - return flutter_map.TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: - 'https://prostor.zgs.gov.si/geoserver/wms?', - layers: const [ - 'pregledovalnik:zemljepisna_imena', - ], - format: 'image/png', - transparent: true, - crs: slovenianCrs, - ), - tileProvider: _tileProvider, - userAgentPackageName: 'com.meshcore.sar', - maxZoom: 19, - errorTileCallback: (tile, error, stackTrace) { - debugPrint( - '🔴 Place names overlay tile error at ${tile.coordinates}: $error', - ); - }, - ); - }, - ), - // Municipality borders overlay - Consumer( - builder: (context, mapProvider, _) { - if (!mapProvider.showMunicipalityBordersOverlay || - _currentLayer.crs == null) { - return const SizedBox.shrink(); - } - return flutter_map.TileLayer( - wmsOptions: WMSTileLayerOptions( - baseUrl: - 'https://prostor.zgs.gov.si/geoserver/wms?', - layers: const ['pregledovalnik:NEP_RPE_OBCINE'], - styles: const ['obcine'], - format: 'image/png', - transparent: true, - crs: slovenianCrs, - ), - tileProvider: _tileProvider, - userAgentPackageName: 'com.meshcore.sar', - maxZoom: 19, - errorTileCallback: (tile, error, stackTrace) { - debugPrint( - '🔴 Municipality borders overlay tile error at ${tile.coordinates}: $error', - ); - }, - ); - }, - ), - // Imported trail layer (rendered at bottom for reference) - Consumer( - builder: (context, mapProvider, _) { - if (mapProvider.importedTrail == null || - mapProvider.importedTrail!.points.length < 2) { - return const SizedBox.shrink(); - } - - return PolylineLayer( - polylines: [ - Polyline( - points: - mapProvider.importedTrail!.latLngPoints, - color: Colors.green.withValues(alpha: 0.7), - strokeWidth: 3.0, - borderColor: Colors.white.withValues( - alpha: 0.4, - ), - borderStrokeWidth: 1.0, - // DOTTED pattern to distinguish from other trails - pattern: StrokePattern.dotted( - spacingFactor: 2, + // SAR markers + ..._markerService.generateSarMarkers( + sarMarkers: sarMarkers, + context: context, + mapRotation: _getMapRotation(), + onTap: (marker) { + // Navigate to the corresponding message in Messages tab + messagesProvider.navigateToMessage(marker.id); + widget.onNavigateToMessages?.call(); + }, + ), + // User location marker with directional pointer + if (_markerService.generateUserLocationMarker( + position: _locationService.currentPosition, + heading: _currentHeading, + context: context, + ) != + null) + _markerService.generateUserLocationMarker( + position: _locationService.currentPosition, + heading: _currentHeading, + context: context, + )!, + // Measurement point 1 marker + if (drawingProvider.measurementPoint1 != null) + Marker( + point: drawingProvider.measurementPoint1!, + width: 60, + height: 80, + rotate: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.yellow.shade700, + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + 'Start', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, ), ), - ], - ); - }, - ), - // Contact trail polylines (rendered before user trail and markers) - Consumer( - builder: (context, mapProvider, _) { - // Determine which contacts to show trails for - final contactsToShow = - mapProvider.showAllContactTrails - ? contactsWithLocation // Show all when master toggle is ON - : contactsWithLocation.where( - (contact) => - mapProvider.isContactPathVisible( - contact.publicKeyHex, - ), - ); // Individual toggles - - return PolylineLayer( - polylines: contactsToShow - .where( - (contact) => - contact.advertHistory.length >= 2, - ) - .map((contact) { - // Use TrailColorService for consistent, emoji-based colors - final color = - TrailColorService.getTrailColor( - contact, - ); - - return Polyline( - points: contact.advertHistory - .map((advert) => advert.location) - .toList(), - color: color.withValues( - alpha: 0.95, - ), // More opaque for better visibility - strokeWidth: - 4.5, // Thicker for better visibility on all map backgrounds - borderColor: Colors.white.withValues( - alpha: 0.6, - ), // Stronger border contrast - borderStrokeWidth: 2.0, // Wider border - // DASHED pattern to distinguish from solid user trail - pattern: StrokePattern.dashed( - segments: [8, 4], - ), - ); - }) - .toList(), - ); - }, - ), - // Location trail layer (rendered after paths, before drawings) - const LocationTrailLayer(), - // Measurement line layer (rendered before drawings) - if (drawingProvider.measurementPoint1 != null && - drawingProvider.measurementPoint2 != null) - PolylineLayer( - polylines: [ - Polyline( - points: [ - drawingProvider.measurementPoint1!, - drawingProvider.measurementPoint2!, - ], - color: Colors.yellow.withValues(alpha: 0.8), - strokeWidth: 3.0, - borderColor: Colors.black.withValues( - alpha: 0.5, + ), + const SizedBox(height: 2), + Container( + decoration: BoxDecoration( + color: Colors.yellow, + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 2, + ), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 4, + offset: Offset(0, 2), + ), + ], ), - borderStrokeWidth: 1.0, - pattern: StrokePattern.dashed( - segments: [10, 5], + padding: const EdgeInsets.all(6), + child: const Icon( + Icons.location_on, + color: Colors.white, + size: 18, ), ), ], ), - // Drawing layer (rendered after paths, before markers) - DrawingLayer( - drawings: drawingProvider.drawings, - previewDrawing: drawingProvider.getPreviewDrawing(), - isSimpleMode: isSimpleMode, ), - MarkerLayer( - markers: [ - // Contact markers - ..._markerService.generateContactMarkers( - contacts: contactsWithLocation, - context: context, - mapRotation: _getMapRotation(), - userPosition: _locationService.currentPosition, - onTap: (contact) { - _showDetailedCompassWithContact( - context, - contactsProvider.contactsWithLocation, - messagesProvider.sarMarkers, - contact, - ); - }, - ), - // SAR markers - ..._markerService.generateSarMarkers( - sarMarkers: sarMarkers, - context: context, - mapRotation: _getMapRotation(), - onTap: (marker) { - // Navigate to the corresponding message in Messages tab - messagesProvider.navigateToMessage(marker.id); - widget.onNavigateToMessages?.call(); - }, - ), - // User location marker with directional pointer - if (_markerService.generateUserLocationMarker( - position: _locationService.currentPosition, - heading: _currentHeading, - context: context, - ) != - null) - _markerService.generateUserLocationMarker( - position: _locationService.currentPosition, - heading: _currentHeading, - context: context, - )!, - // Measurement point 1 marker - if (drawingProvider.measurementPoint1 != null) - Marker( - point: drawingProvider.measurementPoint1!, - width: 60, - height: 80, - rotate: false, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.yellow.shade700, - borderRadius: BorderRadius.circular(4), - ), - child: const Text( - 'Start', - style: TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(height: 2), - Container( - decoration: BoxDecoration( - color: Colors.yellow, - shape: BoxShape.circle, - border: Border.all( - color: Colors.white, - width: 2, - ), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 4, - offset: Offset(0, 2), - ), - ], - ), - padding: const EdgeInsets.all(6), - child: const Icon( - Icons.location_on, - color: Colors.white, - size: 18, - ), - ), - ], + // Measurement point 2 marker + if (drawingProvider.measurementPoint2 != null) + Marker( + point: drawingProvider.measurementPoint2!, + width: 60, + height: 80, + rotate: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, ), - ), - // Measurement point 2 marker - if (drawingProvider.measurementPoint2 != null) - Marker( - point: drawingProvider.measurementPoint2!, - width: 60, - height: 80, - rotate: false, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.yellow.shade700, - borderRadius: BorderRadius.circular(4), - ), - child: const Text( - 'End', - style: TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(height: 2), - Container( - decoration: BoxDecoration( - color: Colors.yellow, - shape: BoxShape.circle, - border: Border.all( - color: Colors.white, - width: 2, - ), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 4, - offset: Offset(0, 2), - ), - ], - ), - padding: const EdgeInsets.all(6), - child: const Icon( - Icons.location_on, - color: Colors.white, - size: 18, - ), - ), - ], + decoration: BoxDecoration( + color: Colors.yellow.shade700, + borderRadius: BorderRadius.circular(4), ), - ), - // Dropped pin marker with label - if (_droppedPinLocation != null) - Marker( - key: _pinMarkerKey, - point: _droppedPinLocation!, - width: 200, - height: 100, - rotate: false, - child: GestureDetector( - onTap: () { - // Only open dialog if not dragging - if (!_isDraggingPin) { - _showSarDialogWithLocation( - _droppedPinLocation!, - ); - // Clear the pin after opening dialog - setState(() { - _droppedPinLocation = null; - }); - } - }, - child: Opacity( - // Make pin slightly transparent while dragging - opacity: _isDraggingPin ? 0.7 : 1.0, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Label - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: _isDraggingPin - ? Colors.orange - : Colors.red, - borderRadius: BorderRadius.circular( - 8, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues( - alpha: 0.3, - ), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Text( - _isDraggingPin - ? AppLocalizations.of( - context, - )!.dragToPosition - : AppLocalizations.of( - context, - )!.createSarMarker, - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(height: 4), - // Pin icon pointing down - Icon( - Icons.location_pin, - color: _isDraggingPin - ? Colors.orange - : Colors.red, - size: 48, - shadows: const [ - Shadow( - color: Colors.black26, - blurRadius: 4, - offset: Offset(0, 2), - ), - ], - ), - ], - ), + child: const Text( + 'End', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, ), ), ), - ], + const SizedBox(height: 2), + Container( + decoration: BoxDecoration( + color: Colors.yellow, + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 2, + ), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + padding: const EdgeInsets.all(6), + child: const Icon( + Icons.location_on, + color: Colors.white, + size: 18, + ), + ), + ], + ), ), - // Drawing markers layer (delete buttons on drawings, only shown when in drawing mode) - DrawingMarkersLayer( - drawings: drawingProvider.drawings, - showDeleteButtons: drawingProvider.isDrawing, - isSimpleMode: isSimpleMode, - onDeleteDrawing: (drawingId) { - drawingProvider.removeDrawing(drawingId); - }, - onTapDrawing: (drawing) { - // Navigate to the corresponding message in Messages tab - if (drawing.messageId != null) { - messagesProvider.navigateToMessage( - drawing.messageId!, - ); - widget.onNavigateToMessages?.call(); - } - }, + // Dropped pin marker with label + if (_droppedPinLocation != null) + Marker( + key: _pinMarkerKey, + point: _droppedPinLocation!, + width: 200, + height: 100, + rotate: false, + child: GestureDetector( + onTap: () { + // Only open dialog if not dragging + if (!_isDraggingPin) { + _showSarDialogWithLocation( + _droppedPinLocation!, + ); + // Clear the pin after opening dialog + setState(() { + _droppedPinLocation = null; + }); + } + }, + child: Opacity( + // Make pin slightly transparent while dragging + opacity: _isDraggingPin ? 0.7 : 1.0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Label + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: _isDraggingPin + ? Colors.orange + : Colors.red, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues( + alpha: 0.3, + ), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Text( + _isDraggingPin + ? AppLocalizations.of( + context, + )!.dragToPosition + : AppLocalizations.of( + context, + )!.createSarMarker, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 4), + // Pin icon pointing down + Icon( + Icons.location_pin, + color: _isDraggingPin + ? Colors.orange + : Colors.red, + size: 48, + shadows: const [ + Shadow( + color: Colors.black26, + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + ], + ), + ), + ), ), - ], - ), + ], ), + // Drawing markers layer (delete buttons on drawings, only shown when in drawing mode) + DrawingMarkersLayer( + drawings: drawingProvider.drawings, + showDeleteButtons: drawingProvider.isDrawing, + onDeleteDrawing: (drawingId) { + drawingProvider.removeDrawing(drawingId); + }, + onTapDrawing: (drawing) { + // Navigate to the corresponding message in Messages tab + if (drawing.messageId != null) { + messagesProvider.navigateToMessage(drawing.messageId!); + widget.onNavigateToMessages?.call(); + } + }, + ), + ], + ), + ), // Exit fullscreen button - top left (only shown in fullscreen mode) if (_isFullscreen) Positioned( @@ -2250,15 +2131,6 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { right: 16, child: Column( children: [ - // Drawing toolbar (hidden in simple mode) - Consumer( - builder: (context, appProvider, _) { - if (appProvider.isSimpleMode) { - return const SizedBox.shrink(); - } - return const DrawingToolbar(); - }, - ), // Hide other buttons when in drawing mode if (!drawingProvider.isDrawing) ...[ // Current Location - always center to GPS @@ -2351,49 +2223,29 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { ), const SizedBox(height: 8), ], - // In simple mode: show ruler FAB directly - Consumer( - builder: (context, appProvider, _) { - if (!appProvider.isSimpleMode) { - return const SizedBox.shrink(); + FloatingActionButton.small( + heroTag: 'ruler_tool', + backgroundColor: + drawingProvider.drawingMode == DrawingMode.measure + ? Theme.of(context).colorScheme.primary + : null, + onPressed: () { + if (drawingProvider.drawingMode == + DrawingMode.measure) { + drawingProvider.exitDrawingMode(); + } else { + drawingProvider.setDrawingMode(DrawingMode.measure); } - // Show ruler button - return Column( - children: [ - FloatingActionButton.small( - heroTag: 'ruler_tool', - backgroundColor: - drawingProvider.drawingMode == - DrawingMode.measure - ? Theme.of(context).colorScheme.primary - : null, - onPressed: () { - if (drawingProvider.drawingMode == - DrawingMode.measure) { - // Exit measurement mode - drawingProvider.exitDrawingMode(); - } else { - // Enter measurement mode - drawingProvider.setDrawingMode( - DrawingMode.measure, - ); - } - }, - child: Icon( - Icons.straighten, - color: - drawingProvider.drawingMode == - DrawingMode.measure - ? Colors.white - : null, - ), - ), - if (!drawingProvider.isDrawing) - const SizedBox(height: 8), - ], - ); }, + child: Icon( + Icons.straighten, + color: + drawingProvider.drawingMode == DrawingMode.measure + ? Colors.white + : null, + ), ), + if (!drawingProvider.isDrawing) const SizedBox(height: 8), // Continue with other buttons when not in drawing mode if (!drawingProvider.isDrawing) ...[ // Trail controls button @@ -2405,30 +2257,21 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin { child: const Icon(Icons.layers), ), const SizedBox(height: 8), - // In simple mode: show fullscreen button directly - // In normal mode: show options menu (which includes fullscreen) - if (context.watch().isSimpleMode) - FloatingActionButton.small( - heroTag: 'fullscreen_toggle', - onPressed: () { - setState(() { - _isFullscreen = !_isFullscreen; - }); - _saveSettings(); - widget.onFullscreenChanged?.call(_isFullscreen); - }, - child: Icon( - _isFullscreen - ? Icons.fullscreen_exit - : Icons.fullscreen, - ), - ) - else - FloatingActionButton.small( - heroTag: 'options_menu', - onPressed: () => _showOptionsMenu(context), - child: const Icon(Icons.more_vert), + FloatingActionButton.small( + heroTag: 'fullscreen_toggle', + onPressed: () { + setState(() { + _isFullscreen = !_isFullscreen; + }); + _saveSettings(); + widget.onFullscreenChanged?.call(_isFullscreen); + }, + child: Icon( + _isFullscreen + ? Icons.fullscreen_exit + : Icons.fullscreen, ), + ), ], ], ), diff --git a/lib/screens/messages_tab.dart b/lib/screens/messages_tab.dart index 2380efd..b56a087 100644 --- a/lib/screens/messages_tab.dart +++ b/lib/screens/messages_tab.dart @@ -1436,10 +1436,6 @@ class _MessagesTabState extends State { // Get all recent messages final allMessages = messagesProvider.getRecentMessages(count: 100); - // Get simple mode setting from AppProvider - final appProvider = context.read(); - final isSimpleMode = appProvider.isSimpleMode; - List filteredMessages; // If channel destination is selected, filter by selected channel. @@ -1492,14 +1488,9 @@ class _MessagesTabState extends State { filteredMessages = allMessages; } - // In simple mode, filter out system messages (toast logs) - if (isSimpleMode) { - filteredMessages = filteredMessages - .where((message) => !message.isSystemMessage) - .toList(); - } - - return filteredMessages; + return filteredMessages + .where((message) => !message.isSystemMessage) + .toList(); } void _handleMessageTap(Message message) { diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index fc13e87..9440fde 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -864,19 +864,6 @@ class _SettingsScreenState extends State { _buildSectionHeader('Navigation'), _buildSettingsCard([ - Consumer( - builder: (context, appProvider, child) => SwitchListTile( - secondary: const Icon(Icons.visibility_off), - title: Text(AppLocalizations.of(context)!.simpleMode), - subtitle: Text( - AppLocalizations.of(context)!.simpleModeDescription, - ), - value: appProvider.isSimpleMode, - onChanged: (value) async { - await appProvider.toggleSimpleMode(value); - }, - ), - ), Consumer( builder: (context, appProvider, child) => SwitchListTile( secondary: const Icon(Icons.map_outlined), diff --git a/lib/screens/welcome_wizard_screen.dart b/lib/screens/welcome_wizard_screen.dart index a61ea17..7b82b07 100644 --- a/lib/screens/welcome_wizard_screen.dart +++ b/lib/screens/welcome_wizard_screen.dart @@ -15,7 +15,7 @@ class WelcomeWizardScreen extends StatefulWidget { class _WelcomeWizardScreenState extends State { final PageController _pageController = PageController(); int _currentPage = 0; - static const int _totalPages = 6; + static const int _totalPages = 5; @override void dispose() { @@ -99,7 +99,6 @@ class _WelcomeWizardScreenState extends State { children: [ _buildWelcomePage(context, l10n, colorScheme), _buildConnectingPage(context, l10n, colorScheme), - _buildSimpleModePage(context, l10n, colorScheme), _buildChannelPage(context, l10n, colorScheme), _buildContactsPage(context, l10n, colorScheme), _buildMapPage(context, l10n, colorScheme), @@ -182,42 +181,9 @@ class _WelcomeWizardScreenState extends State { title: l10n.wizardConnectingTitle, description: l10n.wizardConnectingDescription, features: [ - _FeatureItem( - icon: Icons.radio, - text: l10n.wizardConnectingFeature1, - ), - _FeatureItem( - icon: Icons.link, - text: l10n.wizardConnectingFeature2, - ), - _FeatureItem( - icon: Icons.wifi_off, - text: l10n.wizardConnectingFeature3, - ), - ], - colorScheme: colorScheme, - ); - } - - Widget _buildSimpleModePage( - BuildContext context, - AppLocalizations l10n, - ColorScheme colorScheme, - ) { - return _buildPage( - icon: Icons.toggle_on, - iconColor: Colors.green, - title: l10n.wizardSimpleModeTitle, - description: l10n.wizardSimpleModeDescription, - features: [ - _FeatureItem( - icon: Icons.check_circle_outline, - text: l10n.wizardSimpleModeFeature1, - ), - _FeatureItem( - icon: Icons.settings, - text: l10n.wizardSimpleModeFeature2, - ), + _FeatureItem(icon: Icons.radio, text: l10n.wizardConnectingFeature1), + _FeatureItem(icon: Icons.link, text: l10n.wizardConnectingFeature2), + _FeatureItem(icon: Icons.wifi_off, text: l10n.wizardConnectingFeature3), ], colorScheme: colorScheme, ); @@ -234,18 +200,9 @@ class _WelcomeWizardScreenState extends State { title: l10n.wizardChannelTitle, description: l10n.wizardChannelDescription, features: [ - _FeatureItem( - icon: Icons.public, - text: l10n.wizardChannelFeature1, - ), - _FeatureItem( - icon: Icons.groups, - text: l10n.wizardChannelFeature2, - ), - _FeatureItem( - icon: Icons.send, - text: l10n.wizardChannelFeature3, - ), + _FeatureItem(icon: Icons.public, text: l10n.wizardChannelFeature1), + _FeatureItem(icon: Icons.groups, text: l10n.wizardChannelFeature2), + _FeatureItem(icon: Icons.send, text: l10n.wizardChannelFeature3), ], colorScheme: colorScheme, ); @@ -262,14 +219,8 @@ class _WelcomeWizardScreenState extends State { title: l10n.wizardContactsTitle, description: l10n.wizardContactsDescription, features: [ - _FeatureItem( - icon: Icons.person_add, - text: l10n.wizardContactsFeature1, - ), - _FeatureItem( - icon: Icons.chat, - text: l10n.wizardContactsFeature2, - ), + _FeatureItem(icon: Icons.person_add, text: l10n.wizardContactsFeature1), + _FeatureItem(icon: Icons.chat, text: l10n.wizardContactsFeature2), _FeatureItem( icon: Icons.battery_std, text: l10n.wizardContactsFeature3, @@ -290,22 +241,13 @@ class _WelcomeWizardScreenState extends State { title: l10n.wizardMapTitle, description: l10n.wizardMapDescription, features: [ - _FeatureItem( - icon: Icons.location_on, - text: l10n.wizardMapFeature1, - ), + _FeatureItem(icon: Icons.location_on, text: l10n.wizardMapFeature1), _FeatureItem( icon: Icons.person_pin_circle, text: l10n.wizardMapFeature2, ), - _FeatureItem( - icon: Icons.offline_pin, - text: l10n.wizardMapFeature3, - ), - _FeatureItem( - icon: Icons.draw, - text: l10n.wizardMapFeature4, - ), + _FeatureItem(icon: Icons.offline_pin, text: l10n.wizardMapFeature3), + _FeatureItem(icon: Icons.draw, text: l10n.wizardMapFeature4), ], colorScheme: colorScheme, ); @@ -332,20 +274,16 @@ class _WelcomeWizardScreenState extends State { color: iconColor.withValues(alpha: 0.1), shape: BoxShape.circle, ), - child: Icon( - icon, - size: 80, - color: iconColor, - ), + child: Icon(icon, size: 80, color: iconColor), ), const SizedBox(height: 32), // Title Text( title, style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), textAlign: TextAlign.center, ), const SizedBox(height: 16), @@ -353,36 +291,33 @@ class _WelcomeWizardScreenState extends State { Text( description, style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: colorScheme.onSurface.withValues(alpha: 0.7), - height: 1.5, - ), + color: colorScheme.onSurface.withValues(alpha: 0.7), + height: 1.5, + ), textAlign: TextAlign.center, ), if (features != null && features.isNotEmpty) ...[ const SizedBox(height: 32), // Features list - ...features.map((feature) => Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - children: [ - Icon( - feature.icon, - color: colorScheme.primary, - size: 24, - ), - const SizedBox(width: 16), - Expanded( - child: Text( - feature.text, - style: - Theme.of(context).textTheme.bodyMedium?.copyWith( - color: colorScheme.onSurface, - ), + ...features.map( + (feature) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + Icon(feature.icon, color: colorScheme.primary, size: 24), + const SizedBox(width: 16), + Expanded( + child: Text( + feature.text, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurface, ), ), - ], - ), - )), + ), + ], + ), + ), + ), ], const SizedBox(height: 20), ], diff --git a/lib/widgets/contacts/contact_tile.dart b/lib/widgets/contacts/contact_tile.dart index 42382f3..d985065 100644 --- a/lib/widgets/contacts/contact_tile.dart +++ b/lib/widgets/contacts/contact_tile.dart @@ -8,13 +8,11 @@ import '../../models/room_login_state.dart'; import '../../providers/connection_provider.dart'; import '../../providers/contacts_provider.dart'; import '../../providers/map_provider.dart'; -import '../../providers/app_provider.dart'; import 'contact_route_dialog.dart'; import 'room_login_sheet.dart'; import '../common/contact_avatar.dart'; import '../../utils/location_formats.dart'; import '../../utils/toast_logger.dart'; -import '../../utils/battery_display_helper.dart'; import '../../l10n/app_localizations.dart'; class ContactTile extends StatelessWidget { @@ -46,12 +44,6 @@ class ContactTile extends StatelessWidget { @override Widget build(BuildContext context) { - final appProvider = context.watch(); - final isSimpleMode = appProvider.isSimpleMode; - - final hasTelemetry = - contact.telemetry != null && contact.telemetry!.isRecent; - final battery = contact.displayBattery; final location = contact.displayLocation; // Calculate distance if both positions are available String? distanceText; @@ -102,13 +94,11 @@ class ContactTile extends StatelessWidget { ) : null; void handleTap() { - if (isSimpleMode && contact.type == ContactType.chat) { + if (contact.type == ContactType.chat) { _showSetRouteDialog(context, contact); - } else if (isSimpleMode && contact.type == ContactType.repeater) { + } else if (contact.type == ContactType.repeater) { _jumpToMapForRepeater(context, contact); - } else if (isSimpleMode && - contact.type == ContactType.room && - !contact.isPublicChannel) { + } else if (contact.type == ContactType.room && !contact.isPublicChannel) { _showRoomLoginDialog(context, contact); } else { _showContactDetails(context, contact); @@ -155,137 +145,33 @@ class ContactTile extends StatelessWidget { : colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600, ); - final subtitleWidget = isSimpleMode - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (location != null) ...[ - const SizedBox(height: 2), - _buildLocationLine( - context, - latitude: location.latitude, - longitude: location.longitude, - distanceText: distanceText, - ), - if (contact.type != ContactType.channel) ...[ - const SizedBox(height: 6), - Row(children: [_buildRoutePill(context, contact)]), - ], - ] else - Padding( - padding: const EdgeInsets.only(top: 4), - child: Text( - AppLocalizations.of(context)!.noGpsData, - style: Theme.of( - context, - ).textTheme.labelSmall?.copyWith(color: Colors.grey), - ), - ), - ], - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 2), - if (roomLoginState != null && roomLoginState.isLoggedIn) ...[ - Row( - children: [ - if (roomLoginState.isAdmin) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.red.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.red, width: 1), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.admin_panel_settings, - size: 10, - color: Colors.red, - ), - const SizedBox(width: 2), - Text( - AppLocalizations.of(context)!.admin, - style: Theme.of(context).textTheme.labelSmall - ?.copyWith( - color: Colors.red, - fontWeight: FontWeight.bold, - fontSize: 10, - ), - ), - ], - ), - ), - if (roomLoginState.isAdmin) const SizedBox(width: 4), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.green.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.green, width: 1), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.check_circle, - size: 10, - color: Colors.green, - ), - const SizedBox(width: 2), - Text( - AppLocalizations.of(context)!.loggedIn, - style: Theme.of(context).textTheme.labelSmall - ?.copyWith( - color: Colors.green, - fontWeight: FontWeight.bold, - fontSize: 10, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 4), - ], - Row( - children: [ - if (location != null) ...[ - Expanded( - child: _buildLocationLine( - context, - latitude: location.latitude, - longitude: location.longitude, - distanceText: distanceText, - telemetryActive: hasTelemetry, - ), - ), - ] else ...[ - const Icon(Icons.sensors_off, size: 12, color: Colors.grey), - const SizedBox(width: 4), - Text( - AppLocalizations.of(context)!.noGpsData, - style: Theme.of(context).textTheme.labelSmall, - ), - ], - ], - ), - if (contact.type != ContactType.channel) ...[ - const SizedBox(height: 6), - Row(children: [_buildRoutePill(context, contact)]), - ], - ], - ); + final subtitleWidget = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (location != null) ...[ + const SizedBox(height: 2), + _buildLocationLine( + context, + latitude: location.latitude, + longitude: location.longitude, + distanceText: distanceText, + ), + if (contact.type != ContactType.channel) ...[ + const SizedBox(height: 6), + Row(children: [_buildRoutePill(context, contact)]), + ], + ] else + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + AppLocalizations.of(context)!.noGpsData, + style: Theme.of( + context, + ).textTheme.labelSmall?.copyWith(color: Colors.grey), + ), + ), + ], + ); return Container( margin: const EdgeInsets.only(bottom: 8), @@ -383,10 +269,6 @@ class ContactTile extends StatelessWidget { ), const SizedBox(width: 8), Text(timeAgoText, style: timeAgoStyle), - if (!isSimpleMode && battery != null) ...[ - const SizedBox(width: 6), - _buildBatteryBadge(context, battery), - ], if (isPingInProgress) ...[ const SizedBox(width: 6), SizedBox( @@ -1439,33 +1321,4 @@ class ContactTile extends StatelessWidget { ), ); } - - Widget _buildBatteryBadge(BuildContext context, double battery) { - final color = BatteryDisplayHelper.getBatteryColor(battery); - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(999), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - BatteryDisplayHelper.getBatteryIcon(battery), - size: 12, - color: color, - ), - const SizedBox(width: 4), - Text( - '${battery.round()}%', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: color, - fontWeight: FontWeight.w700, - ), - ), - ], - ), - ); - } } diff --git a/lib/widgets/map/drawing_layer.dart b/lib/widgets/map/drawing_layer.dart index 6f71ad7..fa2cd27 100644 --- a/lib/widgets/map/drawing_layer.dart +++ b/lib/widgets/map/drawing_layer.dart @@ -8,14 +8,8 @@ import '../../l10n/app_localizations.dart'; class DrawingLayer extends StatelessWidget { final List drawings; final MapDrawing? previewDrawing; - final bool isSimpleMode; - const DrawingLayer({ - super.key, - required this.drawings, - this.previewDrawing, - this.isSimpleMode = false, - }); + const DrawingLayer({super.key, required this.drawings, this.previewDrawing}); @override Widget build(BuildContext context) { @@ -48,8 +42,7 @@ class DrawingLayer extends StatelessWidget { strokeWidth = 4.0; } else if (drawing.isReceived) { // Received drawing from another node - // In simple mode: solid (opacity 1.0), in normal mode: translucent (0.7) - opacity = isSimpleMode ? 1.0 : 0.7; + opacity = 1.0; strokeWidth = 3.0; } else { // Local drawing (solid line, normal thickness) @@ -87,7 +80,6 @@ class DrawingMarkersLayer extends StatelessWidget { final Function(String drawingId)? onDeleteDrawing; final Function(MapDrawing drawing)? onTapDrawing; final bool showDeleteButtons; - final bool isSimpleMode; const DrawingMarkersLayer({ super.key, @@ -95,7 +87,6 @@ class DrawingMarkersLayer extends StatelessWidget { this.onDeleteDrawing, this.onTapDrawing, this.showDeleteButtons = false, - this.isSimpleMode = false, }); @override @@ -132,73 +123,7 @@ class DrawingMarkersLayer extends StatelessWidget { ), ], ), - child: const Icon( - Icons.close, - color: Colors.white, - size: 20, - ), - ), - ), - ), - ); - } else if (drawing.isReceived && drawing.senderName != null && !isSimpleMode) { - // Show sender badge for received drawings (when not in drawing mode and not in simple mode) - // Make it tappable if message ID is available - markers.add( - Marker( - point: centerPoint, - width: 120, - height: 30, - child: GestureDetector( - onTap: drawing.messageId != null && onTapDrawing != null - ? () => onTapDrawing!(drawing) - : null, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: drawing.color.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white, width: 1.5), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.person, - color: Colors.white, - size: 14, - ), - const SizedBox(width: 4), - Flexible( - child: Text( - drawing.senderName!, - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.bold, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - // Add indicator that this is tappable - if (drawing.messageId != null && onTapDrawing != null) ...[ - const SizedBox(width: 4), - const Icon( - Icons.arrow_forward_ios, - color: Colors.white, - size: 10, - ), - ], - ], - ), + child: const Icon(Icons.close, color: Colors.white, size: 20), ), ), ), @@ -236,9 +161,7 @@ class DrawingMarkersLayer extends StatelessWidget { context: context, builder: (context) => AlertDialog( title: Text(AppLocalizations.of(context)!.deleteDrawing), - content: Text( - 'Delete this ${drawing.type.name}?', - ), + content: Text('Delete this ${drawing.type.name}?'), actions: [ TextButton( onPressed: () => Navigator.pop(context), @@ -249,9 +172,7 @@ class DrawingMarkersLayer extends StatelessWidget { Navigator.pop(context); onDeleteDrawing?.call(drawing.id); }, - style: TextButton.styleFrom( - foregroundColor: Colors.red, - ), + style: TextButton.styleFrom(foregroundColor: Colors.red), child: Text(AppLocalizations.of(context)!.delete), ), ], diff --git a/lib/widgets/map/trail_controls.dart b/lib/widgets/map/trail_controls.dart index 5d08a3b..8730f57 100644 --- a/lib/widgets/map/trail_controls.dart +++ b/lib/widgets/map/trail_controls.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../providers/map_provider.dart'; import '../../providers/contacts_provider.dart'; -import '../../providers/app_provider.dart'; -import '../../services/gpx_service.dart'; import '../../services/trail_color_service.dart'; import '../../l10n/app_localizations.dart'; @@ -13,10 +11,11 @@ class TrailControls extends StatelessWidget { void _showTrailMenu(BuildContext context) { final mapProvider = Provider.of(context, listen: false); - final contactsProvider = Provider.of(context, listen: false); - final appProvider = Provider.of(context, listen: false); + final contactsProvider = Provider.of( + context, + listen: false, + ); final l10n = AppLocalizations.of(context)!; - final isSimpleMode = appProvider.isSimpleMode; // Get contacts with trails (advertHistory >= 2 points) final contactsWithTrails = contactsProvider.contactsWithLocation @@ -34,258 +33,232 @@ class TrailControls extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Row( - children: [ - const Icon(Icons.timeline, size: 24), - const SizedBox(width: 12), - Text( - l10n.locationTrail, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 20), - - // Trail visibility toggle - SwitchListTile( - secondary: const Icon(Icons.visibility), - title: Text(l10n.showTrailOnMap), - subtitle: Text( - mapProvider.isTrailVisible - ? l10n.trailVisible - : l10n.trailHiddenRecording, - ), - value: mapProvider.isTrailVisible, - onChanged: (value) { - mapProvider.toggleTrailVisibility(); - setModalState(() {}); // Update modal UI - }, - ), - const Divider(), - const SizedBox(height: 8), - - // Trail stats - if (mapProvider.currentTrail != null && mapProvider.currentTrail!.points.isNotEmpty) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.blue.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.blue.withValues(alpha: 0.3)), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + Row( children: [ - _buildStatRow( - icon: Icons.straighten, - label: l10n.distance, - value: _formatDistance(mapProvider.totalTrailDistance), - ), - const SizedBox(height: 8), - _buildStatRow( - icon: Icons.access_time, - label: l10n.duration, - value: _formatDuration(mapProvider.trailDuration), - ), - const SizedBox(height: 8), - _buildStatRow( - icon: Icons.place, - label: l10n.points, - value: '${mapProvider.currentTrail!.points.length}', + const Icon(Icons.timeline, size: 24), + const SizedBox(width: 12), + Text( + l10n.locationTrail, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), ), ], ), - ), + const SizedBox(height: 20), - const SizedBox(height: 16), - - // GPX Export/Import buttons (hidden in simple mode) - if (!isSimpleMode) ...[ - if (mapProvider.currentTrail != null && mapProvider.currentTrail!.points.isNotEmpty) - ElevatedButton.icon( - onPressed: () async { - final success = await GpxService.exportTrailToFile(mapProvider.currentTrail!); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(success - ? l10n.trailExportedSuccessfully - : l10n.failedToExportTrail), - backgroundColor: success ? Colors.green : Colors.red, - ), - ); - } + // Trail visibility toggle + SwitchListTile( + secondary: const Icon(Icons.visibility), + title: Text(l10n.showTrailOnMap), + subtitle: Text( + mapProvider.isTrailVisible + ? l10n.trailVisible + : l10n.trailHiddenRecording, + ), + value: mapProvider.isTrailVisible, + onChanged: (value) { + mapProvider.toggleTrailVisibility(); + setModalState(() {}); // Update modal UI }, - icon: const Icon(Icons.upload), - label: Text(l10n.exportTrailToGpx), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.all(16), - ), ), + const Divider(), + const SizedBox(height: 8), - const SizedBox(height: 8), - - ElevatedButton.icon( - onPressed: () async { - try { - final trail = await GpxService.importTrailFromFile(); - if (trail != null && context.mounted) { - _showImportDialog(context, mapProvider, trail, l10n); - } - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.failedToImportTrail(e.toString())), - backgroundColor: Colors.red, - ), - ); - } - } - }, - icon: const Icon(Icons.download), - label: Text(l10n.importTrailFromGpx), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.all(16), - ), - ), - - const SizedBox(height: 16), - ], - - // Clear trail button - if (mapProvider.currentTrail != null && mapProvider.currentTrail!.points.isNotEmpty) - ElevatedButton.icon( - onPressed: () { - _showClearConfirmation(context, mapProvider, l10n); - }, - icon: const Icon(Icons.delete_outline), - label: Text(l10n.clearTrail), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - padding: const EdgeInsets.all(16), - ), - ), - - // No trail message - if (mapProvider.currentTrail == null || mapProvider.currentTrail!.points.isEmpty) - Padding( - padding: const EdgeInsets.all(16), - child: Center( - child: Column( - children: [ - const Icon(Icons.timeline, size: 48, color: Colors.grey), - const SizedBox(height: 8), - Text( - l10n.noTrailRecorded, - style: const TextStyle( - color: Colors.grey, - fontSize: 16, - ), + // Trail stats + if (mapProvider.currentTrail != null && + mapProvider.currentTrail!.points.isNotEmpty) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.blue.withValues(alpha: 0.3), ), - const SizedBox(height: 8), - Text( - l10n.startTrackingToRecord, - style: const TextStyle( - color: Colors.grey, - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - - const SizedBox(height: 8), - const Divider(), - const SizedBox(height: 8), - - // Contact Trails Section - Row( - children: [ - const Icon(Icons.people, size: 20), - const SizedBox(width: 8), - Text( - l10n.contactTrails, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 12), - - // Show All Contact Trails toggle - SwitchListTile( - secondary: const Icon(Icons.route), - title: Text(l10n.showAllContactTrails), - subtitle: Text(contactsWithTrails.isEmpty - ? l10n.noContactsWithLocationHistory - : mapProvider.showAllContactTrails - ? l10n.showingTrailsForContacts(contactsWithTrails.length) - : l10n.individualContactTrails), - value: mapProvider.showAllContactTrails, - onChanged: contactsWithTrails.isNotEmpty - ? (value) { - mapProvider.toggleAllContactTrails(); - setModalState(() {}); // Update modal UI - } - : null, // Disable if no contacts with trails - ), - - // Individual contact trails (when "show all" is OFF) - if (!mapProvider.showAllContactTrails && contactsWithTrails.isNotEmpty) - ExpansionTile( - title: Text(l10n.individualContactTrails), - initiallyExpanded: false, - children: contactsWithTrails.map((contact) { - final trailColor = TrailColorService.getTrailColor(contact); - final isVisible = mapProvider.isContactPathVisible(contact.publicKeyHex); - - return SwitchListTile( - // Color indicator with emoji - secondary: Row( - mainAxisSize: MainAxisSize.min, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (contact.roleEmoji != null) - Text(contact.roleEmoji!, style: const TextStyle(fontSize: 18)), - const SizedBox(width: 4), - Container( - width: 16, - height: 16, - decoration: BoxDecoration( - color: trailColor, - border: Border.all(color: Colors.white, width: 2), - borderRadius: BorderRadius.circular(3), + _buildStatRow( + icon: Icons.straighten, + label: l10n.distance, + value: _formatDistance( + mapProvider.totalTrailDistance, ), ), + const SizedBox(height: 8), + _buildStatRow( + icon: Icons.access_time, + label: l10n.duration, + value: _formatDuration(mapProvider.trailDuration), + ), + const SizedBox(height: 8), + _buildStatRow( + icon: Icons.place, + label: l10n.points, + value: '${mapProvider.currentTrail!.points.length}', + ), ], ), - title: Text(contact.displayName), - subtitle: Text('${contact.advertHistory.length} points'), - value: isVisible, - onChanged: (value) { - mapProvider.toggleContactPath(contact.publicKeyHex); - setModalState(() {}); // Update modal UI + ), + + const SizedBox(height: 16), + + // Clear trail button + if (mapProvider.currentTrail != null && + mapProvider.currentTrail!.points.isNotEmpty) + ElevatedButton.icon( + onPressed: () { + _showClearConfirmation(context, mapProvider, l10n); }, - ); - }).toList(), - ), + icon: const Icon(Icons.delete_outline), + label: Text(l10n.clearTrail), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + padding: const EdgeInsets.all(16), + ), + ), - const SizedBox(height: 8), + // No trail message + if (mapProvider.currentTrail == null || + mapProvider.currentTrail!.points.isEmpty) + Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: Column( + children: [ + const Icon( + Icons.timeline, + size: 48, + color: Colors.grey, + ), + const SizedBox(height: 8), + Text( + l10n.noTrailRecorded, + style: const TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ), + const SizedBox(height: 8), + Text( + l10n.startTrackingToRecord, + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), - // Close button - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(l10n.close), - ), - ], + const SizedBox(height: 8), + const Divider(), + const SizedBox(height: 8), + + // Contact Trails Section + Row( + children: [ + const Icon(Icons.people, size: 20), + const SizedBox(width: 8), + Text( + l10n.contactTrails, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 12), + + // Show All Contact Trails toggle + SwitchListTile( + secondary: const Icon(Icons.route), + title: Text(l10n.showAllContactTrails), + subtitle: Text( + contactsWithTrails.isEmpty + ? l10n.noContactsWithLocationHistory + : mapProvider.showAllContactTrails + ? l10n.showingTrailsForContacts( + contactsWithTrails.length, + ) + : l10n.individualContactTrails, + ), + value: mapProvider.showAllContactTrails, + onChanged: contactsWithTrails.isNotEmpty + ? (value) { + mapProvider.toggleAllContactTrails(); + setModalState(() {}); // Update modal UI + } + : null, // Disable if no contacts with trails + ), + + // Individual contact trails (when "show all" is OFF) + if (!mapProvider.showAllContactTrails && + contactsWithTrails.isNotEmpty) + ExpansionTile( + title: Text(l10n.individualContactTrails), + initiallyExpanded: false, + children: contactsWithTrails.map((contact) { + final trailColor = TrailColorService.getTrailColor( + contact, + ); + final isVisible = mapProvider.isContactPathVisible( + contact.publicKeyHex, + ); + + return SwitchListTile( + // Color indicator with emoji + secondary: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (contact.roleEmoji != null) + Text( + contact.roleEmoji!, + style: const TextStyle(fontSize: 18), + ), + const SizedBox(width: 4), + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: trailColor, + border: Border.all( + color: Colors.white, + width: 2, + ), + borderRadius: BorderRadius.circular(3), + ), + ), + ], + ), + title: Text(contact.displayName), + subtitle: Text( + '${contact.advertHistory.length} points', + ), + value: isVisible, + onChanged: (value) { + mapProvider.toggleContactPath(contact.publicKeyHex); + setModalState(() {}); // Update modal UI + }, + ); + }).toList(), + ), + + const SizedBox(height: 8), + + // Close button + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(l10n.close), + ), + ], ), ), ), @@ -293,7 +266,11 @@ class TrailControls extends StatelessWidget { ); } - void _showClearConfirmation(BuildContext context, MapProvider mapProvider, AppLocalizations l10n) { + void _showClearConfirmation( + BuildContext context, + MapProvider mapProvider, + AppLocalizations l10n, + ) { showDialog( context: context, builder: (context) => AlertDialog( @@ -318,50 +295,6 @@ class TrailControls extends StatelessWidget { ); } - void _showImportDialog(BuildContext context, MapProvider mapProvider, trail, AppLocalizations l10n) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(l10n.importTrail), - content: Text(l10n.importTrailQuestion(trail.points.length)), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(l10n.cancel), - ), - TextButton( - onPressed: () { - mapProvider.setImportedTrail(trail); - Navigator.pop(context); // Close dialog - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.trailImported(trail.points.length)), - backgroundColor: Colors.green, - ), - ); - }, - child: Text(l10n.viewAlongside), - ), - TextButton( - onPressed: () { - mapProvider.replaceCurrentTrailWithImport(trail); - Navigator.pop(context); // Close dialog - Navigator.pop(context); // Close bottom sheet - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.trailReplaced(trail.points.length)), - backgroundColor: Colors.green, - ), - ); - }, - style: TextButton.styleFrom(foregroundColor: Colors.blue), - child: Text(l10n.replaceCurrent), - ), - ], - ), - ); - } - Widget _buildStatRow({ required IconData icon, required String label, @@ -373,18 +306,12 @@ class TrailControls extends StatelessWidget { const SizedBox(width: 8), Text( label, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - ), + style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14), ), const Spacer(), Text( value, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), ], );