dev mode, imperial units incl. custom scalebar

This commit is contained in:
stopflock
2026-02-06 20:28:08 -06:00
parent 8804fdadf4
commit 2620c8758e
21 changed files with 811 additions and 243 deletions
+6 -1
View File
@@ -133,6 +133,7 @@ class AppState extends ChangeNotifier {
bool get isNavigationSearchLoading => _navigationState.isSearchLoading;
List<SearchResult> get navigationSearchResults => _navigationState.searchResults;
int get navigationAvoidanceDistance => _settingsState.navigationAvoidanceDistance;
DistanceUnit get distanceUnit => _settingsState.distanceUnit;
// Profile state
List<NodeProfile> get profiles => _profileState.profiles;
@@ -736,7 +737,11 @@ class AppState extends ChangeNotifier {
/// Set navigation avoidance distance
Future<void> setNavigationAvoidanceDistance(int distance) async {
await _settingsState.setNavigationAvoidanceDistance(distance);
}
}
Future<void> setDistanceUnit(DistanceUnit unit) async {
await _settingsState.setDistanceUnit(unit);
}
// ---------- Queue Methods ----------
void clearQueue() {
+1 -1
View File
@@ -72,7 +72,7 @@ const Duration kOverpassQueryTimeout = Duration(seconds: 45); // Timeout for Ove
const String kSuspectedLocationsCsvUrl = 'https://alprwatch.org/suspected-locations/deflock-latest.csv';
// Development/testing features - set to false for production builds
const bool kEnableDevelopmentModes = true; // Set to false to hide sandbox/simulate modes and force production mode
const bool kEnableDevelopmentModes = false; // Set to false to hide sandbox/simulate modes and force production mode
// Navigation features - set to false to hide navigation UI elements while in development
const bool kEnableNavigationFeatures = true; // Hide navigation until fully implemented
+22 -11
View File
@@ -47,12 +47,16 @@
},
"settings": {
"title": "Einstellungen",
"language": "Sprache",
"language": "Sprache & Region",
"systemDefault": "Systemstandard",
"aboutInfo": "Über / Informationen",
"aboutThisApp": "Über Diese App",
"aboutSubtitle": "App-Informationen und Credits",
"languageSubtitle": "Wählen Sie Ihre bevorzugte Sprache",
"languageSubtitle": "Wählen Sie Ihre bevorzugte Sprache und Einheiten",
"distanceUnit": "Entfernungseinheiten",
"distanceUnitSubtitle": "Wählen Sie zwischen metrischen (km/m) oder imperialen (mi/ft) Einheiten",
"metricUnits": "Metrisch (km, m)",
"imperialUnits": "Imperial (mi, ft)",
"maxNodes": "Max. angezeigte Knoten",
"maxNodesSubtitle": "Obergrenze für die Anzahl der Knoten auf der Karte festlegen.",
"maxNodesWarning": "Sie möchten das wahrscheinlich nicht tun, es sei denn, Sie sind absolut sicher, dass Sie einen guten Grund dafür haben.",
@@ -82,8 +86,7 @@
"enableNotifications": "Benachrichtigungen Aktivieren",
"checkingPermissions": "Berechtigungen prüfen...",
"alertDistance": "Warnentfernung: ",
"meters": "Meter",
"rangeInfo": "Bereich: {}-{} Meter (Standard: {})"
"rangeInfo": "Bereich: {}-{} {} (Standard: {})"
},
"node": {
"title": "Knoten #{}",
@@ -498,13 +501,7 @@
"avoidanceDistance": "Vermeidungsabstand",
"avoidanceDistanceSubtitle": "Mindestabstand zu Überwachungsgeräten",
"searchHistory": "Max. Suchverlauf",
"searchHistorySubtitle": "Maximale Anzahl kürzlicher Suchen zum Merken",
"units": "Einheiten",
"unitsSubtitle": "Anzeigeeinheiten für Entfernungen und Messungen",
"metric": "Metrisch (km, m)",
"imperial": "Britisch (mi, ft)",
"meters": "Meter",
"feet": "Fuß"
"searchHistorySubtitle": "Maximale Anzahl kürzlicher Suchen zum Merken"
},
"suspectedLocations": {
"title": "Verdächtige Standorte",
@@ -540,5 +537,19 @@
"url": "URL",
"coordinates": "Koordinaten",
"noAddressAvailable": "Keine Adresse verfügbar"
},
"units": {
"meters": "m",
"feet": "ft",
"kilometers": "km",
"miles": "mi",
"metersLong": "Meter",
"feetLong": "Fuß",
"kilometersLong": "Kilometer",
"milesLong": "Meilen",
"metric": "Metrisch",
"imperial": "Imperial",
"metricDescription": "Metrisch (km, m)",
"imperialDescription": "Imperial (mi, ft)"
}
}
+22 -11
View File
@@ -84,12 +84,16 @@
},
"settings": {
"title": "Settings",
"language": "Language",
"language": "Language & Region",
"systemDefault": "System Default",
"aboutInfo": "About / Info",
"aboutThisApp": "About This App",
"aboutSubtitle": "App information and credits",
"languageSubtitle": "Choose your preferred language",
"languageSubtitle": "Choose your preferred language and units",
"distanceUnit": "Distance Units",
"distanceUnitSubtitle": "Choose between metric (km/m) or imperial (mi/ft) units",
"metricUnits": "Metric (km, m)",
"imperialUnits": "Imperial (mi, ft)",
"maxNodes": "Max nodes drawn",
"maxNodesSubtitle": "Set an upper limit for the number of nodes on the map.",
"maxNodesWarning": "You probably don't want to do that unless you are absolutely sure you have a good reason for it.",
@@ -119,8 +123,7 @@
"enableNotifications": "Enable Notifications",
"checkingPermissions": "Checking permissions...",
"alertDistance": "Alert distance: ",
"meters": "meters",
"rangeInfo": "Range: {}-{} meters (default: {})"
"rangeInfo": "Range: {}-{} {} (default: {})"
},
"node": {
"title": "Node #{}",
@@ -498,13 +501,7 @@
"avoidanceDistance": "Avoidance Distance",
"avoidanceDistanceSubtitle": "Minimum distance to stay away from surveillance devices",
"searchHistory": "Max Search History",
"searchHistorySubtitle": "Maximum number of recent searches to remember",
"units": "Units",
"unitsSubtitle": "Display units for distances and measurements",
"metric": "Metric (km, m)",
"imperial": "Imperial (mi, ft)",
"meters": "meters",
"feet": "feet"
"searchHistorySubtitle": "Maximum number of recent searches to remember"
},
"suspectedLocations": {
"title": "Suspected Locations",
@@ -540,5 +537,19 @@
"url": "URL",
"coordinates": "Coordinates",
"noAddressAvailable": "No address available"
},
"units": {
"meters": "m",
"feet": "ft",
"kilometers": "km",
"miles": "mi",
"metersLong": "meters",
"feetLong": "feet",
"kilometersLong": "kilometers",
"milesLong": "miles",
"metric": "Metric",
"imperial": "Imperial",
"metricDescription": "Metric (km, m)",
"imperialDescription": "Imperial (mi, ft)"
}
}
+22 -11
View File
@@ -84,12 +84,16 @@
},
"settings": {
"title": "Configuración",
"language": "Idioma",
"language": "Idioma y Región",
"systemDefault": "Sistema por Defecto",
"aboutInfo": "Acerca de / Información",
"aboutThisApp": "Acerca de Esta App",
"aboutSubtitle": "Información de la aplicación y créditos",
"languageSubtitle": "Elige tu idioma preferido",
"languageSubtitle": "Elige tu idioma preferido y unidades",
"distanceUnit": "Unidades de Distancia",
"distanceUnitSubtitle": "Elige entre unidades métricas (km/m) o imperiales (mi/ft)",
"metricUnits": "Métrico (km, m)",
"imperialUnits": "Imperial (mi, ft)",
"maxNodes": "Máx. nodos dibujados",
"maxNodesSubtitle": "Establecer un límite superior para el número de nodos en el mapa.",
"maxNodesWarning": "Probablemente no quieras hacer eso a menos que estés absolutamente seguro de que tienes una buena razón para ello.",
@@ -119,8 +123,7 @@
"enableNotifications": "Habilitar Notificaciones",
"checkingPermissions": "Verificando permisos...",
"alertDistance": "Distancia de alerta: ",
"meters": "metros",
"rangeInfo": "Rango: {}-{} metros (predeterminado: {})"
"rangeInfo": "Rango: {}-{} {} (predeterminado: {})"
},
"node": {
"title": "Nodo #{}",
@@ -498,13 +501,7 @@
"avoidanceDistance": "Distancia de evitación",
"avoidanceDistanceSubtitle": "Distancia mínima para mantenerse alejado de dispositivos de vigilancia",
"searchHistory": "Historial máximo de búsqueda",
"searchHistorySubtitle": "Número máximo de búsquedas recientes para recordar",
"units": "Unidades",
"unitsSubtitle": "Unidades de visualización para distancias y medidas",
"metric": "Métrico (km, m)",
"imperial": "Imperial (mi, ft)",
"meters": "metros",
"feet": "pies"
"searchHistorySubtitle": "Número máximo de búsquedas recientes para recordar"
},
"suspectedLocations": {
"title": "Ubicaciones Sospechosas",
@@ -540,5 +537,19 @@
"url": "URL",
"coordinates": "Coordenadas",
"noAddressAvailable": "No hay dirección disponible"
},
"units": {
"meters": "m",
"feet": "ft",
"kilometers": "km",
"miles": "mi",
"metersLong": "metros",
"feetLong": "pies",
"kilometersLong": "kilómetros",
"milesLong": "millas",
"metric": "Métrico",
"imperial": "Imperial",
"metricDescription": "Métrico (km, m)",
"imperialDescription": "Imperial (mi, ft)"
}
}
+21 -10
View File
@@ -89,7 +89,11 @@
"aboutInfo": "À Propos / Informations",
"aboutThisApp": "À Propos de Cette App",
"aboutSubtitle": "Informations sur l'application et crédits",
"languageSubtitle": "Choisissez votre langue préférée",
"languageSubtitle": "Choisissez votre langue préférée et unités",
"distanceUnit": "Unités de Distance",
"distanceUnitSubtitle": "Choisir entre unités métriques (km/m) ou impériales (mi/ft)",
"metricUnits": "Métrique (km, m)",
"imperialUnits": "Impérial (mi, ft)",
"maxNodes": "Max. nœuds dessinés",
"maxNodesSubtitle": "Définir une limite supérieure pour le nombre de nœuds sur la carte.",
"maxNodesWarning": "Vous ne voulez probablement pas faire cela à moins d'être absolument sûr d'avoir une bonne raison de le faire.",
@@ -119,8 +123,7 @@
"enableNotifications": "Activer les Notifications",
"checkingPermissions": "Vérification des autorisations...",
"alertDistance": "Distance d'alerte : ",
"meters": "mètres",
"rangeInfo": "Plage : {}-{} mètres (par défaut : {})"
"rangeInfo": "Plage : {}-{} {} (par défaut : {})"
},
"node": {
"title": "Nœud #{}",
@@ -498,13 +501,7 @@
"avoidanceDistance": "Distance d'évitement",
"avoidanceDistanceSubtitle": "Distance minimale pour éviter les dispositifs de surveillance",
"searchHistory": "Historique de recherche max",
"searchHistorySubtitle": "Nombre maximum de recherches récentes à retenir",
"units": "Unités",
"unitsSubtitle": "Unités d'affichage pour distances et mesures",
"metric": "Métrique (km, m)",
"imperial": "Impérial (mi, ft)",
"meters": "mètres",
"feet": "pieds"
"searchHistorySubtitle": "Nombre maximum de recherches récentes à retenir"
},
"suspectedLocations": {
"title": "Emplacements Suspects",
@@ -540,5 +537,19 @@
"url": "URL",
"coordinates": "Coordonnées",
"noAddressAvailable": "Aucune adresse disponible"
},
"units": {
"meters": "m",
"feet": "ft",
"kilometers": "km",
"miles": "mi",
"metersLong": "mètres",
"feetLong": "pieds",
"kilometersLong": "kilomètres",
"milesLong": "milles",
"metric": "Métrique",
"imperial": "Impérial",
"metricDescription": "Métrique (km, m)",
"imperialDescription": "Impérial (mi, ft)"
}
}
+21 -10
View File
@@ -89,7 +89,11 @@
"aboutInfo": "Informazioni",
"aboutThisApp": "Informazioni su questa App",
"aboutSubtitle": "Informazioni sull'applicazione e crediti",
"languageSubtitle": "Scegli la tua lingua preferita",
"languageSubtitle": "Scegli la tua lingua preferita e unità",
"distanceUnit": "Unità di Distanza",
"distanceUnitSubtitle": "Scegli tra unità metriche (km/m) o imperiali (mi/ft)",
"metricUnits": "Metrico (km, m)",
"imperialUnits": "Imperiale (mi, ft)",
"maxNodes": "Max nodi disegnati",
"maxNodesSubtitle": "Imposta un limite superiore per il numero di nodi sulla mappa.",
"maxNodesWarning": "Probabilmente non vuoi farlo a meno che non sei assolutamente sicuro di avere una buona ragione per farlo.",
@@ -119,8 +123,7 @@
"enableNotifications": "Abilita Notifiche",
"checkingPermissions": "Controllo autorizzazioni...",
"alertDistance": "Distanza di avviso: ",
"meters": "metri",
"rangeInfo": "Intervallo: {}-{} metri (predefinito: {})"
"rangeInfo": "Intervallo: {}-{} {} (predefinito: {})"
},
"node": {
"title": "Nodo #{}",
@@ -498,13 +501,7 @@
"avoidanceDistance": "Distanza di evitamento",
"avoidanceDistanceSubtitle": "Distanza minima da mantenere dai dispositivi di sorveglianza",
"searchHistory": "Cronologia ricerca max",
"searchHistorySubtitle": "Numero massimo di ricerche recenti da ricordare",
"units": "Unità",
"unitsSubtitle": "Unità di visualizzazione per distanze e misure",
"metric": "Metrico (km, m)",
"imperial": "Imperiale (mi, ft)",
"meters": "metri",
"feet": "piedi"
"searchHistorySubtitle": "Numero massimo di ricerche recenti da ricordare"
},
"suspectedLocations": {
"title": "Posizioni Sospette",
@@ -540,5 +537,19 @@
"url": "URL",
"coordinates": "Coordinate",
"noAddressAvailable": "Nessun indirizzo disponibile"
},
"units": {
"meters": "m",
"feet": "ft",
"kilometers": "km",
"miles": "mi",
"metersLong": "metri",
"feetLong": "piedi",
"kilometersLong": "chilometri",
"milesLong": "miglia",
"metric": "Metrico",
"imperial": "Imperiale",
"metricDescription": "Metrico (km, m)",
"imperialDescription": "Imperiale (mi, ft)"
}
}
+21 -10
View File
@@ -89,7 +89,11 @@
"aboutInfo": "Sobre / Informações",
"aboutThisApp": "Sobre este App",
"aboutSubtitle": "Informações do aplicativo e créditos",
"languageSubtitle": "Escolha seu idioma preferido",
"languageSubtitle": "Escolha seu idioma preferido e unidades",
"distanceUnit": "Unidades de Distância",
"distanceUnitSubtitle": "Escolha entre unidades métricas (km/m) ou imperiais (mi/ft)",
"metricUnits": "Métrico (km, m)",
"imperialUnits": "Imperial (mi, ft)",
"maxNodes": "Máx. de nós desenhados",
"maxNodesSubtitle": "Definir um limite superior para o número de nós no mapa.",
"maxNodesWarning": "Você provavelmente não quer fazer isso a menos que tenha certeza absoluta de que tem uma boa razão para isso.",
@@ -119,8 +123,7 @@
"enableNotifications": "Habilitar Notificações",
"checkingPermissions": "Verificando permissões...",
"alertDistance": "Distância de alerta: ",
"meters": "metros",
"rangeInfo": "Faixa: {}-{} metros (padrão: {})"
"rangeInfo": "Faixa: {}-{} {} (padrão: {})"
},
"node": {
"title": "Nó #{}",
@@ -498,13 +501,7 @@
"avoidanceDistance": "Distância de evasão",
"avoidanceDistanceSubtitle": "Distância mínima para ficar longe de dispositivos de vigilância",
"searchHistory": "Histórico máximo de busca",
"searchHistorySubtitle": "Número máximo de buscas recentes para lembrar",
"units": "Unidades",
"unitsSubtitle": "Unidades de exibição para distâncias e medidas",
"metric": "Métrico (km, m)",
"imperial": "Imperial (mi, ft)",
"meters": "metros",
"feet": "pés"
"searchHistorySubtitle": "Número máximo de buscas recentes para lembrar"
},
"suspectedLocations": {
"title": "Localizações Suspeitas",
@@ -540,5 +537,19 @@
"url": "URL",
"coordinates": "Coordenadas",
"noAddressAvailable": "Nenhum endereço disponível"
},
"units": {
"meters": "m",
"feet": "ft",
"kilometers": "km",
"miles": "mi",
"metersLong": "metros",
"feetLong": "pés",
"kilometersLong": "quilômetros",
"milesLong": "milhas",
"metric": "Métrico",
"imperial": "Imperial",
"metricDescription": "Métrico (km, m)",
"imperialDescription": "Imperial (mi, ft)"
}
}
+21 -10
View File
@@ -89,7 +89,11 @@
"aboutInfo": "关于 / 信息",
"aboutThisApp": "关于此应用",
"aboutSubtitle": "应用程序信息和鸣谢",
"languageSubtitle": "选择您的首选语言",
"languageSubtitle": "选择您的首选语言和单位",
"distanceUnit": "距离单位",
"distanceUnitSubtitle": "选择公制 (公里/米) 或英制 (英里/英尺) 单位",
"metricUnits": "公制 (公里, 米)",
"imperialUnits": "英制 (英里, 英尺)",
"maxNodes": "最大节点绘制数",
"maxNodesSubtitle": "设置地图上节点数量的上限。",
"maxNodesWarning": "除非您确定有充分的理由,否则您可能不想这样做。",
@@ -119,8 +123,7 @@
"enableNotifications": "启用通知",
"checkingPermissions": "检查权限中...",
"alertDistance": "警报距离:",
"meters": "米",
"rangeInfo": "范围:{}-{} 米(默认:{}"
"rangeInfo": "范围:{}-{} {}(默认:{}"
},
"node": {
"title": "节点 #{}",
@@ -498,13 +501,7 @@
"avoidanceDistance": "回避距离",
"avoidanceDistanceSubtitle": "与监控设备保持的最小距离",
"searchHistory": "最大搜索历史",
"searchHistorySubtitle": "要记住的最近搜索次数",
"units": "单位",
"unitsSubtitle": "距离和测量的显示单位",
"metric": "公制(公里,米)",
"imperial": "英制(英里,英尺)",
"meters": "米",
"feet": "英尺"
"searchHistorySubtitle": "要记住的最近搜索次数"
},
"suspectedLocations": {
"title": "疑似位置",
@@ -540,5 +537,19 @@
"url": "网址",
"coordinates": "坐标",
"noAddressAvailable": "无可用地址"
},
"units": {
"meters": "米",
"feet": "英尺",
"kilometers": "公里",
"miles": "英里",
"metersLong": "米",
"feetLong": "英尺",
"kilometersLong": "公里",
"milesLong": "英里",
"metric": "公制",
"imperial": "英制",
"metricDescription": "公制 (公里, 米)",
"imperialDescription": "英制 (英里, 英尺)"
}
}
+101 -65
View File
@@ -1,79 +1,115 @@
import 'package:flutter/material.dart';
import '../services/localization_service.dart';
import '../services/distance_service.dart';
import '../app_state.dart';
import '../state/settings_state.dart';
import 'package:provider/provider.dart';
class NavigationSettingsScreen extends StatelessWidget {
class NavigationSettingsScreen extends StatefulWidget {
const NavigationSettingsScreen({super.key});
@override
State<NavigationSettingsScreen> createState() => _NavigationSettingsScreenState();
}
class _NavigationSettingsScreenState extends State<NavigationSettingsScreen> {
late TextEditingController _distanceController;
@override
void initState() {
super.initState();
final appState = context.read<AppState>();
final displayValue = DistanceService.convertFromMeters(
appState.navigationAvoidanceDistance.toDouble(),
appState.distanceUnit
);
_distanceController = TextEditingController(
text: displayValue.round().toString(),
);
}
@override
void dispose() {
_distanceController.dispose();
super.dispose();
}
void _updateDistance(AppState appState, String value) {
final displayValue = double.tryParse(value) ?? (appState.distanceUnit == DistanceUnit.metric ? 250.0 : 820.0);
final metersValue = DistanceService.convertToMeters(displayValue, appState.distanceUnit, isSmallDistance: true);
appState.setNavigationAvoidanceDistance(metersValue.round().clamp(0, 2000));
}
@override
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
final locService = LocalizationService.instance;
return AnimatedBuilder(
animation: LocalizationService.instance,
builder: (context, child) => Scaffold(
appBar: AppBar(
title: Text(locService.t('navigation.navigationSettings')),
),
body: Padding(
padding: EdgeInsets.fromLTRB(
16,
16,
16,
16 + MediaQuery.of(context).padding.bottom,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: const Icon(Icons.social_distance),
title: Text(locService.t('navigation.avoidanceDistance')),
subtitle: Text(locService.t('navigation.avoidanceDistanceSubtitle')),
trailing: SizedBox(
width: 80,
child: TextFormField(
initialValue: appState.navigationAvoidanceDistance.toString(),
keyboardType: const TextInputType.numberWithOptions(signed: false, decimal: false),
textInputAction: TextInputAction.done,
decoration: const InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 6, horizontal: 8),
border: OutlineInputBorder(),
suffixText: 'm',
return Consumer<AppState>(
builder: (context, appState, child) {
// Update the text field when the unit or distance changes
final displayValue = DistanceService.convertFromMeters(
appState.navigationAvoidanceDistance.toDouble(),
appState.distanceUnit
);
if (_distanceController.text != displayValue.round().toString()) {
_distanceController.text = displayValue.round().toString();
}
final locService = LocalizationService.instance;
return AnimatedBuilder(
animation: LocalizationService.instance,
builder: (context, child) {
return Scaffold(
appBar: AppBar(
title: Text(locService.t('navigation.navigationSettings')),
),
body: Padding(
padding: EdgeInsets.fromLTRB(
16,
16,
16,
16 + MediaQuery.of(context).padding.bottom,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
leading: const Icon(Icons.social_distance),
title: Text(locService.t('navigation.avoidanceDistance')),
subtitle: Text(locService.t('navigation.avoidanceDistanceSubtitle')),
trailing: SizedBox(
width: 80,
child: TextField(
controller: _distanceController,
keyboardType: const TextInputType.numberWithOptions(signed: false, decimal: false),
textInputAction: TextInputAction.done,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
border: const OutlineInputBorder(),
suffixText: DistanceService.getSmallDistanceUnit(appState.distanceUnit),
),
onSubmitted: (value) => _updateDistance(appState, value),
onEditingComplete: () => _updateDistance(appState, _distanceController.text),
)
)
),
onFieldSubmitted: (value) {
final distance = int.tryParse(value) ?? 250;
appState.setNavigationAvoidanceDistance(distance.clamp(0, 2000));
}
)
)
const Divider(),
_buildDisabledSetting(
context,
icon: Icons.history,
title: locService.t('navigation.searchHistory'),
subtitle: locService.t('navigation.searchHistorySubtitle'),
value: '10 searches',
),
],
),
),
const Divider(),
_buildDisabledSetting(
context,
icon: Icons.history,
title: locService.t('navigation.searchHistory'),
subtitle: locService.t('navigation.searchHistorySubtitle'),
value: '10 searches',
),
const Divider(),
_buildDisabledSetting(
context,
icon: Icons.straighten,
title: locService.t('navigation.units'),
subtitle: locService.t('navigation.unitsSubtitle'),
value: locService.t('navigation.metric'),
),
],
),
),
),
);
},
);
},
);
}
@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:provider/provider.dart';
import '../../../services/localization_service.dart';
import '../../../app_state.dart';
import '../../../state/settings_state.dart';
class LanguageSection extends StatefulWidget {
const LanguageSection({super.key});
@@ -49,41 +52,91 @@ class _LanguageSectionState extends State<LanguageSection> {
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: LocalizationService.instance,
builder: (context, child) {
final locService = LocalizationService.instance;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// System Default option
RadioListTile<String?>(
title: Text(locService.t('settings.systemDefault')),
value: null,
groupValue: _selectedLanguage,
onChanged: _setLanguage,
),
// English always appears second (if available)
if (locService.availableLanguages.contains('en'))
RadioListTile<String>(
title: Text(_languageNames['en'] ?? 'English'),
value: 'en',
groupValue: _selectedLanguage,
onChanged: _setLanguage,
return Consumer<AppState>(
builder: (context, appState, child) {
return AnimatedBuilder(
animation: LocalizationService.instance,
builder: (context, child) {
final locService = LocalizationService.instance;
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Language section
// System Default option
RadioListTile<String?>(
title: Text(locService.t('settings.systemDefault')),
value: null,
groupValue: _selectedLanguage,
onChanged: _setLanguage,
),
// English always appears second (if available)
if (locService.availableLanguages.contains('en'))
RadioListTile<String>(
title: Text(_languageNames['en'] ?? 'English'),
value: 'en',
groupValue: _selectedLanguage,
onChanged: _setLanguage,
),
// Other language options (excluding English since it's already shown)
...locService.availableLanguages
.where((langCode) => langCode != 'en')
.map((langCode) =>
RadioListTile<String>(
title: Text(_languageNames[langCode] ?? langCode.toUpperCase()),
value: langCode,
groupValue: _selectedLanguage,
onChanged: _setLanguage,
),
),
// Divider between language and units
const SizedBox(height: 24),
const Divider(),
const SizedBox(height: 16),
// Distance Units section
Text(
locService.t('settings.distanceUnit'),
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(
locService.t('settings.distanceUnitSubtitle'),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color?.withOpacity(0.7),
),
),
const SizedBox(height: 8),
// Metric option
RadioListTile<DistanceUnit>(
title: Text(locService.t('units.metricDescription')),
value: DistanceUnit.metric,
groupValue: appState.distanceUnit,
onChanged: (unit) {
if (unit != null) {
appState.setDistanceUnit(unit);
}
},
),
// Imperial option
RadioListTile<DistanceUnit>(
title: Text(locService.t('units.imperialDescription')),
value: DistanceUnit.imperial,
groupValue: appState.distanceUnit,
onChanged: (unit) {
if (unit != null) {
appState.setDistanceUnit(unit);
}
},
),
],
),
// Other language options (excluding English since it's already shown)
...locService.availableLanguages
.where((langCode) => langCode != 'en')
.map((langCode) =>
RadioListTile<String>(
title: Text(_languageNames[langCode] ?? langCode.toUpperCase()),
value: langCode,
groupValue: _selectedLanguage,
onChanged: _setLanguage,
),
),
],
);
},
);
},
);
@@ -5,6 +5,8 @@ import 'package:provider/provider.dart';
import '../../../app_state.dart';
import '../../../services/localization_service.dart';
import '../../../services/proximity_alert_service.dart';
import '../../../services/distance_service.dart';
import '../../../state/settings_state.dart';
import '../../../dev_config.dart';
/// Settings section for proximity alerts configuration
@@ -25,8 +27,13 @@ class _ProximityAlertsSectionState extends State<ProximityAlertsSection> {
void initState() {
super.initState();
final appState = context.read<AppState>();
// Convert meters to display units for the text field
final displayValue = DistanceService.convertFromMeters(
appState.proximityAlertDistance.toDouble(),
appState.distanceUnit
);
_distanceController = TextEditingController(
text: appState.proximityAlertDistance.toString(),
text: displayValue.round().toString(),
);
_checkNotificationPermissions();
}
@@ -69,12 +76,18 @@ class _ProximityAlertsSectionState extends State<ProximityAlertsSection> {
void _updateDistance(AppState appState) {
final text = _distanceController.text.trim();
final distance = int.tryParse(text);
if (distance != null) {
appState.setProximityAlertDistance(distance);
final displayValue = double.tryParse(text);
if (displayValue != null) {
// Convert from display units back to meters for storage
final metersValue = DistanceService.convertToMeters(displayValue, appState.distanceUnit, isSmallDistance: true);
appState.setProximityAlertDistance(metersValue.round());
} else {
// Reset to current value if invalid
_distanceController.text = appState.proximityAlertDistance.toString();
final displayValue = DistanceService.convertFromMeters(
appState.proximityAlertDistance.toDouble(),
appState.distanceUnit
);
_distanceController.text = displayValue.round().toString();
}
}
@@ -84,6 +97,15 @@ class _ProximityAlertsSectionState extends State<ProximityAlertsSection> {
builder: (context, appState, child) {
final locService = LocalizationService.instance;
// Update the text field when the unit or distance changes
final displayValue = DistanceService.convertFromMeters(
appState.proximityAlertDistance.toDouble(),
appState.distanceUnit
);
if (_distanceController.text != displayValue.round().toString()) {
_distanceController.text = displayValue.round().toString();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -171,49 +193,50 @@ class _ProximityAlertsSectionState extends State<ProximityAlertsSection> {
),
],
// Distance setting (only show when enabled)
if (appState.proximityAlertsEnabled) ...[
const SizedBox(height: 12),
Row(
children: [
Text(locService.t('proximityAlerts.alertDistance')),
SizedBox(
width: 80,
child: TextField(
controller: _distanceController,
keyboardType: const TextInputType.numberWithOptions(signed: true, decimal: true),
textInputAction: TextInputAction.done,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
decoration: const InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 8,
// Distance setting (only show when enabled)
if (appState.proximityAlertsEnabled) ...[
const SizedBox(height: 12),
Row(
children: [
Text(locService.t('proximityAlerts.alertDistance')),
SizedBox(
width: 80,
child: TextField(
controller: _distanceController,
keyboardType: const TextInputType.numberWithOptions(signed: true, decimal: true),
textInputAction: TextInputAction.done,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
decoration: const InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 8,
),
border: OutlineInputBorder(),
),
border: OutlineInputBorder(),
onSubmitted: (_) => _updateDistance(appState),
onEditingComplete: () => _updateDistance(appState),
),
onSubmitted: (_) => _updateDistance(appState),
onEditingComplete: () => _updateDistance(appState),
),
),
const SizedBox(width: 8),
Text(locService.t('proximityAlerts.meters')),
],
),
const SizedBox(height: 8),
Text(
locService.t('proximityAlerts.rangeInfo', params: [
kProximityAlertMinDistance.toString(),
kProximityAlertMaxDistance.toString(),
kProximityAlertDefaultDistance.toString(),
]),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color?.withOpacity(0.6),
const SizedBox(width: 8),
Text(locService.t('units.${appState.distanceUnit == DistanceUnit.metric ? 'metersLong' : 'feetLong'}')),
],
),
),
],
const SizedBox(height: 8),
Text(
locService.t('proximityAlerts.rangeInfo', params: [
DistanceService.convertFromMeters(kProximityAlertMinDistance.toDouble(), appState.distanceUnit).round().toString(),
DistanceService.convertFromMeters(kProximityAlertMaxDistance.toDouble(), appState.distanceUnit).round().toString(),
locService.t('units.${appState.distanceUnit == DistanceUnit.metric ? 'metersLong' : 'feetLong'}'),
DistanceService.convertFromMeters(kProximityAlertDefaultDistance.toDouble(), appState.distanceUnit).round().toString(),
]),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color?.withOpacity(0.6),
),
),
],
],
);
},
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../../app_state.dart';
import '../../../services/localization_service.dart';
import '../../../services/distance_service.dart';
import '../../../state/settings_state.dart';
class SuspectedLocationsSection extends StatefulWidget {
const SuspectedLocationsSection({super.key});
@@ -188,22 +190,28 @@ class _SuspectedLocationsSectionState extends State<SuspectedLocationsSection> {
ListTile(
leading: const Icon(Icons.social_distance),
title: Text(locService.t('suspectedLocations.minimumDistance')),
subtitle: Text(locService.t('suspectedLocations.minimumDistanceSubtitle', params: [appState.suspectedLocationMinDistance.toString()])),
subtitle: Text(locService.t('suspectedLocations.minimumDistanceSubtitle', params: [
DistanceService.formatDistance(appState.suspectedLocationMinDistance.toDouble(), appState.distanceUnit)
])),
trailing: SizedBox(
width: 80,
child: TextFormField(
initialValue: appState.suspectedLocationMinDistance.toString(),
initialValue: DistanceService.convertFromMeters(
appState.suspectedLocationMinDistance.toDouble(),
appState.distanceUnit
).round().toString(),
keyboardType: const TextInputType.numberWithOptions(signed: true, decimal: true),
textInputAction: TextInputAction.done,
decoration: const InputDecoration(
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 6, horizontal: 8),
border: OutlineInputBorder(),
suffixText: 'm',
contentPadding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
border: const OutlineInputBorder(),
suffixText: DistanceService.getSmallDistanceUnit(appState.distanceUnit),
),
onFieldSubmitted: (value) {
final distance = int.tryParse(value) ?? 100;
appState.setSuspectedLocationMinDistance(distance.clamp(0, 1000));
final displayValue = double.tryParse(value) ?? (appState.distanceUnit == DistanceUnit.metric ? 100.0 : 328.0);
final metersValue = DistanceService.convertToMeters(displayValue, appState.distanceUnit, isSmallDistance: true);
appState.setSuspectedLocationMinDistance(metersValue.round().clamp(0, 1000));
},
),
),
+88
View File
@@ -0,0 +1,88 @@
import '../state/settings_state.dart';
/// Service for distance unit conversions and formatting
///
/// Follows brutalist principles: simple, explicit conversions without fancy abstractions.
/// All APIs work in metric units (meters/km), this service only handles display formatting.
class DistanceService {
// Conversion constants
static const double _metersToFeet = 3.28084;
static const double _metersToMiles = 0.000621371;
static const double _kmToMiles = 0.621371;
/// Format distance for display based on unit preference
///
/// For metric: uses meters for < 1000m, kilometers for >= 1000m
/// For imperial: uses feet for < 5280ft (1 mile), miles for >= 5280ft
static String formatDistance(double distanceInMeters, DistanceUnit unit) {
switch (unit) {
case DistanceUnit.metric:
if (distanceInMeters < 1000) {
return '${distanceInMeters.round()} m';
} else {
return '${(distanceInMeters / 1000).toStringAsFixed(1)} km';
}
case DistanceUnit.imperial:
final distanceInFeet = distanceInMeters * _metersToFeet;
if (distanceInFeet < 5280) {
return '${distanceInFeet.round()} ft';
} else {
final distanceInMiles = distanceInMeters * _metersToMiles;
return '${distanceInMiles.toStringAsFixed(1)} mi';
}
}
}
/// Format large distances (like route distances) for display
///
/// Always uses the larger unit (km/miles) for routes
static String formatRouteDistance(double distanceInMeters, DistanceUnit unit) {
switch (unit) {
case DistanceUnit.metric:
return '${(distanceInMeters / 1000).toStringAsFixed(1)} km';
case DistanceUnit.imperial:
final distanceInMiles = distanceInMeters * _metersToMiles;
return '${distanceInMiles.toStringAsFixed(1)} mi';
}
}
/// Get the unit suffix for small distances (used in form fields, etc.)
static String getSmallDistanceUnit(DistanceUnit unit) {
switch (unit) {
case DistanceUnit.metric:
return 'm';
case DistanceUnit.imperial:
return 'ft';
}
}
/// Convert displayed distance value back to meters for API usage
///
/// This is for form fields where users enter values in their preferred units
static double convertToMeters(double value, DistanceUnit unit, {bool isSmallDistance = true}) {
switch (unit) {
case DistanceUnit.metric:
return isSmallDistance ? value : value * 1000; // m or km to m
case DistanceUnit.imperial:
if (isSmallDistance) {
return value / _metersToFeet; // ft to m
} else {
return value / _metersToMiles; // miles to m
}
}
}
/// Convert meters to the preferred small distance unit for form display
static double convertFromMeters(double meters, DistanceUnit unit) {
switch (unit) {
case DistanceUnit.metric:
return meters;
case DistanceUnit.imperial:
return meters * _metersToFeet;
}
}
}
+27
View File
@@ -16,6 +16,12 @@ enum FollowMeMode {
rotating, // Follow position and rotation based on heading
}
// Enum for distance units
enum DistanceUnit {
metric, // kilometers, meters
imperial, // miles, feet
}
class SettingsState extends ChangeNotifier {
static const String _offlineModePrefsKey = 'offline_mode';
static const String _maxNodesPrefsKey = 'max_nodes';
@@ -30,6 +36,7 @@ class SettingsState extends ChangeNotifier {
static const String _suspectedLocationMinDistancePrefsKey = 'suspected_location_min_distance';
static const String _pauseQueueProcessingPrefsKey = 'pause_queue_processing';
static const String _navigationAvoidanceDistancePrefsKey = 'navigation_avoidance_distance';
static const String _distanceUnitPrefsKey = 'distance_unit';
bool _offlineMode = false;
bool _pauseQueueProcessing = false;
@@ -43,6 +50,7 @@ class SettingsState extends ChangeNotifier {
List<TileProvider> _tileProviders = [];
String _selectedTileTypeId = '';
int _navigationAvoidanceDistance = 250; // meters
DistanceUnit _distanceUnit = DistanceUnit.metric;
// Getters
bool get offlineMode => _offlineMode;
@@ -57,6 +65,7 @@ class SettingsState extends ChangeNotifier {
List<TileProvider> get tileProviders => List.unmodifiable(_tileProviders);
String get selectedTileTypeId => _selectedTileTypeId;
int get navigationAvoidanceDistance => _navigationAvoidanceDistance;
DistanceUnit get distanceUnit => _distanceUnit;
/// Get the currently selected tile type
TileType? get selectedTileType {
@@ -109,6 +118,14 @@ class SettingsState extends ChangeNotifier {
_navigationAvoidanceDistance = prefs.getInt(_navigationAvoidanceDistancePrefsKey) ?? 250;
}
// Load distance unit
if (prefs.containsKey(_distanceUnitPrefsKey)) {
final unitIndex = prefs.getInt(_distanceUnitPrefsKey) ?? 0;
if (unitIndex >= 0 && unitIndex < DistanceUnit.values.length) {
_distanceUnit = DistanceUnit.values[unitIndex];
}
}
// Load proximity alerts settings
_proximityAlertsEnabled = prefs.getBool(_proximityAlertsEnabledPrefsKey) ?? false;
_proximityAlertDistance = prefs.getInt(_proximityAlertDistancePrefsKey) ?? kProximityAlertDefaultDistance;
@@ -369,4 +386,14 @@ class SettingsState extends ChangeNotifier {
}
}
/// Set distance unit (metric or imperial)
Future<void> setDistanceUnit(DistanceUnit unit) async {
if (_distanceUnit != unit) {
_distanceUnit = unit;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_distanceUnitPrefsKey, unit.index);
notifyListeners();
}
}
}
+250
View File
@@ -0,0 +1,250 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
import '../app_state.dart';
import '../state/settings_state.dart';
/// Custom scale bar widget that respects user's distance unit preference
///
/// Replaces flutter_map's built-in Scalebar to support metric/imperial units.
/// Uses the existing DistanceUnit enum from SettingsState.
///
/// Based on the brutalist code philosophy: simple, explicit, maintainable.
class CustomScaleBar extends StatelessWidget {
const CustomScaleBar({
super.key,
this.maxWidthPx = 120,
this.barHeight = 8,
this.padding = const EdgeInsets.all(10),
this.alignment = Alignment.bottomLeft,
this.textStyle,
});
final double maxWidthPx;
final double barHeight;
final EdgeInsets padding;
final Alignment alignment;
final TextStyle? textStyle;
@override
Widget build(BuildContext context) {
return Consumer<AppState>(
builder: (context, appState, child) {
final camera = MapCamera.of(context);
final center = camera.center;
final zoom = camera.zoom;
// Calculate meters represented by maxWidthPx at current zoom around map center
final maxMeters = _metersForPixelSpan(camera, center, zoom, maxWidthPx);
// Calculate nice intervals in the display unit for better user experience
final niceMeters = _niceDistanceInDisplayUnit(maxMeters, appState.distanceUnit);
// Calculate actual bar width in pixels
final metersPerPx = maxMeters / maxWidthPx;
final barWidthPx = (niceMeters / metersPerPx).clamp(1.0, maxWidthPx);
// Format the label based on user's unit preference
final label = _formatLabel(niceMeters, appState.distanceUnit);
// Use styling that matches the original flutter_map scale bar
final style = textStyle ??
const TextStyle(
fontSize: 12,
color: Colors.black,
fontWeight: FontWeight.bold,
);
return Align(
alignment: alignment,
child: Padding(
padding: padding,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: style),
const SizedBox(height: 2),
CustomPaint(
size: Size(barWidthPx, barHeight + 6),
painter: _ScaleBarPainter(barHeight: barHeight),
),
],
),
),
);
},
);
}
/// Calculate the real-world distance represented by a pixel span at the current zoom level
///
/// Uses a simple approach: calculate the distance between two points that are
/// separated by the pixel span at the map center latitude.
double _metersForPixelSpan(
MapCamera camera,
LatLng center,
double zoom,
double pixelSpan,
) {
// At the equator, 1 degree of longitude = ~111,320 meters
// At other latitudes, it's scaled by cos(latitude)
const metersPerDegreeAtEquator = 111320.0;
final metersPerDegreeLongitude = metersPerDegreeAtEquator * math.cos(center.latitude * math.pi / 180);
// Calculate degrees per pixel at this zoom level
// Web Mercator: 360 degrees spans 2^zoom tiles, each tile is 256 pixels
final tilesAtZoom = math.pow(2, zoom);
final pixelsAtZoom = tilesAtZoom * 256;
final degreesPerPixel = 360.0 / pixelsAtZoom;
// Calculate the longitude span represented by our pixel span
final longitudeSpan = degreesPerPixel * pixelSpan;
// Convert to meters
return longitudeSpan * metersPerDegreeLongitude;
}
/// Convert a maximum distance to a "nice" rounded distance in the display unit
///
/// For metric: Nice intervals like 1m, 2m, 5m, 10m, 1km, 2km, 5km
/// For imperial: Nice intervals like 1ft, 2ft, 5ft, 10ft, 1mi, 2mi, 5mi
double _niceDistanceInDisplayUnit(double maxMeters, DistanceUnit unit) {
if (maxMeters <= 0) return 0;
switch (unit) {
case DistanceUnit.metric:
return _calculateNiceDistance(maxMeters, [
// Small metric intervals (meters)
1, 2, 5, 10, 20, 50, 100, 200, 500,
// Large metric intervals (kilometers, converted to meters)
1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000,
1000000, 2000000, 5000000, 10000000,
]);
case DistanceUnit.imperial:
const feetToMeters = 0.3048;
const milesToMeters = 1609.34;
return _calculateNiceDistance(maxMeters, [
// Small imperial intervals (feet, converted to meters)
1 * feetToMeters,
2 * feetToMeters,
5 * feetToMeters,
10 * feetToMeters,
20 * feetToMeters,
50 * feetToMeters,
100 * feetToMeters,
200 * feetToMeters,
500 * feetToMeters,
1000 * feetToMeters,
2000 * feetToMeters,
5000 * feetToMeters,
// Large imperial intervals (miles, converted to meters)
1 * milesToMeters,
2 * milesToMeters,
5 * milesToMeters,
10 * milesToMeters,
20 * milesToMeters,
50 * milesToMeters,
100 * milesToMeters,
200 * milesToMeters,
500 * milesToMeters,
1000 * milesToMeters,
]);
}
}
/// Find the largest "nice" distance that fits within the maximum
double _calculateNiceDistance(double maxMeters, List<double> intervals) {
// Find the largest interval that's still smaller than our max
for (int i = intervals.length - 1; i >= 0; i--) {
if (intervals[i] <= maxMeters) {
return intervals[i];
}
}
// Fallback to smallest interval if none fit
return intervals.first;
}
/// Format the distance label according to the user's unit preference
///
/// Uses the same logic as DistanceService for consistency:
/// - Metric: meters < 1000m, kilometers ≥ 1000m
/// - Imperial: feet < 5280ft, miles ≥ 5280ft
String _formatLabel(double meters, DistanceUnit unit) {
switch (unit) {
case DistanceUnit.metric:
if (meters >= 1000) {
final km = meters / 1000.0;
return '${_trim(km)} km';
}
return '${meters.round()} m';
case DistanceUnit.imperial:
final feet = meters * 3.28084;
if (feet >= 5280) {
final miles = feet / 5280.0;
return '${_trim(miles)} mi';
}
return '${feet.round()} ft';
}
}
/// Trim unnecessary decimal places from distance values
String _trim(double v) {
if (v >= 100) return v.toStringAsFixed(0);
if (v >= 10) return v.toStringAsFixed(1).replaceAll(RegExp(r'\.0$'), '');
return v
.toStringAsFixed(2)
.replaceAll(RegExp(r'0+$'), '')
.replaceAll(RegExp(r'\.$'), '');
}
}
/// Custom painter for drawing the scale bar
///
/// Draws a simple horizontal line with vertical end markers,
/// matching the style of the original flutter_map scale bar.
class _ScaleBarPainter extends CustomPainter {
_ScaleBarPainter({required this.barHeight});
final double barHeight;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.black
..strokeWidth = 3 // Match original scale bar stroke width
..style = PaintingStyle.stroke;
final yTop = 2.0;
final yBottom = yTop + barHeight;
// Draw horizontal base line
canvas.drawLine(Offset(0, yBottom), Offset(size.width, yBottom), paint);
// Draw left vertical marker
canvas.drawLine(Offset(0, yTop), Offset(0, yBottom), paint);
// Draw right vertical marker
canvas.drawLine(Offset(size.width, yTop), Offset(size.width, yBottom), paint);
// Draw middle marker for longer scales (visual clarity)
if (size.width >= 40) {
final midX = size.width / 2;
canvas.drawLine(
Offset(midX, yBottom - barHeight * 0.6),
Offset(midX, yBottom),
paint,
);
}
}
@override
bool shouldRepaint(covariant _ScaleBarPainter oldDelegate) =>
oldDelegate.barHeight != barHeight;
}
+5 -6
View File
@@ -30,6 +30,7 @@ import 'proximity_alert_banner.dart';
import '../dev_config.dart';
import '../services/proximity_alert_service.dart';
import 'sheet_aware_map.dart';
import 'custom_scale_bar.dart';
class MapView extends StatefulWidget {
final AnimatedMapController controller;
@@ -498,20 +499,18 @@ class MapViewState extends State<MapView> {
selectedTileType: appState.selectedTileType,
),
cameraLayers,
// Built-in scale bar from flutter_map, positioned relative to button bar with safe area
// Custom scale bar that respects user's distance unit preference
Builder(
builder: (context) {
final safeArea = MediaQuery.of(context).padding;
return Scalebar(
return CustomScaleBar(
alignment: Alignment.bottomLeft,
padding: EdgeInsets.only(
left: leftPositionWithSafeArea(8, safeArea),
bottom: bottomPositionFromButtonBar(kScaleBarSpacingAboveButtonBar, safeArea.bottom)
),
textStyle: TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
lineColor: Colors.black,
strokeWidth: 3,
// backgroundColor removed in flutter_map >=8 (wrap in Container if needed)
maxWidthPx: 120,
barHeight: 8,
);
},
),
+4 -3
View File
@@ -6,6 +6,7 @@ import 'package:latlong2/latlong.dart';
import '../app_state.dart';
import '../dev_config.dart';
import '../services/localization_service.dart';
import '../services/distance_service.dart';
class NavigationSheet extends StatelessWidget {
final VoidCallback? onStartRoute;
@@ -166,7 +167,7 @@ class NavigationSheet extends StatelessWidget {
// Show distance from first point
if (appState.distanceFromFirstPoint != null) ...[
Text(
'Distance: ${(appState.distanceFromFirstPoint! / 1000).toStringAsFixed(1)} km',
'Distance: ${DistanceService.formatRouteDistance(appState.distanceFromFirstPoint!.toDouble(), appState.distanceUnit)}',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
@@ -192,7 +193,7 @@ class NavigationSheet extends StatelessWidget {
const SizedBox(width: 8),
Expanded(
child: Text(
'Trips longer than ${(kNavigationDistanceWarningThreshold / 1000).toStringAsFixed(0)} km are likely to time out. We are working to improve this; stay tuned.',
'Trips longer than ${DistanceService.formatRouteDistance(kNavigationDistanceWarningThreshold.toDouble(), appState.distanceUnit)} are likely to time out. We are working to improve this; stay tuned.',
style: TextStyle(
fontSize: 14,
color: Colors.amber[700],
@@ -343,7 +344,7 @@ class NavigationSheet extends StatelessWidget {
],
if (appState.routeDistance != null) ...[
Text(
LocalizationService.instance.t('navigation.distance', params: [(appState.routeDistance! / 1000).toStringAsFixed(1)]),
'Distance: ${DistanceService.formatRouteDistance(appState.routeDistance!.toDouble(), appState.distanceUnit)}',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),