mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Upgrade flutter packages and limit
This commit is contained in:
@@ -14,7 +14,6 @@ import 'messages_tab.dart';
|
||||
import 'contacts_tab.dart';
|
||||
import 'sensors_tab.dart';
|
||||
import 'map_tab.dart';
|
||||
import 'map_management_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
import 'device_config_screen.dart';
|
||||
import 'packet_log_screen.dart';
|
||||
@@ -49,7 +48,8 @@ class HomeScreen extends StatefulWidget {
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
class _HomeScreenState extends State<HomeScreen>
|
||||
with TickerProviderStateMixin, WidgetsBindingObserver {
|
||||
late TabController _tabController;
|
||||
late final AppProvider _appProvider;
|
||||
int _currentIndex = 0;
|
||||
@@ -58,6 +58,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
bool _isMapEnabled = true;
|
||||
bool _isContactsEnabled = true;
|
||||
bool _isSensorsEnabled = false;
|
||||
AppLifecycleState _lifecycleState = AppLifecycleState.resumed;
|
||||
|
||||
List<_HomeTab> get _enabledTabs {
|
||||
return [
|
||||
@@ -79,6 +80,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_appProvider = context.read<AppProvider>();
|
||||
_isMapEnabled = _appProvider.isMapEnabled;
|
||||
_isContactsEnabled = _appProvider.isContactsEnabled;
|
||||
@@ -184,6 +186,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
}
|
||||
|
||||
void _handleTabActivated(_HomeTab tab) {
|
||||
_syncFastLocationUiState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -202,6 +205,20 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
});
|
||||
}
|
||||
|
||||
void _syncFastLocationUiState() {
|
||||
final isActiveTab =
|
||||
_currentTab == _HomeTab.map || _currentTab == _HomeTab.messages;
|
||||
_appProvider.setFastLocationUiActive(
|
||||
_lifecycleState == AppLifecycleState.resumed && isActiveTab,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
_lifecycleState = state;
|
||||
_syncFastLocationUiState();
|
||||
}
|
||||
|
||||
Future<void> _loadRxTxPreference() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (mounted) {
|
||||
@@ -213,6 +230,8 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_appProvider.setFastLocationUiActive(false);
|
||||
_appProvider.removeListener(_handleAppProviderChanged);
|
||||
_tabController.removeListener(_onTabChanged);
|
||||
_tabController.dispose();
|
||||
@@ -580,30 +599,6 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.map),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.mapManagement),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
// Capture context-dependent objects before async gap
|
||||
final navigator = Navigator.of(context);
|
||||
final appProvider = context.read<AppProvider>();
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (!mounted) return;
|
||||
navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MapManagementScreen(
|
||||
tileCacheService: appProvider.tileCacheService,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -854,10 +849,11 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
Text(
|
||||
deviceInfo.selfName ??
|
||||
AppLocalizations.of(context)!.appTitle,
|
||||
style: (isTight
|
||||
? theme.textTheme.titleSmall
|
||||
: theme.textTheme.titleMedium)
|
||||
?.copyWith(fontWeight: FontWeight.w700),
|
||||
style:
|
||||
(isTight
|
||||
? theme.textTheme.titleSmall
|
||||
: theme.textTheme.titleMedium)
|
||||
?.copyWith(fontWeight: FontWeight.w700),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
@@ -927,7 +923,8 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const DeviceConfigScreen(),
|
||||
builder: (context) =>
|
||||
const DeviceConfigScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart' as flutter_map;
|
||||
@@ -9,9 +8,6 @@ import 'package:provider/provider.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:flutter_compass/flutter_compass.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:vector_map_tiles/vector_map_tiles.dart';
|
||||
import 'package:vector_tile_renderer/vector_tile_renderer.dart' as vtr;
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../utils/slovenian_crs.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../providers/messages_provider.dart';
|
||||
@@ -23,11 +19,9 @@ import '../models/contact.dart';
|
||||
import '../models/sar_marker.dart';
|
||||
import '../models/map_layer.dart';
|
||||
import '../models/message.dart';
|
||||
import '../services/tile_cache_service.dart';
|
||||
import '../services/background_location_service.dart';
|
||||
import '../services/location_tracking_service.dart';
|
||||
import '../services/map_marker_service.dart';
|
||||
import '../services/mbtiles_service.dart';
|
||||
import '../services/trail_color_service.dart';
|
||||
import '../widgets/map_debug_info.dart';
|
||||
import '../widgets/map/compass_widget.dart';
|
||||
@@ -37,11 +31,9 @@ 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';
|
||||
import '../widgets/map/download_area_overlay.dart';
|
||||
import '../widgets/messages/sar_update_sheet.dart';
|
||||
import '../utils/key_comparison.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import 'map_management_screen.dart';
|
||||
|
||||
class MapTab extends StatefulWidget {
|
||||
final Function(bool)? onFullscreenChanged;
|
||||
@@ -59,11 +51,15 @@ class MapTab extends StatefulWidget {
|
||||
|
||||
class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
final MapController _mapController = MapController();
|
||||
late final TileCacheService _tileCache;
|
||||
static final TileProvider _tileProvider = NetworkTileProvider(
|
||||
cachingProvider: BuiltInMapCachingProvider.getOrCreateInstance(
|
||||
maxCacheSize: 10_000_000_000,
|
||||
overrideFreshAge: const Duration(days: 365),
|
||||
),
|
||||
);
|
||||
// DO NOT create a new LocationTrackingService instance here
|
||||
// Use the singleton from AppProvider instead via _locationService getter
|
||||
final MapMarkerService _markerService = MapMarkerService();
|
||||
bool _isInitialized = false;
|
||||
bool _isMapReady = false; // Track when map widget is actually rendered
|
||||
MapLayer _currentLayer = MapLayer.openStreetMap;
|
||||
double? _compassHeading; // Compass sensor heading
|
||||
@@ -78,9 +74,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
bool _isDisposing = false; // Flag to prevent updates during disposal
|
||||
MapProvider? _mapProvider;
|
||||
|
||||
// MBTiles layers
|
||||
List<MapLayer> _mbtilesLayers = [];
|
||||
|
||||
// Store original location callback to restore in dispose
|
||||
void Function(Position)? _originalLocationCallback;
|
||||
|
||||
@@ -88,10 +81,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
late final MapLayer _slovenianAerialLayer;
|
||||
late final MapLayer _dtk25Layer;
|
||||
|
||||
// Vector tile theme
|
||||
vtr.Theme? _vectorTheme;
|
||||
bool _isLoadingTheme = false;
|
||||
|
||||
// Dropped pin state
|
||||
LatLng? _droppedPinLocation;
|
||||
bool _isDraggingPin = false;
|
||||
@@ -117,13 +106,11 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tileCache = context.read<TileCacheService>();
|
||||
// Initialize Slovenian WMS layers with CRS
|
||||
_slovenianAerialLayer = MapLayer.getSlovenianAerial2024(slovenianCrs);
|
||||
_dtk25Layer = MapLayer.getDTK25(slovenianCrs);
|
||||
_loadSettings();
|
||||
_loadMbtilesLayers();
|
||||
_initializeTileCache();
|
||||
_markMapReadyWhenMounted();
|
||||
_setupLocationCallbacks();
|
||||
_startCompassTracking();
|
||||
|
||||
@@ -242,36 +229,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
});
|
||||
}
|
||||
|
||||
/// Load MBTiles layers from file system
|
||||
Future<void> _loadMbtilesLayers() async {
|
||||
try {
|
||||
final mbtilesService = MbtilesService();
|
||||
final metadata = await mbtilesService.getAllMetadata();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_mbtilesLayers = metadata.map((meta) {
|
||||
// Determine if data is gzipped (for Geofabrik files)
|
||||
final isGzipped = meta.format == 'pbf';
|
||||
|
||||
return MapLayer.fromMbtilesFile(
|
||||
name: meta.name,
|
||||
mbtilesFile: meta.file,
|
||||
styleUrl: 'https://tiles.openfreemap.org/styles/bright',
|
||||
sourceName: 'openmaptiles',
|
||||
maxZoom: 20.0, // Override to 20 for overzooming
|
||||
isGzipped: isGzipped,
|
||||
attribution: meta.attribution,
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
debugPrint('Loaded ${_mbtilesLayers.length} MBTiles layers');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading MBTiles layers: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (mounted) {
|
||||
@@ -282,8 +239,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
|
||||
// Load last map layer
|
||||
final lastLayerType = prefs.getInt('map_last_layer_type');
|
||||
final lastLayerName = prefs.getString('map_last_layer_name');
|
||||
|
||||
setState(() {
|
||||
_rotateMarkerWithHeading =
|
||||
prefs.getBool('map_rotate_with_heading') ?? false;
|
||||
@@ -304,17 +259,11 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
_savedMapZoom = lastZoom;
|
||||
}
|
||||
|
||||
// Restore last used map layer (by type and name for MBTiles)
|
||||
// Restore last used map layer.
|
||||
if (lastLayerType != null) {
|
||||
final layerType = MapLayerType.values[lastLayerType];
|
||||
if (layerType == MapLayerType.vectorMbtiles &&
|
||||
lastLayerName != null) {
|
||||
// Find MBTiles layer by name
|
||||
final mbtilesLayer = _mbtilesLayers.firstWhere(
|
||||
(layer) => layer.name == lastLayerName,
|
||||
orElse: () => MapLayer.openStreetMap,
|
||||
);
|
||||
_currentLayer = mbtilesLayer;
|
||||
if (layerType == MapLayerType.vectorMbtiles) {
|
||||
_currentLayer = MapLayer.openStreetMap;
|
||||
} else if (layerType == MapLayerType.wmsBase) {
|
||||
// Use Slovenian aerial layer if that's what was saved
|
||||
_currentLayer = _slovenianAerialLayer;
|
||||
@@ -347,11 +296,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
_backgroundTrackingEnabled,
|
||||
);
|
||||
|
||||
// Save layer type and name (for MBTiles layers)
|
||||
// Save layer type.
|
||||
await prefs.setInt('map_last_layer_type', _currentLayer.type.index);
|
||||
if (_currentLayer.type == MapLayerType.vectorMbtiles) {
|
||||
await prefs.setString('map_last_layer_name', _currentLayer.name);
|
||||
}
|
||||
await prefs.remove('map_last_layer_name');
|
||||
}
|
||||
|
||||
Future<void> _saveMapPosition() async {
|
||||
@@ -384,49 +331,17 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initializeTileCache() async {
|
||||
try {
|
||||
await _tileCache.initialize();
|
||||
if (mounted) {
|
||||
void _markMapReadyWhenMounted() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isInitialized = true;
|
||||
_isMapReady = true;
|
||||
});
|
||||
// Wait for the map to render, then mark it as ready
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
// Give the map widget one more frame to fully initialize
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isMapReady = true;
|
||||
});
|
||||
debugPrint('Map is now ready for controller operations');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error initializing tile cache: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isInitialized = true; // Continue without caching
|
||||
});
|
||||
// Still mark map as ready after a delay
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isMapReady = true;
|
||||
});
|
||||
debugPrint('Map is now ready for controller operations');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
debugPrint('Map is now ready for controller operations');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -483,45 +398,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
);
|
||||
}
|
||||
|
||||
/// Load vector tile theme from URL
|
||||
Future<void> _loadVectorTheme(String styleUrl) async {
|
||||
if (_isLoadingTheme) return;
|
||||
|
||||
setState(() {
|
||||
_isLoadingTheme = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await http.get(Uri.parse(styleUrl));
|
||||
if (response.statusCode == 200) {
|
||||
final styleJson = jsonDecode(response.body) as Map<String, Object?>;
|
||||
final theme = vtr.ThemeReader().read(styleJson);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_vectorTheme = theme;
|
||||
_isLoadingTheme = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw Exception('Failed to load style: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading vector theme: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoadingTheme = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to load map style: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showLayerSelector(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@@ -544,14 +420,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.download),
|
||||
tooltip: AppLocalizations.of(context)!.downloadVisibleArea,
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_navigateToDownload(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -649,65 +517,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
},
|
||||
),
|
||||
],
|
||||
// Offline MBTiles layers section
|
||||
if (_mbtilesLayers.isNotEmpty) ...[
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.offlineLayers,
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
),
|
||||
..._mbtilesLayers.map(
|
||||
(layer) => ListTile(
|
||||
leading: _currentLayer == layer
|
||||
? const Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.green,
|
||||
)
|
||||
: Icon(
|
||||
layer.isVector ? Icons.layers : Icons.image,
|
||||
color: layer.isVector
|
||||
? Colors.blue
|
||||
: Colors.orange,
|
||||
),
|
||||
title: Text(layer.name),
|
||||
subtitle: Text(layer.attribution),
|
||||
onTap: () async {
|
||||
// Capture navigator before async operation
|
||||
final navigator = Navigator.of(context);
|
||||
// Load vector theme if switching to vector layer
|
||||
if (layer.isVector && layer.styleUrl != null) {
|
||||
navigator.pop();
|
||||
await _loadVectorTheme(layer.styleUrl!);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_currentLayer = layer;
|
||||
// Clamp zoom level if current zoom exceeds new layer's max
|
||||
if (_isMapReady &&
|
||||
_mapController.camera.zoom > layer.maxZoom) {
|
||||
_mapController.move(
|
||||
_mapController.camera.center,
|
||||
layer.maxZoom,
|
||||
);
|
||||
}
|
||||
});
|
||||
_saveSettings();
|
||||
|
||||
if (!layer.isVector && mounted) {
|
||||
navigator.pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
// WMS Overlays section (only for Slovenian/Croatian regions and when WMS base layer is selected)
|
||||
if ((AppLocalizations.of(context)!.localeName == 'sl' ||
|
||||
AppLocalizations.of(context)!.localeName == 'hr') &&
|
||||
@@ -899,21 +708,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToDownload(BuildContext context) {
|
||||
if (!_isMapReady) return;
|
||||
|
||||
try {
|
||||
// Get current map bounds
|
||||
final bounds = _mapController.camera.visibleBounds;
|
||||
|
||||
// Enter download area selection mode (show preview overlay)
|
||||
final mapProvider = context.read<MapProvider>();
|
||||
mapProvider.enterDownloadAreaMode(bounds);
|
||||
} catch (e) {
|
||||
debugPrint('Error accessing map camera: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _showOptionsMenu(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@@ -1412,8 +1206,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
return Stack(
|
||||
children: [
|
||||
// Map widget
|
||||
_isInitialized
|
||||
? Listener(
|
||||
Listener(
|
||||
onPointerMove: (PointerMoveEvent event) {
|
||||
// Track pointer movement for mobile drag (onPointerHover doesn't work on mobile)
|
||||
if (_isDraggingPin) {
|
||||
@@ -1566,17 +1359,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
},
|
||||
),
|
||||
children: [
|
||||
// Render vector or raster tile layer based on layer type
|
||||
if (_currentLayer.isVector && _vectorTheme != null)
|
||||
VectorTileLayer(
|
||||
theme: _vectorTheme!,
|
||||
tileProviders: TileProviders({
|
||||
_currentLayer.sourceName ?? 'default': _tileCache
|
||||
.getVectorTileProvider(_currentLayer)!,
|
||||
}),
|
||||
maximumZoom: _currentLayer.maxZoom,
|
||||
)
|
||||
else if (_currentLayer.isWms &&
|
||||
// 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)
|
||||
@@ -1591,9 +1375,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
crs: _currentLayer.crs!,
|
||||
),
|
||||
// Use cached tile provider for offline support
|
||||
tileProvider: _tileCache.getTileProviderForWms(
|
||||
_currentLayer,
|
||||
),
|
||||
tileProvider: _tileProvider,
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
maxZoom: _currentLayer.maxZoom,
|
||||
errorTileCallback: (tile, error, stackTrace) {
|
||||
@@ -1602,13 +1384,10 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
);
|
||||
},
|
||||
)
|
||||
else if (!_currentLayer.isVector &&
|
||||
!_currentLayer.isWms)
|
||||
else if (!_currentLayer.isWms)
|
||||
flutter_map.TileLayer(
|
||||
urlTemplate: _currentLayer.urlTemplate,
|
||||
tileProvider: _tileCache.getTileProvider(
|
||||
_currentLayer,
|
||||
),
|
||||
tileProvider: _tileProvider,
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
maxZoom: _currentLayer.maxZoom,
|
||||
),
|
||||
@@ -1632,23 +1411,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
transparent: true,
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
tileProvider: _tileCache.getTileProviderForWms(
|
||||
MapLayer(
|
||||
type: MapLayerType.wmsBase,
|
||||
name: 'Cadastral Parcels',
|
||||
urlTemplate: '',
|
||||
attribution: '© GURS',
|
||||
maxZoom: 19,
|
||||
isWms: true,
|
||||
wmsBaseUrl:
|
||||
'https://prostor.zgs.gov.si/geowebcache/service/wms?',
|
||||
wmsLayers: const [
|
||||
'pregledovalnik:kn_parcele',
|
||||
],
|
||||
wmsFormat: 'image/png',
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
),
|
||||
tileProvider: _tileProvider,
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
maxZoom: 19,
|
||||
errorTileCallback: (tile, error, stackTrace) {
|
||||
@@ -1680,23 +1443,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
transparent: true,
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
tileProvider: _tileCache.getTileProviderForWms(
|
||||
MapLayer(
|
||||
type: MapLayerType.wmsBase,
|
||||
name: 'Forest Roads',
|
||||
urlTemplate: '',
|
||||
attribution: '© GURS',
|
||||
maxZoom: 19,
|
||||
isWms: true,
|
||||
wmsBaseUrl:
|
||||
'https://prostor.zgs.gov.si/geoserver/wms?',
|
||||
wmsLayers: const [
|
||||
'pregledovalnik:gozdne_ceste',
|
||||
],
|
||||
wmsFormat: 'image/png',
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
),
|
||||
tileProvider: _tileProvider,
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
maxZoom: 19,
|
||||
errorTileCallback: (tile, error, stackTrace) {
|
||||
@@ -1728,23 +1475,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
transparent: true,
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
tileProvider: _tileCache.getTileProviderForWms(
|
||||
MapLayer(
|
||||
type: MapLayerType.wmsBase,
|
||||
name: 'Hiking Trails',
|
||||
urlTemplate: '',
|
||||
attribution: '© GURS',
|
||||
maxZoom: 19,
|
||||
isWms: true,
|
||||
wmsBaseUrl:
|
||||
'https://prostor.zgs.gov.si/geoserver/wms?',
|
||||
wmsLayers: const [
|
||||
'pregledovalnik:KGI_LINIJE_PLANINSKE_POTI_G',
|
||||
],
|
||||
wmsFormat: 'image/png',
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
),
|
||||
tileProvider: _tileProvider,
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
maxZoom: 19,
|
||||
errorTileCallback: (tile, error, stackTrace) {
|
||||
@@ -1773,23 +1504,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
transparent: true,
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
tileProvider: _tileCache.getTileProviderForWms(
|
||||
MapLayer(
|
||||
type: MapLayerType.wmsBase,
|
||||
name: 'Main Roads',
|
||||
urlTemplate: '',
|
||||
attribution: '© GURS',
|
||||
maxZoom: 19,
|
||||
isWms: true,
|
||||
wmsBaseUrl:
|
||||
'https://prostor.zgs.gov.si/geoserver/wms?',
|
||||
wmsLayers: const [
|
||||
'pregledovalnik:KGI_LINIJE_CESTE_G',
|
||||
],
|
||||
wmsFormat: 'image/png',
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
),
|
||||
tileProvider: _tileProvider,
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
maxZoom: 19,
|
||||
errorTileCallback: (tile, error, stackTrace) {
|
||||
@@ -1818,23 +1533,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
transparent: true,
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
tileProvider: _tileCache.getTileProviderForWms(
|
||||
MapLayer(
|
||||
type: MapLayerType.wmsBase,
|
||||
name: 'House Numbers',
|
||||
urlTemplate: '',
|
||||
attribution: '© GURS',
|
||||
maxZoom: 19,
|
||||
isWms: true,
|
||||
wmsBaseUrl:
|
||||
'https://prostor.zgs.gov.si/geoserver/wms?',
|
||||
wmsLayers: const [
|
||||
'pregledovalnik:NEP_HISNE_STEVILKE',
|
||||
],
|
||||
wmsFormat: 'image/png',
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
),
|
||||
tileProvider: _tileProvider,
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
maxZoom: 19,
|
||||
errorTileCallback: (tile, error, stackTrace) {
|
||||
@@ -1863,23 +1562,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
transparent: true,
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
tileProvider: _tileCache.getTileProviderForWms(
|
||||
MapLayer(
|
||||
type: MapLayerType.wmsBase,
|
||||
name: 'Fire Hazard Zones',
|
||||
urlTemplate: '',
|
||||
attribution: '© GURS',
|
||||
maxZoom: 19,
|
||||
isWms: true,
|
||||
wmsBaseUrl:
|
||||
'https://prostor.zgs.gov.si/geoserver/wms?',
|
||||
wmsLayers: const [
|
||||
'pregledovalnik:pozarna_ogrozenost',
|
||||
],
|
||||
wmsFormat: 'image/png',
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
),
|
||||
tileProvider: _tileProvider,
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
maxZoom: 19,
|
||||
errorTileCallback: (tile, error, stackTrace) {
|
||||
@@ -1906,23 +1589,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
transparent: true,
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
tileProvider: _tileCache.getTileProviderForWms(
|
||||
MapLayer(
|
||||
type: MapLayerType.wmsBase,
|
||||
name: 'Historical Fires',
|
||||
urlTemplate: '',
|
||||
attribution: '© GURS',
|
||||
maxZoom: 19,
|
||||
isWms: true,
|
||||
wmsBaseUrl:
|
||||
'https://prostor.zgs.gov.si/geoserver/wms?',
|
||||
wmsLayers: const [
|
||||
'pregledovalnik:gozdni_pozari',
|
||||
],
|
||||
wmsFormat: 'image/png',
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
),
|
||||
tileProvider: _tileProvider,
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
maxZoom: 19,
|
||||
errorTileCallback: (tile, error, stackTrace) {
|
||||
@@ -1951,23 +1618,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
transparent: true,
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
tileProvider: _tileCache.getTileProviderForWms(
|
||||
MapLayer(
|
||||
type: MapLayerType.wmsBase,
|
||||
name: 'Firebreaks',
|
||||
urlTemplate: '',
|
||||
attribution: '© GURS',
|
||||
maxZoom: 19,
|
||||
isWms: true,
|
||||
wmsBaseUrl:
|
||||
'https://prostor.zgs.gov.si/geoserver/wms?',
|
||||
wmsLayers: const [
|
||||
'pregledovalnik:protipozarne_preseke',
|
||||
],
|
||||
wmsFormat: 'image/png',
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
),
|
||||
tileProvider: _tileProvider,
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
maxZoom: 19,
|
||||
errorTileCallback: (tile, error, stackTrace) {
|
||||
@@ -1994,23 +1645,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
transparent: true,
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
tileProvider: _tileCache.getTileProviderForWms(
|
||||
MapLayer(
|
||||
type: MapLayerType.wmsBase,
|
||||
name: 'Kras Fire Zones',
|
||||
urlTemplate: '',
|
||||
attribution: '© GURS',
|
||||
maxZoom: 19,
|
||||
isWms: true,
|
||||
wmsBaseUrl:
|
||||
'https://prostor.zgs.gov.si/geoserver/wms?',
|
||||
wmsLayers: const [
|
||||
'pregledovalnik:pozarisce_kras',
|
||||
],
|
||||
wmsFormat: 'image/png',
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
),
|
||||
tileProvider: _tileProvider,
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
maxZoom: 19,
|
||||
errorTileCallback: (tile, error, stackTrace) {
|
||||
@@ -2039,23 +1674,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
transparent: true,
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
tileProvider: _tileCache.getTileProviderForWms(
|
||||
MapLayer(
|
||||
type: MapLayerType.wmsBase,
|
||||
name: 'Place Names',
|
||||
urlTemplate: '',
|
||||
attribution: '© GURS',
|
||||
maxZoom: 19,
|
||||
isWms: true,
|
||||
wmsBaseUrl:
|
||||
'https://prostor.zgs.gov.si/geoserver/wms?',
|
||||
wmsLayers: const [
|
||||
'pregledovalnik:zemljepisna_imena',
|
||||
],
|
||||
wmsFormat: 'image/png',
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
),
|
||||
tileProvider: _tileProvider,
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
maxZoom: 19,
|
||||
errorTileCallback: (tile, error, stackTrace) {
|
||||
@@ -2083,23 +1702,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
transparent: true,
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
tileProvider: _tileCache.getTileProviderForWms(
|
||||
MapLayer(
|
||||
type: MapLayerType.wmsBase,
|
||||
name: 'Municipality Borders',
|
||||
urlTemplate: '',
|
||||
attribution: '© GURS',
|
||||
maxZoom: 19,
|
||||
isWms: true,
|
||||
wmsBaseUrl:
|
||||
'https://prostor.zgs.gov.si/geoserver/wms?',
|
||||
wmsLayers: const [
|
||||
'pregledovalnik:NEP_RPE_OBCINE',
|
||||
],
|
||||
wmsFormat: 'image/png',
|
||||
crs: slovenianCrs,
|
||||
),
|
||||
),
|
||||
tileProvider: _tileProvider,
|
||||
userAgentPackageName: 'com.meshcore.sar',
|
||||
maxZoom: 19,
|
||||
errorTileCallback: (tile, error, stackTrace) {
|
||||
@@ -2474,114 +2077,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
}
|
||||
},
|
||||
),
|
||||
// Download area selection polygon (rendered on top when in selection mode)
|
||||
Consumer<MapProvider>(
|
||||
builder: (context, mapProvider, _) {
|
||||
if (!mapProvider.isSelectingDownloadArea ||
|
||||
mapProvider.downloadAreaBounds == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final bounds = mapProvider.downloadAreaBounds!;
|
||||
|
||||
// Add padding to the bounds so the rectangle is visible within the screen
|
||||
// Calculate 5% padding on each side
|
||||
final latPadding =
|
||||
(bounds.north - bounds.south) * 0.05;
|
||||
final lonPadding =
|
||||
(bounds.east - bounds.west) * 0.05;
|
||||
|
||||
return PolygonLayer(
|
||||
polygons: [
|
||||
Polygon(
|
||||
points: [
|
||||
LatLng(
|
||||
bounds.north - latPadding,
|
||||
bounds.west + lonPadding,
|
||||
), // Top-left
|
||||
LatLng(
|
||||
bounds.north - latPadding,
|
||||
bounds.east - lonPadding,
|
||||
), // Top-right
|
||||
LatLng(
|
||||
bounds.south + latPadding,
|
||||
bounds.east - lonPadding,
|
||||
), // Bottom-right
|
||||
LatLng(
|
||||
bounds.south + latPadding,
|
||||
bounds.west + lonPadding,
|
||||
), // Bottom-left
|
||||
],
|
||||
color: Colors.blue.withValues(alpha: 0.2),
|
||||
borderColor: Colors.blue,
|
||||
borderStrokeWidth: 3.0,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.initializingMap,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Download area overlay (shown when in download area selection mode)
|
||||
Consumer<MapProvider>(
|
||||
builder: (context, mapProvider, _) {
|
||||
if (!mapProvider.isSelectingDownloadArea ||
|
||||
mapProvider.downloadAreaBounds == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return DownloadAreaOverlay(
|
||||
bounds: mapProvider.downloadAreaBounds!,
|
||||
onConfirm: () {
|
||||
// Navigate to map management screen with selected bounds
|
||||
final bounds = mapProvider.downloadAreaBounds!;
|
||||
final zoom = _mapController.camera.zoom.round();
|
||||
|
||||
// Find matching layer from MapLayer.allLayers to avoid instance mismatch
|
||||
// Only pass initialLayer if it's in allLayers (standard layers only)
|
||||
MapLayer? initialLayer;
|
||||
try {
|
||||
initialLayer = MapLayer.allLayers.firstWhere(
|
||||
(layer) => layer.type == _currentLayer.type,
|
||||
);
|
||||
} catch (e) {
|
||||
// Current layer not in allLayers (WMS/vector), don't pass it
|
||||
initialLayer = null;
|
||||
}
|
||||
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MapManagementScreen(
|
||||
tileCacheService: _tileCache,
|
||||
initialBounds: bounds,
|
||||
initialLayer: initialLayer,
|
||||
initialZoom: zoom,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Exit download area selection mode
|
||||
mapProvider.exitDownloadAreaMode();
|
||||
},
|
||||
onCancel: () {
|
||||
// Exit download area selection mode
|
||||
mapProvider.exitDownloadAreaMode();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
// Exit fullscreen button - top left (only shown in fullscreen mode)
|
||||
if (_isFullscreen)
|
||||
Positioned(
|
||||
|
||||
@@ -67,6 +67,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Uint8List? _previewCompressedBytes;
|
||||
bool _isPreviewLoading = false;
|
||||
bool _showCurrentImagePreview = true;
|
||||
bool _fastLocationUpdatesEnabled = false;
|
||||
double _fastLocationMovementThresholdMeters = 10.0;
|
||||
int _fastLocationActiveCadenceSeconds = 10;
|
||||
final ImagePicker _imagePicker = ImagePicker();
|
||||
final LocationTrackingService _locationService = LocationTrackingService();
|
||||
|
||||
@@ -81,6 +84,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_loadVoiceBitratePreference();
|
||||
_loadRouteHashSizePreference();
|
||||
_loadImagePreferences();
|
||||
_loadFastLocationSettings();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -169,6 +173,108 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
await _refreshImageModePreview();
|
||||
}
|
||||
|
||||
Future<void> _loadFastLocationSettings() async {
|
||||
await _locationService.loadSettings();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_fastLocationUpdatesEnabled = _locationService.fastLocationUpdatesEnabled;
|
||||
_fastLocationMovementThresholdMeters =
|
||||
_locationService.fastLocationMovementThresholdMeters;
|
||||
_fastLocationActiveCadenceSeconds =
|
||||
_locationService.fastLocationActiveCadenceSeconds;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _setFastLocationUpdatesEnabled(bool enabled) async {
|
||||
await _locationService.setFastLocationUpdatesEnabled(enabled);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_fastLocationUpdatesEnabled = _locationService.fastLocationUpdatesEnabled;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _editFastLocationMovementThreshold() async {
|
||||
final controller = TextEditingController(
|
||||
text: _fastLocationMovementThresholdMeters.toStringAsFixed(0),
|
||||
);
|
||||
final value = await showDialog<double>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Fast GPS movement threshold'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Meters',
|
||||
helperText: 'Valid range: 1 to 1000 meters',
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final parsed = double.tryParse(controller.text.trim());
|
||||
if (parsed == null) return;
|
||||
Navigator.pop(context, parsed.clamp(1.0, 1000.0));
|
||||
},
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (value == null) return;
|
||||
await _locationService.updateFastLocationMovementThreshold(value);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_fastLocationMovementThresholdMeters =
|
||||
_locationService.fastLocationMovementThresholdMeters;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _editFastLocationActiveCadence() async {
|
||||
final controller = TextEditingController(
|
||||
text: _fastLocationActiveCadenceSeconds.toString(),
|
||||
);
|
||||
final value = await showDialog<int>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Fast GPS active-use interval'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Seconds',
|
||||
helperText: 'Valid range: 5 to 60 seconds',
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final parsed = int.tryParse(controller.text.trim());
|
||||
if (parsed == null) return;
|
||||
Navigator.pop(context, parsed.clamp(5, 60));
|
||||
},
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (value == null) return;
|
||||
await _locationService.updateFastLocationActiveCadenceSeconds(value);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_fastLocationActiveCadenceSeconds =
|
||||
_locationService.fastLocationActiveCadenceSeconds;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveImageMaxSize(int size) async {
|
||||
await ImagePreferences.setMaxSize(size);
|
||||
if (!mounted) return;
|
||||
@@ -1018,6 +1124,31 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
_buildSectionHeader(AppLocalizations.of(context)!.permissionsSection),
|
||||
_buildSettingsCard([
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.gps_fixed),
|
||||
title: const Text('Fast private GPS updates'),
|
||||
subtitle: const Text(
|
||||
'Use private zero-hop updates while moving significantly or while actively using map/messages.',
|
||||
),
|
||||
value: _fastLocationUpdatesEnabled,
|
||||
onChanged: _setFastLocationUpdatesEnabled,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.straighten),
|
||||
title: const Text('Movement threshold'),
|
||||
subtitle: Text(
|
||||
'${_fastLocationMovementThresholdMeters.toStringAsFixed(0)} m',
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: _editFastLocationMovementThreshold,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.timer),
|
||||
title: const Text('Active-use update interval'),
|
||||
subtitle: Text('$_fastLocationActiveCadenceSeconds s'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: _editFastLocationActiveCadence,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.location_on),
|
||||
title: Text(AppLocalizations.of(context)!.locationPermission),
|
||||
|
||||
Reference in New Issue
Block a user