Upgrade flutter packages and limit

This commit is contained in:
Janez T
2026-03-07 20:59:01 +01:00
parent fe2e08c4e4
commit 1c3af4f927
23 changed files with 743 additions and 2825 deletions

View File

@@ -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

View File

@@ -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(

View File

@@ -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),