Merge branch 'zjs81:main' into main

This commit is contained in:
Ben Allfree
2026-02-24 13:26:23 -08:00
committed by GitHub
44 changed files with 1771 additions and 191 deletions
+22
View File
@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
class LosIcon extends StatelessWidget {
final double size;
final Color? color;
const LosIcon({super.key, this.size = 24, this.color});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final iconTheme = IconTheme.of(context);
final iconColor =
color ??
iconTheme.color ??
theme.iconTheme.color ??
theme.colorScheme.onSurface;
return Icon(Symbols.elevation, size: size, color: iconColor);
}
}
+28 -2
View File
@@ -1,4 +1,6 @@
{ {
"channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } },
"@@locale": "bg", "@@locale": "bg",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Контакти", "nav_contacts": "Контакти",
@@ -1718,5 +1720,29 @@
"losPointName": "Име на точката", "losPointName": "Име на точката",
"losShowPanelTooltip": "Показване на LOS панел", "losShowPanelTooltip": "Показване на LOS панел",
"losHidePanelTooltip": "Скриване на LOS панела", "losHidePanelTooltip": "Скриване на LOS панела",
"losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)" "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)",
} "losLegendRadioHorizon": "Радиохоризонт",
"losLegendLosBeam": "Линия на видимост",
"losLegendTerrain": "Терен",
"losFrequencyLabel": "Честота",
"losFrequencyInfoTooltip": "Преглед на детайли за изчислението",
"losFrequencyDialogTitle": "Изчисляване на радиохоризонта",
"losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
}
}
+28 -2
View File
@@ -1,4 +1,6 @@
{ {
"channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } },
"@@locale": "de", "@@locale": "de",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Kontakte", "nav_contacts": "Kontakte",
@@ -1746,5 +1748,29 @@
"losPointName": "Punktname", "losPointName": "Punktname",
"losShowPanelTooltip": "LOS-Panel anzeigen", "losShowPanelTooltip": "LOS-Panel anzeigen",
"losHidePanelTooltip": "LOS-Panel ausblenden", "losHidePanelTooltip": "LOS-Panel ausblenden",
"losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)" "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)",
} "losLegendRadioHorizon": "Funkhorizont",
"losLegendLosBeam": "Sichtlinie",
"losLegendTerrain": "Gelände",
"losFrequencyLabel": "Frequenz",
"losFrequencyInfoTooltip": "Details zur Berechnung anzeigen",
"losFrequencyDialogTitle": "Berechnung des Funkhorizonts",
"losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
}
}
+33 -1
View File
@@ -365,6 +365,14 @@
} }
} }
}, },
"channels_channelDeleteFailed": "Failed to delete channel \"{name}\"",
"@channels_channelDeleteFailed": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"channels_channelDeleted": "Channel \"{name}\" deleted", "channels_channelDeleted": "Channel \"{name}\" deleted",
"@channels_channelDeleted": { "@channels_channelDeleted": {
"placeholders": { "placeholders": {
@@ -1667,6 +1675,30 @@
"losShowPanelTooltip": "Show LOS panel", "losShowPanelTooltip": "Show LOS panel",
"losHidePanelTooltip": "Hide LOS panel", "losHidePanelTooltip": "Hide LOS panel",
"losElevationAttribution": "Elevation data: Open-Meteo (CC BY 4.0)", "losElevationAttribution": "Elevation data: Open-Meteo (CC BY 4.0)",
"losLegendRadioHorizon": "Radio horizon",
"losLegendLosBeam": "LOS beam",
"losLegendTerrain": "Terrain",
"losFrequencyLabel": "Frequency",
"losFrequencyInfoTooltip": "View calculation details",
"losFrequencyDialogTitle": "Radio horizon calculation",
"losFrequencyDialogDescription": "Starting from k={baselineK} at {baselineFreq} MHz, the calculation adjusts the k-factor for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
},
"contacts_pathTrace": "Path Trace", "contacts_pathTrace": "Path Trace",
"contacts_ping": "Ping", "contacts_ping": "Ping",
"contacts_repeaterPathTrace": "Path trace to repeater", "contacts_repeaterPathTrace": "Path trace to repeater",
@@ -1747,4 +1779,4 @@
"settings_gpxExportShareSubject": "meshcore-open GPX map data export", "settings_gpxExportShareSubject": "meshcore-open GPX map data export",
"snrIndicator_nearByRepeaters": "Nearby Repeaters", "snrIndicator_nearByRepeaters": "Nearby Repeaters",
"snrIndicator_lastSeen": "Last seen" "snrIndicator_lastSeen": "Last seen"
} }
+28 -2
View File
@@ -1,4 +1,6 @@
{ {
"channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } },
"@@locale": "es", "@@locale": "es",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Contactos", "nav_contacts": "Contactos",
@@ -1746,5 +1748,29 @@
"losPointName": "Nombre del punto", "losPointName": "Nombre del punto",
"losShowPanelTooltip": "Mostrar panel LOS", "losShowPanelTooltip": "Mostrar panel LOS",
"losHidePanelTooltip": "Ocultar panel LOS", "losHidePanelTooltip": "Ocultar panel LOS",
"losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)" "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)",
} "losLegendRadioHorizon": "Horizonte radioeléctrico",
"losLegendLosBeam": "Línea de visión",
"losLegendTerrain": "Terreno",
"losFrequencyLabel": "Frecuencia",
"losFrequencyInfoTooltip": "Ver detalles del cálculo",
"losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico",
"losFrequencyDialogDescription": "A partir de k={baselineK} en {baselineFreq} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
}
}
+28 -2
View File
@@ -1,4 +1,6 @@
{ {
"channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } },
"@@locale": "fr", "@@locale": "fr",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Contacts", "nav_contacts": "Contacts",
@@ -1718,5 +1720,29 @@
"losPointName": "Nom du point", "losPointName": "Nom du point",
"losShowPanelTooltip": "Afficher le panneau LOS", "losShowPanelTooltip": "Afficher le panneau LOS",
"losHidePanelTooltip": "Masquer le panneau LOS", "losHidePanelTooltip": "Masquer le panneau LOS",
"losElevationAttribution": "Données d'altitude : Open-Meteo (CC BY 4.0)" "losElevationAttribution": "Données daltitude : Open-Meteo (CC BY 4.0)",
} "losLegendRadioHorizon": "Horizon radio",
"losLegendLosBeam": "Ligne de visée",
"losLegendTerrain": "Terrain",
"losFrequencyLabel": "Fréquence",
"losFrequencyInfoTooltip": "Voir les détails du calcul",
"losFrequencyDialogTitle": "Calcul de lhorizon radio",
"losFrequencyDialogDescription": "À partir de k={baselineK} à {baselineFreq} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
}
}
+28 -2
View File
@@ -1,4 +1,6 @@
{ {
"channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } },
"@@locale": "it", "@@locale": "it",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Contatti", "nav_contacts": "Contatti",
@@ -1718,5 +1720,29 @@
"losPointName": "Nome del punto", "losPointName": "Nome del punto",
"losShowPanelTooltip": "Mostra il pannello LOS", "losShowPanelTooltip": "Mostra il pannello LOS",
"losHidePanelTooltip": "Nascondi il pannello LOS", "losHidePanelTooltip": "Nascondi il pannello LOS",
"losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)" "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)",
} "losLegendRadioHorizon": "Orizzonte radio",
"losLegendLosBeam": "Linea di vista",
"losLegendTerrain": "Terreno",
"losFrequencyLabel": "Frequenza",
"losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo",
"losFrequencyDialogTitle": "Calcolo dellorizzonte radio",
"losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
}
}
+53
View File
@@ -1582,6 +1582,12 @@ abstract class AppLocalizations {
/// **'Delete \"{name}\"? This cannot be undone.'** /// **'Delete \"{name}\"? This cannot be undone.'**
String channels_deleteChannelConfirm(String name); String channels_deleteChannelConfirm(String name);
/// No description provided for @channels_channelDeleteFailed.
///
/// In en, this message translates to:
/// **'Failed to delete channel \"{name}\"'**
String channels_channelDeleteFailed(String name);
/// No description provided for @channels_channelDeleted. /// No description provided for @channels_channelDeleted.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -5004,6 +5010,53 @@ abstract class AppLocalizations {
/// **'Elevation data: Open-Meteo (CC BY 4.0)'** /// **'Elevation data: Open-Meteo (CC BY 4.0)'**
String get losElevationAttribution; String get losElevationAttribution;
/// No description provided for @losLegendRadioHorizon.
///
/// In en, this message translates to:
/// **'Radio horizon'**
String get losLegendRadioHorizon;
/// No description provided for @losLegendLosBeam.
///
/// In en, this message translates to:
/// **'LOS beam'**
String get losLegendLosBeam;
/// No description provided for @losLegendTerrain.
///
/// In en, this message translates to:
/// **'Terrain'**
String get losLegendTerrain;
/// No description provided for @losFrequencyLabel.
///
/// In en, this message translates to:
/// **'Frequency'**
String get losFrequencyLabel;
/// No description provided for @losFrequencyInfoTooltip.
///
/// In en, this message translates to:
/// **'View calculation details'**
String get losFrequencyInfoTooltip;
/// No description provided for @losFrequencyDialogTitle.
///
/// In en, this message translates to:
/// **'Radio horizon calculation'**
String get losFrequencyDialogTitle;
/// Explain how the calculation uses the baseline frequency and derived k-factor.
///
/// In en, this message translates to:
/// **'Starting from k={baselineK} at {baselineFreq} MHz, the calculation adjusts the k-factor for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.'**
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
);
/// No description provided for @contacts_pathTrace. /// No description provided for @contacts_pathTrace.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+33
View File
@@ -820,6 +820,11 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Изтрий \"$name\"? Това не може да бъде отменено.'; return 'Изтрий \"$name\"? Това не може да бъде отменено.';
} }
@override
String channels_channelDeleteFailed(String name) {
return 'Неуспешно изтриване на канала \"$name\"';
}
@override @override
String channels_channelDeleted(String name) { String channels_channelDeleted(String name) {
return 'Каналът \"$name\" е изтрит'; return 'Каналът \"$name\" е изтрит';
@@ -2867,6 +2872,34 @@ class AppLocalizationsBg extends AppLocalizations {
String get losElevationAttribution => String get losElevationAttribution =>
'Данни за надморска височина: Open-Meteo (CC BY 4.0)'; 'Данни за надморска височина: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Радиохоризонт';
@override
String get losLegendLosBeam => 'Линия на видимост';
@override
String get losLegendTerrain => 'Терен';
@override
String get losFrequencyLabel => 'Честота';
@override
String get losFrequencyInfoTooltip => 'Преглед на детайли за изчислението';
@override
String get losFrequencyDialogTitle => 'Изчисляване на радиохоризонта';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.';
}
@override @override
String get contacts_pathTrace => 'Пътен проследяване'; String get contacts_pathTrace => 'Пътен проследяване';
+33
View File
@@ -817,6 +817,11 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Löschen von \"$name\"? Dies kann nicht rückgängig gemacht werden.'; return 'Löschen von \"$name\"? Dies kann nicht rückgängig gemacht werden.';
} }
@override
String channels_channelDeleteFailed(String name) {
return 'Kanal $name konnte nicht gelöscht werden';
}
@override @override
String channels_channelDeleted(String name) { String channels_channelDeleted(String name) {
return 'Kanal \"$name\" gelöscht'; return 'Kanal \"$name\" gelöscht';
@@ -2873,6 +2878,34 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)'; String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Funkhorizont';
@override
String get losLegendLosBeam => 'Sichtlinie';
@override
String get losLegendTerrain => 'Gelände';
@override
String get losFrequencyLabel => 'Frequenz';
@override
String get losFrequencyInfoTooltip => 'Details zur Berechnung anzeigen';
@override
String get losFrequencyDialogTitle => 'Berechnung des Funkhorizonts';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Ausgehend von k=$baselineK bei $baselineFreq MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.';
}
@override @override
String get contacts_pathTrace => 'Pfadverfolgung'; String get contacts_pathTrace => 'Pfadverfolgung';
+33
View File
@@ -808,6 +808,11 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Delete \"$name\"? This cannot be undone.'; return 'Delete \"$name\"? This cannot be undone.';
} }
@override
String channels_channelDeleteFailed(String name) {
return 'Failed to delete channel \"$name\"';
}
@override @override
String channels_channelDeleted(String name) { String channels_channelDeleted(String name) {
return 'Channel \"$name\" deleted'; return 'Channel \"$name\" deleted';
@@ -2824,6 +2829,34 @@ class AppLocalizationsEn extends AppLocalizations {
String get losElevationAttribution => String get losElevationAttribution =>
'Elevation data: Open-Meteo (CC BY 4.0)'; 'Elevation data: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Radio horizon';
@override
String get losLegendLosBeam => 'LOS beam';
@override
String get losLegendTerrain => 'Terrain';
@override
String get losFrequencyLabel => 'Frequency';
@override
String get losFrequencyInfoTooltip => 'View calculation details';
@override
String get losFrequencyDialogTitle => 'Radio horizon calculation';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation adjusts the k-factor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.';
}
@override @override
String get contacts_pathTrace => 'Path Trace'; String get contacts_pathTrace => 'Path Trace';
+33
View File
@@ -818,6 +818,11 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Eliminar \"$name\"? Esto no se puede deshacer.'; return 'Eliminar \"$name\"? Esto no se puede deshacer.';
} }
@override
String channels_channelDeleteFailed(String name) {
return 'No se pudo eliminar el canal \"$name\"';
}
@override @override
String channels_channelDeleted(String name) { String channels_channelDeleted(String name) {
return 'Canal \"$name\" eliminado'; return 'Canal \"$name\" eliminado';
@@ -2867,6 +2872,34 @@ class AppLocalizationsEs extends AppLocalizations {
String get losElevationAttribution => String get losElevationAttribution =>
'Datos de elevación: Open-Meteo (CC BY 4.0)'; 'Datos de elevación: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Horizonte radioeléctrico';
@override
String get losLegendLosBeam => 'Línea de visión';
@override
String get losLegendTerrain => 'Terreno';
@override
String get losFrequencyLabel => 'Frecuencia';
@override
String get losFrequencyInfoTooltip => 'Ver detalles del cálculo';
@override
String get losFrequencyDialogTitle => 'Cálculo del horizonte radioeléctrico';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'A partir de k=$baselineK en $baselineFreq MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.';
}
@override @override
String get contacts_pathTrace => 'Rastreo de caminos'; String get contacts_pathTrace => 'Rastreo de caminos';
+34 -1
View File
@@ -820,6 +820,11 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Supprimer $name? Cela ne peut pas être annulé.'; return 'Supprimer $name? Cela ne peut pas être annulé.';
} }
@override
String channels_channelDeleteFailed(String name) {
return 'Échec de la suppression de la chaîne \"$name\"';
}
@override @override
String channels_channelDeleted(String name) { String channels_channelDeleted(String name) {
return 'Le canal \"$name\" a été supprimé'; return 'Le canal \"$name\" a été supprimé';
@@ -2880,7 +2885,35 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get losElevationAttribution => String get losElevationAttribution =>
'Données d\'altitude : Open-Meteo (CC BY 4.0)'; 'Données daltitude : Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Horizon radio';
@override
String get losLegendLosBeam => 'Ligne de visée';
@override
String get losLegendTerrain => 'Terrain';
@override
String get losFrequencyLabel => 'Fréquence';
@override
String get losFrequencyInfoTooltip => 'Voir les détails du calcul';
@override
String get losFrequencyDialogTitle => 'Calcul de lhorizon radio';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'À partir de k=$baselineK à $baselineFreq MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.';
}
@override @override
String get contacts_pathTrace => 'Traçage de chemin'; String get contacts_pathTrace => 'Traçage de chemin';
+33
View File
@@ -816,6 +816,11 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Eliminare \"$name\"? Non può essere annullato.'; return 'Eliminare \"$name\"? Non può essere annullato.';
} }
@override
String channels_channelDeleteFailed(String name) {
return 'Impossibile eliminare il canale \"$name\"';
}
@override @override
String channels_channelDeleted(String name) { String channels_channelDeleted(String name) {
return 'Canale \"$name\" eliminato'; return 'Canale \"$name\" eliminato';
@@ -2867,6 +2872,34 @@ class AppLocalizationsIt extends AppLocalizations {
String get losElevationAttribution => String get losElevationAttribution =>
'Dati di elevazione: Open-Meteo (CC BY 4.0)'; 'Dati di elevazione: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Orizzonte radio';
@override
String get losLegendLosBeam => 'Linea di vista';
@override
String get losLegendTerrain => 'Terreno';
@override
String get losFrequencyLabel => 'Frequenza';
@override
String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo';
@override
String get losFrequencyDialogTitle => 'Calcolo dellorizzonte radio';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo regola il fattore k per l\'attuale banda $frequencyMHz MHz, che definisce il limite curvo dell\'orizzonte radio.';
}
@override @override
String get contacts_pathTrace => 'Traccia Percorso'; String get contacts_pathTrace => 'Traccia Percorso';
+33
View File
@@ -813,6 +813,11 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Verwijderen \"$name\"? Dit kan niet worden teruggedraaid.'; return 'Verwijderen \"$name\"? Dit kan niet worden teruggedraaid.';
} }
@override
String channels_channelDeleteFailed(String name) {
return 'Kan kanaal $name niet verwijderen';
}
@override @override
String channels_channelDeleted(String name) { String channels_channelDeleted(String name) {
return 'Kanaal \"$name\" verwijderd'; return 'Kanaal \"$name\" verwijderd';
@@ -2857,6 +2862,34 @@ class AppLocalizationsNl extends AppLocalizations {
String get losElevationAttribution => String get losElevationAttribution =>
'Hoogtegegevens: Open-Meteo (CC BY 4.0)'; 'Hoogtegegevens: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Radiohorizon';
@override
String get losLegendLosBeam => 'Zichtlijn';
@override
String get losLegendTerrain => 'Terrein';
@override
String get losFrequencyLabel => 'Frequentie';
@override
String get losFrequencyInfoTooltip => 'Bekijk details van de berekening';
@override
String get losFrequencyDialogTitle => 'Berekening van de radiohorizon';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Beginnend met k=$baselineK bij $baselineFreq MHz, wordt bij de berekening de k-factor aangepast voor de huidige $frequencyMHz MHz-band, die de gebogen radiohorizonkap definieert.';
}
@override @override
String get contacts_pathTrace => 'Pad Traceren'; String get contacts_pathTrace => 'Pad Traceren';
+33
View File
@@ -818,6 +818,11 @@ class AppLocalizationsPl extends AppLocalizations {
return 'Usuń \"$name\"? Nie można tego cofnąć.'; return 'Usuń \"$name\"? Nie można tego cofnąć.';
} }
@override
String channels_channelDeleteFailed(String name) {
return 'Nie udało się usunąć kanału \"$name\"';
}
@override @override
String channels_channelDeleted(String name) { String channels_channelDeleted(String name) {
return 'Kanał \"$name\" usunięto'; return 'Kanał \"$name\" usunięto';
@@ -2863,6 +2868,34 @@ class AppLocalizationsPl extends AppLocalizations {
String get losElevationAttribution => String get losElevationAttribution =>
'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)'; 'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Horyzont radiowy';
@override
String get losLegendLosBeam => 'Linia widoczności';
@override
String get losLegendTerrain => 'Teren';
@override
String get losFrequencyLabel => 'Częstotliwość';
@override
String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczenia';
@override
String get losFrequencyDialogTitle => 'Obliczanie horyzontu radiowego';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.';
}
@override @override
String get contacts_pathTrace => 'Śledzenie Ścieżek'; String get contacts_pathTrace => 'Śledzenie Ścieżek';
+33
View File
@@ -819,6 +819,11 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Excluir \"$name\"? Não pode ser desfeito.'; return 'Excluir \"$name\"? Não pode ser desfeito.';
} }
@override
String channels_channelDeleteFailed(String name) {
return 'Falha ao excluir o canal \"$name\"';
}
@override @override
String channels_channelDeleted(String name) { String channels_channelDeleted(String name) {
return 'Canal \"$name\" excluído'; return 'Canal \"$name\" excluído';
@@ -2866,6 +2871,34 @@ class AppLocalizationsPt extends AppLocalizations {
String get losElevationAttribution => String get losElevationAttribution =>
'Dados de elevação: Open-Meteo (CC BY 4.0)'; 'Dados de elevação: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Horizonte de rádio';
@override
String get losLegendLosBeam => 'Linha de visada';
@override
String get losLegendTerrain => 'Terreno';
@override
String get losFrequencyLabel => 'Frequência';
@override
String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo';
@override
String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Começando em k=$baselineK em $baselineFreq MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.';
}
@override @override
String get contacts_pathTrace => 'Traçado de Caminho'; String get contacts_pathTrace => 'Traçado de Caminho';
+33
View File
@@ -817,6 +817,11 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Удалить \"$name\"? Это действие нельзя отменить.'; return 'Удалить \"$name\"? Это действие нельзя отменить.';
} }
@override
String channels_channelDeleteFailed(String name) {
return 'Не удалось удалить канал $name.';
}
@override @override
String channels_channelDeleted(String name) { String channels_channelDeleted(String name) {
return 'Канал \"$name\" удалён'; return 'Канал \"$name\" удалён';
@@ -2869,6 +2874,34 @@ class AppLocalizationsRu extends AppLocalizations {
String get losElevationAttribution => String get losElevationAttribution =>
'Данные о высоте: Open-Meteo (CC BY 4.0)'; 'Данные о высоте: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Радиогоризонт';
@override
String get losLegendLosBeam => 'Линия прямой видимости';
@override
String get losLegendTerrain => 'Рельеф';
@override
String get losFrequencyLabel => 'Частота';
@override
String get losFrequencyInfoTooltip => 'Просмотреть детали расчёта';
@override
String get losFrequencyDialogTitle => 'Расчёт радиогоризонта';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Начиная с k=$baselineK на частоте $baselineFreq МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.';
}
@override @override
String get contacts_pathTrace => 'Трассировка пути'; String get contacts_pathTrace => 'Трассировка пути';
+33
View File
@@ -813,6 +813,11 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Odstrániť \"$name\"? To sa nedá zrušiť.'; return 'Odstrániť \"$name\"? To sa nedá zrušiť.';
} }
@override
String channels_channelDeleteFailed(String name) {
return 'Kanál \"$name\" sa nepodarilo odstrániť';
}
@override @override
String channels_channelDeleted(String name) { String channels_channelDeleted(String name) {
return 'Kanál \"$name\" bol odstránený'; return 'Kanál \"$name\" bol odstránený';
@@ -2851,6 +2856,34 @@ class AppLocalizationsSk extends AppLocalizations {
String get losElevationAttribution => String get losElevationAttribution =>
'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)'; 'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Rádiový horizont';
@override
String get losLegendLosBeam => 'Priama viditeľnosť';
@override
String get losLegendTerrain => 'Terén';
@override
String get losFrequencyLabel => 'Frekvencia';
@override
String get losFrequencyInfoTooltip => 'Zobraziť podrobnosti výpočtu';
@override
String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Počnúc od k=$baselineK pri $baselineFreq MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.';
}
@override @override
String get contacts_pathTrace => 'Sledovanie lúčov'; String get contacts_pathTrace => 'Sledovanie lúčov';
+33
View File
@@ -811,6 +811,11 @@ class AppLocalizationsSl extends AppLocalizations {
return 'Izbrišem \"$name\"? To se ne da povrniti.'; return 'Izbrišem \"$name\"? To se ne da povrniti.';
} }
@override
String channels_channelDeleteFailed(String name) {
return 'Kanala $name ni bilo mogoče izbrisati';
}
@override @override
String channels_channelDeleted(String name) { String channels_channelDeleted(String name) {
return 'Kanal \"$name\" izbrisan.'; return 'Kanal \"$name\" izbrisan.';
@@ -2854,6 +2859,34 @@ class AppLocalizationsSl extends AppLocalizations {
String get losElevationAttribution => String get losElevationAttribution =>
'Podatki o višini: Open-Meteo (CC BY 4.0)'; 'Podatki o višini: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Radijski horizont';
@override
String get losLegendLosBeam => 'Linija vidnosti';
@override
String get losLegendTerrain => 'Teren';
@override
String get losFrequencyLabel => 'Frekvenca';
@override
String get losFrequencyInfoTooltip => 'Prikaži podrobnosti izračuna';
@override
String get losFrequencyDialogTitle => 'Izračun radijskega horizonta';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Začenši od k=$baselineK pri $baselineFreq MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.';
}
@override @override
String get contacts_pathTrace => 'Sledenje poti'; String get contacts_pathTrace => 'Sledenje poti';
+33
View File
@@ -807,6 +807,11 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Radera \"$name\"? Detta kan inte ångras.'; return 'Radera \"$name\"? Detta kan inte ångras.';
} }
@override
String channels_channelDeleteFailed(String name) {
return 'Det gick inte att ta bort kanalen \"$name\"';
}
@override @override
String channels_channelDeleted(String name) { String channels_channelDeleted(String name) {
return 'Kanalen \"$name\" raderad'; return 'Kanalen \"$name\" raderad';
@@ -2837,6 +2842,34 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)'; String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Radiohorisont';
@override
String get losLegendLosBeam => 'Siktlinje';
@override
String get losLegendTerrain => 'Terräng';
@override
String get losFrequencyLabel => 'Frekvens';
@override
String get losFrequencyInfoTooltip => 'Visa detaljer om beräkningen';
@override
String get losFrequencyDialogTitle => 'Beräkning av radiohorisonten';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Med start från k=$baselineK vid $baselineFreq MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.';
}
@override @override
String get contacts_pathTrace => 'Path Trace'; String get contacts_pathTrace => 'Path Trace';
+33
View File
@@ -815,6 +815,11 @@ class AppLocalizationsUk extends AppLocalizations {
return 'Видалити $name? Це не можна скасувати.'; return 'Видалити $name? Це не можна скасувати.';
} }
@override
String channels_channelDeleteFailed(String name) {
return 'Не вдалося видалити канал \"$name\"';
}
@override @override
String channels_channelDeleted(String name) { String channels_channelDeleted(String name) {
return 'Канал «$name» видалено'; return 'Канал «$name» видалено';
@@ -2877,6 +2882,34 @@ class AppLocalizationsUk extends AppLocalizations {
String get losElevationAttribution => String get losElevationAttribution =>
'Дані про висоту: Open-Meteo (CC BY 4.0)'; 'Дані про висоту: Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => 'Радіогоризонт';
@override
String get losLegendLosBeam => 'Лінія прямої видимості';
@override
String get losLegendTerrain => 'Рельєф';
@override
String get losFrequencyLabel => 'Частота';
@override
String get losFrequencyInfoTooltip => 'Переглянути деталі розрахунку';
@override
String get losFrequencyDialogTitle => 'Розрахунок радіогоризонту';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return 'Починаючи з k=$baselineK на $baselineFreq МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.';
}
@override @override
String get contacts_pathTrace => 'Трасування шляхів'; String get contacts_pathTrace => 'Трасування шляхів';
+33
View File
@@ -775,6 +775,11 @@ class AppLocalizationsZh extends AppLocalizations {
return 'Delete \"$name\"? This cannot be undone.'; return 'Delete \"$name\"? This cannot be undone.';
} }
@override
String channels_channelDeleteFailed(String name) {
return '无法删除频道 \"$name\"';
}
@override @override
String channels_channelDeleted(String name) { String channels_channelDeleted(String name) {
return '删除频道 \"$name\"'; return '删除频道 \"$name\"';
@@ -2715,6 +2720,34 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get losElevationAttribution => '高程数据:Open-Meteo (CC BY 4.0)'; String get losElevationAttribution => '高程数据:Open-Meteo (CC BY 4.0)';
@override
String get losLegendRadioHorizon => '无线电地平线';
@override
String get losLegendLosBeam => '视距波束';
@override
String get losLegendTerrain => '地形';
@override
String get losFrequencyLabel => '频率';
@override
String get losFrequencyInfoTooltip => '查看计算详情';
@override
String get losFrequencyDialogTitle => '无线电地平线计算';
@override
String losFrequencyDialogDescription(
double baselineK,
double baselineFreq,
double frequencyMHz,
double kFactor,
) {
return '$baselineFreq MHz 处的 k=$baselineK 开始,计算调整当前 $frequencyMHz MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。';
}
@override @override
String get contacts_pathTrace => '路径追踪'; String get contacts_pathTrace => '路径追踪';
+28 -2
View File
@@ -1,4 +1,6 @@
{ {
"channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } },
"@@locale": "nl", "@@locale": "nl",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Contacten", "nav_contacts": "Contacten",
@@ -1718,5 +1720,29 @@
"losPointName": "Puntnaam", "losPointName": "Puntnaam",
"losShowPanelTooltip": "Toon LOS-paneel", "losShowPanelTooltip": "Toon LOS-paneel",
"losHidePanelTooltip": "LOS-paneel verbergen", "losHidePanelTooltip": "LOS-paneel verbergen",
"losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)" "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)",
} "losLegendRadioHorizon": "Radiohorizon",
"losLegendLosBeam": "Zichtlijn",
"losLegendTerrain": "Terrein",
"losFrequencyLabel": "Frequentie",
"losFrequencyInfoTooltip": "Bekijk details van de berekening",
"losFrequencyDialogTitle": "Berekening van de radiohorizon",
"losFrequencyDialogDescription": "Beginnend met k={baselineK} bij {baselineFreq} MHz, wordt bij de berekening de k-factor aangepast voor de huidige {frequencyMHz} MHz-band, die de gebogen radiohorizonkap definieert.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
}
}
+28 -2
View File
@@ -1,4 +1,6 @@
{ {
"channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } },
"@@locale": "pl", "@@locale": "pl",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Kontakty", "nav_contacts": "Kontakty",
@@ -1718,5 +1720,29 @@
"losPointName": "Nazwa punktu", "losPointName": "Nazwa punktu",
"losShowPanelTooltip": "Pokaż panel LOS", "losShowPanelTooltip": "Pokaż panel LOS",
"losHidePanelTooltip": "Ukryj panel LOS", "losHidePanelTooltip": "Ukryj panel LOS",
"losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)" "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)",
} "losLegendRadioHorizon": "Horyzont radiowy",
"losLegendLosBeam": "Linia widoczności",
"losLegendTerrain": "Teren",
"losFrequencyLabel": "Częstotliwość",
"losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia",
"losFrequencyDialogTitle": "Obliczanie horyzontu radiowego",
"losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
}
}
+28 -2
View File
@@ -1,4 +1,6 @@
{ {
"channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } },
"@@locale": "pt", "@@locale": "pt",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Contactos", "nav_contacts": "Contactos",
@@ -1718,5 +1720,29 @@
"losPointName": "Nome do ponto", "losPointName": "Nome do ponto",
"losShowPanelTooltip": "Mostrar painel LOS", "losShowPanelTooltip": "Mostrar painel LOS",
"losHidePanelTooltip": "Ocultar painel LOS", "losHidePanelTooltip": "Ocultar painel LOS",
"losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)" "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)",
} "losLegendRadioHorizon": "Horizonte de rádio",
"losLegendLosBeam": "Linha de visada",
"losLegendTerrain": "Terreno",
"losFrequencyLabel": "Frequência",
"losFrequencyInfoTooltip": "Ver detalhes do cálculo",
"losFrequencyDialogTitle": "Cálculo do horizonte de rádio",
"losFrequencyDialogDescription": "Começando em k={baselineK} em {baselineFreq} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
}
}
+28 -2
View File
@@ -1,4 +1,6 @@
{ {
"channels_channelDeleteFailed": "Не удалось удалить канал {name}.",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } },
"@@locale": "ru", "@@locale": "ru",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Контакты", "nav_contacts": "Контакты",
@@ -958,5 +960,29 @@
"losPointName": "Имя точки", "losPointName": "Имя точки",
"losShowPanelTooltip": "Показать панель LOS", "losShowPanelTooltip": "Показать панель LOS",
"losHidePanelTooltip": "Скрыть панель LOS", "losHidePanelTooltip": "Скрыть панель LOS",
"losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)" "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)",
} "losLegendRadioHorizon": "Радиогоризонт",
"losLegendLosBeam": "Линия прямой видимости",
"losLegendTerrain": "Рельеф",
"losFrequencyLabel": "Частота",
"losFrequencyInfoTooltip": "Просмотреть детали расчёта",
"losFrequencyDialogTitle": "Расчёт радиогоризонта",
"losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {baselineFreq} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
}
}
+28 -2
View File
@@ -1,4 +1,6 @@
{ {
"channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } },
"@@locale": "sk", "@@locale": "sk",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Kontakty", "nav_contacts": "Kontakty",
@@ -1718,5 +1720,29 @@
"losPointName": "Názov bodu", "losPointName": "Názov bodu",
"losShowPanelTooltip": "Zobraziť panel LOS", "losShowPanelTooltip": "Zobraziť panel LOS",
"losHidePanelTooltip": "Skryť panel LOS", "losHidePanelTooltip": "Skryť panel LOS",
"losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)" "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)",
} "losLegendRadioHorizon": "Rádiový horizont",
"losLegendLosBeam": "Priama viditeľnosť",
"losLegendTerrain": "Terén",
"losFrequencyLabel": "Frekvencia",
"losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu",
"losFrequencyDialogTitle": "Výpočet rádiového horizontu",
"losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {baselineFreq} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
}
}
+28 -2
View File
@@ -1,4 +1,6 @@
{ {
"channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } },
"@@locale": "sl", "@@locale": "sl",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Stiki", "nav_contacts": "Stiki",
@@ -1718,5 +1720,29 @@
"losPointName": "Ime točke", "losPointName": "Ime točke",
"losShowPanelTooltip": "Pokaži ploščo LOS", "losShowPanelTooltip": "Pokaži ploščo LOS",
"losHidePanelTooltip": "Skrij ploščo LOS", "losHidePanelTooltip": "Skrij ploščo LOS",
"losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)" "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)",
} "losLegendRadioHorizon": "Radijski horizont",
"losLegendLosBeam": "Linija vidnosti",
"losLegendTerrain": "Teren",
"losFrequencyLabel": "Frekvenca",
"losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna",
"losFrequencyDialogTitle": "Izračun radijskega horizonta",
"losFrequencyDialogDescription": "Začenši od k={baselineK} pri {baselineFreq} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
}
}
+28 -2
View File
@@ -1,4 +1,6 @@
{ {
"channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } },
"@@locale": "sv", "@@locale": "sv",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Kontakter", "nav_contacts": "Kontakter",
@@ -1718,5 +1720,29 @@
"losPointName": "Punktnamn", "losPointName": "Punktnamn",
"losShowPanelTooltip": "Visa LOS-panelen", "losShowPanelTooltip": "Visa LOS-panelen",
"losHidePanelTooltip": "Dölj LOS-panelen", "losHidePanelTooltip": "Dölj LOS-panelen",
"losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)" "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)",
} "losLegendRadioHorizon": "Radiohorisont",
"losLegendLosBeam": "Siktlinje",
"losLegendTerrain": "Terräng",
"losFrequencyLabel": "Frekvens",
"losFrequencyInfoTooltip": "Visa detaljer om beräkningen",
"losFrequencyDialogTitle": "Beräkning av radiohorisonten",
"losFrequencyDialogDescription": "Med start från k={baselineK} vid {baselineFreq} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
}
}
+28 -2
View File
@@ -1,4 +1,6 @@
{ {
"channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } },
"@@locale": "uk", "@@locale": "uk",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "Контакти", "nav_contacts": "Контакти",
@@ -1718,5 +1720,29 @@
"losPointName": "Назва точки", "losPointName": "Назва точки",
"losShowPanelTooltip": "Показати панель LOS", "losShowPanelTooltip": "Показати панель LOS",
"losHidePanelTooltip": "Приховати панель LOS", "losHidePanelTooltip": "Приховати панель LOS",
"losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)" "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)",
} "losLegendRadioHorizon": "Радіогоризонт",
"losLegendLosBeam": "Лінія прямої видимості",
"losLegendTerrain": "Рельєф",
"losFrequencyLabel": "Частота",
"losFrequencyInfoTooltip": "Переглянути деталі розрахунку",
"losFrequencyDialogTitle": "Розрахунок радіогоризонту",
"losFrequencyDialogDescription": "Починаючи з k={baselineK} на {baselineFreq} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
}
}
+28 -2
View File
@@ -1,4 +1,6 @@
{ {
"channels_channelDeleteFailed": "无法删除频道 \"{name}\"",
"@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } },
"@@locale": "zh", "@@locale": "zh",
"appTitle": "MeshCore Open", "appTitle": "MeshCore Open",
"nav_contacts": "联系方式", "nav_contacts": "联系方式",
@@ -1718,5 +1720,29 @@
"losPointName": "点名称", "losPointName": "点名称",
"losShowPanelTooltip": "显示 LOS 面板", "losShowPanelTooltip": "显示 LOS 面板",
"losHidePanelTooltip": "隐藏 LOS 面板", "losHidePanelTooltip": "隐藏 LOS 面板",
"losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)" "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)",
} "losLegendRadioHorizon": "无线电地平线",
"losLegendLosBeam": "视距波束",
"losLegendTerrain": "地形",
"losFrequencyLabel": "频率",
"losFrequencyInfoTooltip": "查看计算详情",
"losFrequencyDialogTitle": "无线电地平线计算",
"losFrequencyDialogDescription": "从 {baselineFreq} MHz 处的 k={baselineK} 开始,计算调整当前 {frequencyMHz} MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。",
"@losFrequencyDialogDescription": {
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
"placeholders": {
"baselineK": {
"type": "double"
},
"baselineFreq": {
"type": "double"
},
"frequencyMHz": {
"type": "double"
},
"kFactor": {
"type": "double"
}
}
}
}
+8
View File
@@ -15,6 +15,7 @@ import 'services/ble_debug_log_service.dart';
import 'services/app_debug_log_service.dart'; import 'services/app_debug_log_service.dart';
import 'services/background_service.dart'; import 'services/background_service.dart';
import 'services/map_tile_cache_service.dart'; import 'services/map_tile_cache_service.dart';
import 'services/chat_text_scale_service.dart';
import 'storage/prefs_manager.dart'; import 'storage/prefs_manager.dart';
import 'utils/app_logger.dart'; import 'utils/app_logger.dart';
@@ -34,6 +35,7 @@ void main() async {
final appDebugLogService = AppDebugLogService(); final appDebugLogService = AppDebugLogService();
final backgroundService = BackgroundService(); final backgroundService = BackgroundService();
final mapTileCacheService = MapTileCacheService(); final mapTileCacheService = MapTileCacheService();
final chatTextScaleService = ChatTextScaleService();
// Load settings // Load settings
await appSettingsService.loadSettings(); await appSettingsService.loadSettings();
@@ -50,6 +52,8 @@ void main() async {
await backgroundService.initialize(); await backgroundService.initialize();
_registerThirdPartyLicenses(); _registerThirdPartyLicenses();
await chatTextScaleService.initialize();
// Wire up connector with services // Wire up connector with services
connector.initialize( connector.initialize(
retryService: retryService, retryService: retryService,
@@ -78,6 +82,7 @@ void main() async {
bleDebugLogService: bleDebugLogService, bleDebugLogService: bleDebugLogService,
appDebugLogService: appDebugLogService, appDebugLogService: appDebugLogService,
mapTileCacheService: mapTileCacheService, mapTileCacheService: mapTileCacheService,
chatTextScaleService: chatTextScaleService,
), ),
); );
} }
@@ -112,6 +117,7 @@ class MeshCoreApp extends StatelessWidget {
final BleDebugLogService bleDebugLogService; final BleDebugLogService bleDebugLogService;
final AppDebugLogService appDebugLogService; final AppDebugLogService appDebugLogService;
final MapTileCacheService mapTileCacheService; final MapTileCacheService mapTileCacheService;
final ChatTextScaleService chatTextScaleService;
const MeshCoreApp({ const MeshCoreApp({
super.key, super.key,
@@ -123,6 +129,7 @@ class MeshCoreApp extends StatelessWidget {
required this.bleDebugLogService, required this.bleDebugLogService,
required this.appDebugLogService, required this.appDebugLogService,
required this.mapTileCacheService, required this.mapTileCacheService,
required this.chatTextScaleService,
}); });
@override @override
@@ -135,6 +142,7 @@ class MeshCoreApp extends StatelessWidget {
ChangeNotifierProvider.value(value: appSettingsService), ChangeNotifierProvider.value(value: appSettingsService),
ChangeNotifierProvider.value(value: bleDebugLogService), ChangeNotifierProvider.value(value: bleDebugLogService),
ChangeNotifierProvider.value(value: appDebugLogService), ChangeNotifierProvider.value(value: appDebugLogService),
ChangeNotifierProvider.value(value: chatTextScaleService),
Provider.value(value: storage), Provider.value(value: storage),
Provider.value(value: mapTileCacheService), Provider.value(value: mapTileCacheService),
], ],
+75 -43
View File
@@ -18,7 +18,9 @@ import '../l10n/l10n.dart';
import '../models/channel.dart'; import '../models/channel.dart';
import '../models/channel_message.dart'; import '../models/channel_message.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import '../services/chat_text_scale_service.dart';
import '../utils/emoji_utils.dart'; import '../utils/emoji_utils.dart';
import '../widgets/chat_zoom_wrapper.dart';
import '../widgets/emoji_picker.dart'; import '../widgets/emoji_picker.dart';
import '../widgets/gif_message.dart'; import '../widgets/gif_message.dart';
import '../widgets/jump_to_bottom_button.dart'; import '../widgets/jump_to_bottom_button.dart';
@@ -219,37 +221,50 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return Stack( return Stack(
children: [ children: [
ListView.builder( ChatZoomWrapper(
reverse: true, // List grows from bottom up child: ListView.builder(
controller: _scrollController, reverse: true, // List grows from bottom up
padding: const EdgeInsets.all(8), controller: _scrollController,
itemCount: itemCount, padding: const EdgeInsets.all(8),
itemBuilder: (context, index) { itemCount: itemCount,
// Loading indicator now appears at end (bottom) of reversed list itemBuilder: (context, index) {
if (_isLoadingOlder && index == itemCount - 1) { // Loading indicator now appears at end (bottom) of reversed list
return const Padding( if (_isLoadingOlder && index == itemCount - 1) {
padding: EdgeInsets.symmetric(vertical: 16), return const Padding(
child: Center( padding: EdgeInsets.symmetric(vertical: 16),
child: SizedBox( child: Center(
width: 20, child: SizedBox(
height: 20, width: 20,
child: CircularProgressIndicator( height: 20,
strokeWidth: 2, child: CircularProgressIndicator(
strokeWidth: 2,
),
), ),
), ),
);
}
final messageIndex = index;
final message = reversedMessages[messageIndex];
if (!_messageKeys.containsKey(message.messageId)) {
_messageKeys[message.messageId] = GlobalKey();
}
return Container(
key: _messageKeys[message.messageId]!,
child: Builder(
builder: (context) {
final textScale = context
.select<ChatTextScaleService, double>(
(service) => service.scale,
);
return _buildMessageBubble(
message,
textScale,
);
},
), ),
); );
} },
final messageIndex = index; ),
final message = reversedMessages[messageIndex];
if (!_messageKeys.containsKey(message.messageId)) {
_messageKeys[message.messageId] = GlobalKey();
}
return Container(
key: _messageKeys[message.messageId]!,
child: _buildMessageBubble(message),
);
},
), ),
JumpToBottomButton(scrollController: _scrollController), JumpToBottomButton(scrollController: _scrollController),
], ],
@@ -264,7 +279,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
); );
} }
Widget _buildMessageBubble(ChannelMessage message) { Widget _buildMessageBubble(ChannelMessage message, double textScale) {
final settingsService = context.watch<AppSettingsService>(); final settingsService = context.watch<AppSettingsService>();
final enableTracing = settingsService.settings.enableMessageTracing; final enableTracing = settingsService.settings.enableMessageTracing;
final isOutgoing = message.isOutgoing; final isOutgoing = message.isOutgoing;
@@ -278,6 +293,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
const maxSwipeOffset = 64.0; const maxSwipeOffset = 64.0;
const replySwipeThreshold = 64.0; const replySwipeThreshold = 64.0;
const bodyFontSize = 14.0;
final messageBody = Column( final messageBody = Column(
crossAxisAlignment: isOutgoing crossAxisAlignment: isOutgoing
? CrossAxisAlignment.end ? CrossAxisAlignment.end
@@ -334,7 +350,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
if (gifId == null) const SizedBox(height: 4), if (gifId == null) const SizedBox(height: 4),
], ],
if (message.replyToMessageId != null) ...[ if (message.replyToMessageId != null) ...[
_buildReplyPreview(message), _buildReplyPreview(message, textScale),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
if (poi != null) if (poi != null)
@@ -342,6 +358,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
context, context,
poi, poi,
isOutgoing, isOutgoing,
textScale,
trailing: (!enableTracing && isOutgoing) trailing: (!enableTracing && isOutgoing)
? Padding( ? Padding(
padding: const EdgeInsets.only(bottom: 2), padding: const EdgeInsets.only(bottom: 2),
@@ -415,9 +432,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Flexible( Flexible(
child: Linkify( child: Linkify(
text: message.text, text: message.text,
style: const TextStyle(fontSize: 14), style: TextStyle(
linkStyle: const TextStyle( fontSize: bodyFontSize * textScale,
fontSize: 14, ),
linkStyle: TextStyle(
fontSize: bodyFontSize * textScale,
color: Colors.green, color: Colors.green,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
), ),
@@ -595,7 +614,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
); );
} }
Widget _buildReplyPreview(ChannelMessage message) { Widget _buildReplyPreview(ChannelMessage message, double textScale) {
final connector = context.read<MeshCoreConnector>(); final connector = context.read<MeshCoreConnector>();
final isOwnNode = message.replyToSenderName == connector.selfName; final isOwnNode = message.replyToSenderName == connector.selfName;
final replyText = message.replyToText ?? ''; final replyText = message.replyToText ?? '';
@@ -623,7 +642,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
context.l10n.chat_location, context.l10n.chat_location,
style: TextStyle(fontSize: 12, color: previewTextColor), style: TextStyle(fontSize: 12 * textScale, color: previewTextColor),
), ),
], ],
); );
@@ -633,7 +652,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12 * textScale,
color: previewTextColor, color: previewTextColor,
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
), ),
@@ -657,7 +676,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Text( Text(
context.l10n.chat_replyTo(message.replyToSenderName ?? ''), context.l10n.chat_replyTo(message.replyToSenderName ?? ''),
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11 * textScale,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: isOwnNode color: isOwnNode
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
@@ -736,7 +755,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Widget _buildPoiMessage( Widget _buildPoiMessage(
BuildContext context, BuildContext context,
_PoiInfo poi, _PoiInfo poi,
bool isOutgoing, { bool isOutgoing,
double textScale, {
Widget? trailing, Widget? trailing,
}) { }) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
@@ -774,12 +794,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
children: [ children: [
Text( Text(
context.l10n.chat_poiShared, context.l10n.chat_poiShared,
style: TextStyle(color: textColor, fontWeight: FontWeight.w600), style: TextStyle(
color: textColor,
fontWeight: FontWeight.w600,
fontSize: 14 * textScale,
),
), ),
if (poi.label.isNotEmpty) if (poi.label.isNotEmpty)
Text( Text(
poi.label, poi.label,
style: TextStyle(color: metaColor, fontSize: 12), style: TextStyle(color: metaColor, fontSize: 12 * textScale),
), ),
], ],
), ),
@@ -849,7 +873,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return colors[hash.abs() % colors.length]; return colors[hash.abs() % colors.length];
} }
Widget _buildReplyBanner() { Widget _buildReplyBanner(double textScale) {
final message = _replyingToMessage!; final message = _replyingToMessage!;
return Container( return Container(
width: double.infinity, width: double.infinity,
@@ -875,7 +899,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Text( Text(
context.l10n.chat_replyingTo(message.senderName), context.l10n.chat_replyingTo(message.senderName),
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12 * textScale,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSecondaryContainer, color: Theme.of(context).colorScheme.onSecondaryContainer,
), ),
@@ -885,7 +909,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11 * textScale,
color: Theme.of( color: Theme.of(
context, context,
).colorScheme.onSecondaryContainer.withValues(alpha: 0.7), ).colorScheme.onSecondaryContainer.withValues(alpha: 0.7),
@@ -912,7 +936,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (_replyingToMessage != null) _buildReplyBanner(), if (_replyingToMessage != null)
Builder(
builder: (context) {
final textScale = context.select<ChatTextScaleService, double>(
(service) => service.scale,
);
return _buildReplyBanner(textScale);
},
),
Container( Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
+57 -25
View File
@@ -489,12 +489,13 @@ class _ChannelsScreenState extends State<ChannelsScreen>
ChannelMessageStore channelMessageStore, ChannelMessageStore channelMessageStore,
Channel channel, Channel channel,
) { ) {
final parentContext = context;
final settingsService = context.read<AppSettingsService>(); final settingsService = context.read<AppSettingsService>();
final isMuted = settingsService.isChannelMuted(channel.name); final isMuted = settingsService.isChannelMuted(channel.name);
showModalBottomSheet( showModalBottomSheet(
context: context, context: parentContext,
builder: (context) => SafeArea( builder: (sheetContext) => SafeArea(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -502,10 +503,10 @@ class _ChannelsScreenState extends State<ChannelsScreen>
leading: const Icon(Icons.edit_outlined), leading: const Icon(Icons.edit_outlined),
title: Text(context.l10n.channels_editChannel), title: Text(context.l10n.channels_editChannel),
onTap: () async { onTap: () async {
Navigator.pop(context); Navigator.pop(sheetContext);
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
if (context.mounted) { if (parentContext.mounted) {
_showEditChannelDialog(context, connector, channel); _showEditChannelDialog(parentContext, connector, channel);
} }
}, },
), ),
@@ -521,7 +522,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
: context.l10n.channels_muteChannel, : context.l10n.channels_muteChannel,
), ),
onTap: () async { onTap: () async {
Navigator.pop(context); Navigator.pop(sheetContext);
if (isMuted) { if (isMuted) {
await settingsService.unmuteChannel(channel.name); await settingsService.unmuteChannel(channel.name);
} else { } else {
@@ -536,9 +537,9 @@ class _ChannelsScreenState extends State<ChannelsScreen>
style: const TextStyle(color: Colors.red), style: const TextStyle(color: Colors.red),
), ),
onTap: () async { onTap: () async {
Navigator.pop(context); Navigator.pop(sheetContext);
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
if (context.mounted) { if (parentContext.mounted) {
_confirmDeleteChannel( _confirmDeleteChannel(
context, context,
connector, connector,
@@ -1454,7 +1455,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
child: Text(dialogContext.l10n.common_cancel), child: Text(dialogContext.l10n.common_cancel),
), ),
FilledButton( FilledButton(
onPressed: () { onPressed: () async {
final name = nameController.text.trim(); final name = nameController.text.trim();
final pskHex = pskController.text.trim(); final pskHex = pskController.text.trim();
@@ -1471,13 +1472,25 @@ class _ChannelsScreenState extends State<ChannelsScreen>
} }
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
connector.setChannel(channel.index, name, psk); try {
connector.setChannelSmazEnabled(channel.index, smazEnabled); await connector.setChannel(channel.index, name, psk);
ScaffoldMessenger.of(context).showSnackBar( await connector.setChannelSmazEnabled(
SnackBar( channel.index,
content: Text(context.l10n.channels_channelUpdated(name)), smazEnabled,
), );
); if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.channels_channelUpdated(name)),
),
);
} catch (e, st) {
debugPrint(st.toString());
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update channel: $e')),
);
}
}, },
child: Text(dialogContext.l10n.common_save), child: Text(dialogContext.l10n.common_save),
), ),
@@ -1506,17 +1519,36 @@ class _ChannelsScreenState extends State<ChannelsScreen>
child: Text(dialogContext.l10n.common_cancel), child: Text(dialogContext.l10n.common_cancel),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () async {
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
connector.deleteChannel(channel.index); try {
channelMessageStore.clearChannelMessages(channel.index); await connector.deleteChannel(channel.index);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar( channelMessageStore.clearChannelMessages(channel.index);
content: Text(
context.l10n.channels_channelDeleted(channel.name), if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.channels_channelDeleted(channel.name),
),
), ),
), );
); } catch (e, st) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.channels_channelDeleteFailed(channel.name),
),
),
);
// Preserve existing logging (if it was there)
debugPrint('Failed to delete channel: $e\n$st');
}
}, },
child: Text( child: Text(
dialogContext.l10n.common_delete, dialogContext.l10n.common_delete,
+74 -49
View File
@@ -22,7 +22,9 @@ import '../models/contact.dart';
import '../models/message.dart'; import '../models/message.dart';
import '../models/path_history.dart'; import '../models/path_history.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import '../services/chat_text_scale_service.dart';
import '../services/path_history_service.dart'; import '../services/path_history_service.dart';
import '../widgets/chat_zoom_wrapper.dart';
import '../widgets/elements_ui.dart'; import '../widgets/elements_ui.dart';
import 'channel_message_path_screen.dart'; import 'channel_message_path_screen.dart';
import 'map_screen.dart'; import 'map_screen.dart';
@@ -270,52 +272,62 @@ class _ChatScreenState extends State<ChatScreen> {
_scrollController.scrollToBottomIfAtBottom(); _scrollController.scrollToBottomIfAtBottom();
}); });
return ListView.builder( return ChatZoomWrapper(
reverse: true, // List grows from bottom up child: ListView.builder(
controller: _scrollController, reverse: true, // List grows from bottom up
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), controller: _scrollController,
itemCount: itemCount, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
itemBuilder: (context, index) { itemCount: itemCount,
// Loading indicator now appears at end (bottom) of reversed list itemBuilder: (context, index) {
if (_isLoadingOlder && index == itemCount - 1) { // Loading indicator now appears at end (bottom) of reversed list
return const Padding( if (_isLoadingOlder && index == itemCount - 1) {
padding: EdgeInsets.symmetric(vertical: 16), return const Padding(
child: Center( padding: EdgeInsets.symmetric(vertical: 16),
child: SizedBox( child: Center(
width: 20, child: SizedBox(
height: 20, width: 20,
child: CircularProgressIndicator(strokeWidth: 2), height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
), ),
), );
); }
} final messageIndex = index;
final messageIndex = index; Contact contact = widget.contact;
Contact contact = widget.contact; final message = reversedMessages[messageIndex];
final message = reversedMessages[messageIndex]; String fourByteHex = '';
String fourByteHex = ''; if (widget.contact.type == advTypeRoom) {
if (widget.contact.type == advTypeRoom) { contact = _resolveContactFrom4Bytes(
contact = _resolveContactFrom4Bytes( connector,
connector, message.fourByteRoomContactKey.isEmpty
message.fourByteRoomContactKey.isEmpty ? Uint8List.fromList([0, 0, 0, 0])
? Uint8List.fromList([0, 0, 0, 0]) : message.fourByteRoomContactKey,
: message.fourByteRoomContactKey, );
); fourByteHex = message.fourByteRoomContactKey
fourByteHex = message.fourByteRoomContactKey .map((b) => b.toRadixString(16).padLeft(2, '0'))
.map((b) => b.toRadixString(16).padLeft(2, '0')) .join()
.join() .toUpperCase();
.toUpperCase(); }
}
return _MessageBubble( return Builder(
message: message, builder: (context) {
senderName: widget.contact.type == advTypeRoom final textScale = context.select<ChatTextScaleService, double>(
? "${contact.name} [$fourByteHex]" (service) => service.scale,
: contact.name, );
isRoomServer: widget.contact.type == advTypeRoom, return _MessageBubble(
onTap: () => _openMessagePath(message, contact), message: message,
onLongPress: () => _showMessageActions(message, contact), senderName: widget.contact.type == advTypeRoom
); ? "${contact.name} [$fourByteHex]"
}, : contact.name,
isRoomServer: widget.contact.type == advTypeRoom,
textScale: textScale,
onTap: () => _openMessagePath(message, contact),
onLongPress: () => _showMessageActions(message, contact),
);
},
);
},
),
); );
} }
@@ -1163,11 +1175,13 @@ class _MessageBubble extends StatelessWidget {
final bool isRoomServer; final bool isRoomServer;
final VoidCallback? onTap; final VoidCallback? onTap;
final VoidCallback? onLongPress; final VoidCallback? onLongPress;
final double textScale;
const _MessageBubble({ const _MessageBubble({
required this.message, required this.message,
required this.senderName, required this.senderName,
required this.isRoomServer, required this.isRoomServer,
required this.textScale,
this.onTap, this.onTap,
this.onLongPress, this.onLongPress,
}); });
@@ -1190,6 +1204,7 @@ class _MessageBubble extends StatelessWidget {
? colorScheme.onErrorContainer ? colorScheme.onErrorContainer
: (isOutgoing ? colorScheme.onPrimary : colorScheme.onSurface); : (isOutgoing ? colorScheme.onPrimary : colorScheme.onSurface);
final metaColor = textColor.withValues(alpha: 0.7); final metaColor = textColor.withValues(alpha: 0.7);
const bodyFontSize = 14.0;
String messageText = message.text; String messageText = message.text;
if (isRoomServer && !isOutgoing) { if (isRoomServer && !isOutgoing) {
messageText = message.text.substring(4.clamp(0, message.text.length)); messageText = message.text.substring(4.clamp(0, message.text.length));
@@ -1258,6 +1273,7 @@ class _MessageBubble extends StatelessWidget {
poi, poi,
textColor, textColor,
metaColor, metaColor,
textScale,
trailing: (!enableTracing && isOutgoing) trailing: (!enableTracing && isOutgoing)
? Padding( ? Padding(
padding: const EdgeInsets.only(bottom: 2), padding: const EdgeInsets.only(bottom: 2),
@@ -1321,10 +1337,14 @@ class _MessageBubble extends StatelessWidget {
Flexible( Flexible(
child: Linkify( child: Linkify(
text: messageText, text: messageText,
style: TextStyle(color: textColor), style: TextStyle(
linkStyle: const TextStyle( color: textColor,
fontSize: bodyFontSize * textScale,
),
linkStyle: TextStyle(
color: Colors.green, color: Colors.green,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
fontSize: bodyFontSize * textScale,
), ),
options: const LinkifyOptions( options: const LinkifyOptions(
humanize: false, humanize: false,
@@ -1464,7 +1484,8 @@ class _MessageBubble extends StatelessWidget {
BuildContext context, BuildContext context,
_PoiInfo poi, _PoiInfo poi,
Color textColor, Color textColor,
Color metaColor, { Color metaColor,
double textScale, {
Widget? trailing, Widget? trailing,
}) { }) {
return Row( return Row(
@@ -1493,12 +1514,16 @@ class _MessageBubble extends StatelessWidget {
children: [ children: [
Text( Text(
context.l10n.chat_poiShared, context.l10n.chat_poiShared,
style: TextStyle(color: textColor, fontWeight: FontWeight.w600), style: TextStyle(
color: textColor,
fontWeight: FontWeight.w600,
fontSize: 14 * textScale,
),
), ),
if (poi.label.isNotEmpty) if (poi.label.isNotEmpty)
Text( Text(
poi.label, poi.label,
style: TextStyle(color: metaColor, fontSize: 12), style: TextStyle(color: metaColor, fontSize: 12 * textScale),
), ),
], ],
), ),
+328 -24
View File
@@ -14,8 +14,10 @@ import '../services/app_settings_service.dart';
import '../services/line_of_sight_service.dart'; import '../services/line_of_sight_service.dart';
import '../services/map_tile_cache_service.dart'; import '../services/map_tile_cache_service.dart';
import '../utils/route_transitions.dart'; import '../utils/route_transitions.dart';
import '../connector/meshcore_connector.dart';
import '../widgets/app_bar.dart'; import '../widgets/app_bar.dart';
import '../widgets/quick_switch_bar.dart'; import '../widgets/quick_switch_bar.dart';
import '../icons/los_icon.dart';
class LineOfSightEndpoint { class LineOfSightEndpoint {
final String label; final String label;
@@ -71,6 +73,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
bool _showMarkerLabels = true; bool _showMarkerLabels = true;
bool _didReceivePositionUpdate = false; bool _didReceivePositionUpdate = false;
int _losRequestNonce = 0; int _losRequestNonce = 0;
bool _initialLosScheduled = false;
@override @override
void initState() { void initState() {
@@ -81,7 +84,16 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
_end = widget.candidates[1]; _end = widget.candidates[1];
} }
} }
_runLos(); _scheduleInitialRun();
}
void _scheduleInitialRun() {
if (_initialLosScheduled) return;
_initialLosScheduled = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_runLos();
});
} }
@override @override
@@ -110,10 +122,13 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
}); });
try { try {
final connector = context.read<MeshCoreConnector>();
final frequencyMHz = _normalizeFrequencyMHz(connector.currentFreqHz);
final result = await _lineOfSightService.analyzePath( final result = await _lineOfSightService.analyzePath(
[start.point, end.point], [start.point, end.point],
startAntennaHeightMeters: startAntenna, startAntennaHeightMeters: startAntenna,
endAntennaHeightMeters: endAntenna, endAntennaHeightMeters: endAntenna,
frequencyMHz: frequencyMHz,
); );
if (!mounted) return; if (!mounted) return;
if (!_isRunRequestCurrent( if (!_isRunRequestCurrent(
@@ -424,6 +439,12 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
Widget _buildControlPanel(bool isImperial) { Widget _buildControlPanel(bool isImperial) {
_sanitizeSelection(); _sanitizeSelection();
final segment = _primarySegmentResult(); final segment = _primarySegmentResult();
final connector = context.read<MeshCoreConnector>();
final reportedFrequencyMHz = _normalizeFrequencyMHz(
connector.currentFreqHz,
);
final displayFrequencyMHz = segment?.frequencyMHz ?? reportedFrequencyMHz;
final kFactorUsed = segment?.usedKFactor;
final endpoints = _visibleEndpoints(); final endpoints = _visibleEndpoints();
final distanceUnit = isImperial ? 'mi' : 'km'; final distanceUnit = isImperial ? 'mi' : 'km';
final heightUnit = isImperial ? 'ft' : 'm'; final heightUnit = isImperial ? 'ft' : 'm';
@@ -461,6 +482,9 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
terrainLabel: context.l10n.losLegendTerrain,
losBeamLabel: context.l10n.losLegendLosBeam,
radioHorizonLabel: context.l10n.losLegendRadioHorizon,
), ),
), ),
) )
@@ -474,6 +498,14 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
), ),
), ),
), ),
if (segment != null) ...[
const SizedBox(height: 8),
_LosLegend(
terrainLabel: context.l10n.losLegendTerrain,
losBeamLabel: context.l10n.losLegendLosBeam,
radioHorizonLabel: context.l10n.losLegendRadioHorizon,
),
],
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
segment != null segment != null
@@ -488,6 +520,52 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
if (displayFrequencyMHz != null)
Padding(
padding: const EdgeInsets.only(top: 2, bottom: 4),
child: Row(
children: [
Text(
context.l10n.losFrequencyLabel,
style: TextStyle(
fontSize: 11,
color: Colors.grey[700],
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 8),
Text(
'${displayFrequencyMHz.toStringAsFixed(3)} MHz',
style: TextStyle(fontSize: 11, color: Colors.grey[700]),
),
if (kFactorUsed != null) ...[
const SizedBox(width: 8),
Text(
'k=${kFactorUsed.toStringAsFixed(3)}',
style: TextStyle(
fontSize: 11,
color: Colors.grey[700],
),
),
const SizedBox(width: 4),
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
icon: const Icon(Icons.info_outline, size: 16),
color: Colors.grey[600],
tooltip: context.l10n.losFrequencyInfoTooltip,
onPressed: () {
_showFrequencyInfoDialog(
context,
displayFrequencyMHz,
kFactorUsed,
);
},
),
],
],
),
),
Text( Text(
context.l10n.losElevationAttribution, context.l10n.losElevationAttribution,
style: TextStyle(fontSize: 10, color: Colors.grey[700]), style: TextStyle(fontSize: 10, color: Colors.grey[700]),
@@ -642,7 +720,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: _loading ? null : _runLos, onPressed: _loading ? null : _runLos,
icon: const Icon(Icons.visibility), icon: const LosIcon(),
label: Text(context.l10n.losRun), label: Text(context.l10n.losRun),
), ),
), ),
@@ -896,6 +974,40 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
break; break;
} }
} }
void _showFrequencyInfoDialog(
BuildContext context,
double frequencyMHz,
double kFactor,
) {
final baselineFreq = LineOfSightService.baselineFrequencyMHz;
final baselineK = LineOfSightService.baselineKFactor;
showDialog<void>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.losFrequencyDialogTitle),
content: Text(
context.l10n.losFrequencyDialogDescription(
baselineK,
baselineFreq,
frequencyMHz,
kFactor,
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_ok),
),
],
),
);
}
double? _normalizeFrequencyMHz(int? frequencyKHz) {
if (frequencyKHz == null || frequencyKHz <= 0) return null;
return frequencyKHz / 1000.0;
}
} }
class _LosProfilePainter extends CustomPainter { class _LosProfilePainter extends CustomPainter {
@@ -903,12 +1015,18 @@ class _LosProfilePainter extends CustomPainter {
final String distanceUnit; final String distanceUnit;
final String heightUnit; final String heightUnit;
final TextStyle badgeTextStyle; final TextStyle badgeTextStyle;
final String terrainLabel;
final String losBeamLabel;
final String radioHorizonLabel;
const _LosProfilePainter({ const _LosProfilePainter({
required this.samples, required this.samples,
required this.distanceUnit, required this.distanceUnit,
required this.heightUnit, required this.heightUnit,
required this.badgeTextStyle, required this.badgeTextStyle,
required this.terrainLabel,
required this.losBeamLabel,
required this.radioHorizonLabel,
}); });
@override @override
@@ -920,44 +1038,108 @@ class _LosProfilePainter extends CustomPainter {
if (samples.length < 2) return; if (samples.length < 2) return;
final minY = samples final minY = samples
.map((s) => math.min(s.terrainMeters, s.lineHeightMeters)) .map(
(s) => math.min(
math.min(s.terrainMeters, s.lineHeightMeters),
s.refractedHeightMeters,
),
)
.reduce(math.min); .reduce(math.min);
final maxY = samples final maxY = samples
.map((s) => math.max(s.terrainMeters, s.lineHeightMeters)) .map(
(s) => math.max(
math.max(s.terrainMeters, s.lineHeightMeters),
s.refractedHeightMeters,
),
)
.reduce(math.max); .reduce(math.max);
final ySpan = math.max(1.0, maxY - minY); final ySpan = math.max(1.0, maxY - minY);
final maxDist = math.max(1.0, samples.last.distanceMeters); final maxDist = math.max(1.0, samples.last.distanceMeters);
const horizontalPadding = 12.0;
const verticalPadding = 12.0;
final chartWidth = math.max(1.0, size.width - horizontalPadding * 2);
final chartHeight = math.max(1.0, size.height - verticalPadding * 2);
Offset mapPoint(double x, double y) { Offset mapPoint(double x, double y) {
final px = (x / maxDist) * size.width; final px = horizontalPadding + (x / maxDist) * chartWidth;
final py = size.height - ((y - minY) / ySpan) * size.height; final py =
size.height - verticalPadding - ((y - minY) / ySpan) * chartHeight;
return Offset(px, py); return Offset(px, py);
} }
final terrainPath = ui.Path(); final firstTerrainPoint = mapPoint(
terrainPath.moveTo(0, size.height); samples.first.distanceMeters,
for (final s in samples) { samples.first.terrainMeters,
final p = mapPoint(s.distanceMeters, s.terrainMeters); );
final lastTerrainPoint = mapPoint(
samples.last.distanceMeters,
samples.last.terrainMeters,
);
double distanceForCanvasX(double x) {
final normalized = ((x - horizontalPadding) / chartWidth).clamp(0.0, 1.0);
return normalized * maxDist;
}
double elevationToPixel(double elevation) {
final normalized = ((elevation - minY) / ySpan).clamp(0.0, 1.0);
return size.height - verticalPadding - normalized * chartHeight;
}
double extrapolateTerrain(double distance, bool isLeft) {
final samplesForSlope = isLeft
? samples.sublist(0, math.min(2, samples.length))
: samples.sublist(samples.length - math.min(2, samples.length));
if (samplesForSlope.length < 2) {
return samplesForSlope.first.terrainMeters;
}
final a = samplesForSlope.first;
final b = samplesForSlope.last;
final dx = b.distanceMeters - a.distanceMeters;
if (dx.abs() < 1e-6) return a.terrainMeters;
final slope = (b.terrainMeters - a.terrainMeters) / dx;
return a.terrainMeters + slope * (distance - a.distanceMeters);
}
final leftDistance = distanceForCanvasX(0.0);
final rightDistance = distanceForCanvasX(size.width);
final leftEdgeTerrain = extrapolateTerrain(leftDistance, true);
final rightEdgeTerrain = extrapolateTerrain(rightDistance, false);
final leftEdgePoint = Offset(0.0, elevationToPixel(leftEdgeTerrain));
final rightEdgePoint = Offset(
size.width,
elevationToPixel(rightEdgeTerrain),
);
final terrainPath = ui.Path()
..moveTo(0, size.height)
..lineTo(leftEdgePoint.dx, leftEdgePoint.dy)
..lineTo(firstTerrainPoint.dx, firstTerrainPoint.dy);
for (final sample in samples) {
final p = mapPoint(sample.distanceMeters, sample.terrainMeters);
terrainPath.lineTo(p.dx, p.dy); terrainPath.lineTo(p.dx, p.dy);
} }
terrainPath.lineTo(size.width, size.height); terrainPath
terrainPath.close(); ..lineTo(lastTerrainPoint.dx, lastTerrainPoint.dy)
..lineTo(rightEdgePoint.dx, rightEdgePoint.dy)
..lineTo(size.width, size.height)
..close();
canvas.drawPath(terrainPath, Paint()..color = const Color(0xCC7C6F5D)); const terrainFillColor = Color(0xCC7C6F5D);
const terrainLineColor = Color(0xFF9FE870);
const losLineColor = Color(0xFFE0E7FF);
canvas.drawPath(terrainPath, Paint()..color = terrainFillColor);
final terrainLine = ui.Path(); final terrainLine = ui.Path()..moveTo(leftEdgePoint.dx, leftEdgePoint.dy);
for (int i = 0; i < samples.length; i++) { for (final sample in samples) {
final p = mapPoint(samples[i].distanceMeters, samples[i].terrainMeters); final p = mapPoint(sample.distanceMeters, sample.terrainMeters);
if (i == 0) { terrainLine.lineTo(p.dx, p.dy);
terrainLine.moveTo(p.dx, p.dy);
} else {
terrainLine.lineTo(p.dx, p.dy);
}
} }
terrainLine.lineTo(rightEdgePoint.dx, rightEdgePoint.dy);
canvas.drawPath( canvas.drawPath(
terrainLine, terrainLine,
Paint() Paint()
..color = const Color(0xFF9FE870) ..color = terrainLineColor
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 2, ..strokeWidth = 2,
); );
@@ -977,10 +1159,59 @@ class _LosProfilePainter extends CustomPainter {
canvas.drawPath( canvas.drawPath(
losLine, losLine,
Paint() Paint()
..color = const Color(0xFFE0E7FF) ..color = losLineColor
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 2, ..strokeWidth = 2,
); );
const refractedLineColor = Color(0xFFFFD57F);
final refractedLine = ui.Path();
for (int i = 0; i < samples.length; i++) {
final p = mapPoint(
samples[i].distanceMeters,
samples[i].refractedHeightMeters,
);
if (i == 0) {
refractedLine.moveTo(p.dx, p.dy);
} else {
refractedLine.lineTo(p.dx, p.dy);
}
}
canvas.drawPath(
refractedLine,
Paint()
..color = refractedLineColor
..style = PaintingStyle.stroke
..strokeWidth = 1.5,
);
final capPath = ui.Path();
for (int i = 0; i < samples.length; i++) {
final p = mapPoint(
samples[i].distanceMeters,
samples[i].refractedHeightMeters,
);
if (i == 0) {
capPath.moveTo(p.dx, p.dy);
} else {
capPath.lineTo(p.dx, p.dy);
}
}
for (int i = samples.length - 1; i >= 0; i--) {
final p = mapPoint(
samples[i].distanceMeters,
samples[i].lineHeightMeters,
);
capPath.lineTo(p.dx, p.dy);
}
capPath.close();
const horizonFillColor = Color(0x40FFD57F);
canvas.drawPath(
capPath,
Paint()
..color = horizonFillColor
..style = PaintingStyle.fill,
);
} }
@override @override
@@ -988,7 +1219,10 @@ class _LosProfilePainter extends CustomPainter {
return oldDelegate.samples != samples || return oldDelegate.samples != samples ||
oldDelegate.distanceUnit != distanceUnit || oldDelegate.distanceUnit != distanceUnit ||
oldDelegate.heightUnit != heightUnit || oldDelegate.heightUnit != heightUnit ||
oldDelegate.badgeTextStyle != badgeTextStyle; oldDelegate.badgeTextStyle != badgeTextStyle ||
oldDelegate.terrainLabel != terrainLabel ||
oldDelegate.losBeamLabel != losBeamLabel ||
oldDelegate.radioHorizonLabel != radioHorizonLabel;
} }
void _drawUnitBadge(Canvas canvas, Size size) { void _drawUnitBadge(Canvas canvas, Size size) {
@@ -1001,3 +1235,73 @@ class _LosProfilePainter extends CustomPainter {
painter.paint(canvas, Offset(size.width - painter.width - 8, 8)); painter.paint(canvas, Offset(size.width - painter.width - 8, 8));
} }
} }
class _LosLegend extends StatelessWidget {
static const _terrainColor = Color(0xFF9FE870);
static const _losColor = Color(0xFFE0E7FF);
static const _radioColor = Color(0xFFFFD57F);
final String terrainLabel;
final String losBeamLabel;
final String radioHorizonLabel;
const _LosLegend({
required this.terrainLabel,
required this.losBeamLabel,
required this.radioHorizonLabel,
});
@override
Widget build(BuildContext context) {
final textStyle =
Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.white70,
fontSize: 11,
fontWeight: FontWeight.w500,
) ??
const TextStyle(
color: Colors.white70,
fontSize: 11,
fontWeight: FontWeight.w500,
);
final entries = [
_LegendEntry(terrainLabel, _terrainColor),
_LegendEntry(losBeamLabel, _losColor),
_LegendEntry(radioHorizonLabel, _radioColor),
];
const swatchSize = 10.0;
return Wrap(
spacing: 16,
runSpacing: 6,
children: entries
.map(
(entry) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: swatchSize,
height: swatchSize,
decoration: BoxDecoration(
color: entry.color,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 6),
Text(entry.label, style: textStyle),
],
),
)
.toList(),
);
}
}
class _LegendEntry {
final String label;
final Color color;
const _LegendEntry(this.label, this.color);
}
+2 -1
View File
@@ -20,6 +20,7 @@ import '../services/map_tile_cache_service.dart';
import '../utils/contact_search.dart'; import '../utils/contact_search.dart';
import '../utils/route_transitions.dart'; import '../utils/route_transitions.dart';
import '../widgets/quick_switch_bar.dart'; import '../widgets/quick_switch_bar.dart';
import '../icons/los_icon.dart';
import 'channels_screen.dart'; import 'channels_screen.dart';
import 'chat_screen.dart'; import 'chat_screen.dart';
import 'contacts_screen.dart'; import 'contacts_screen.dart';
@@ -280,7 +281,7 @@ class _MapScreenState extends State<MapScreen> {
), ),
if (!_isBuildingPathTrace) if (!_isBuildingPathTrace)
IconButton( IconButton(
icon: const Icon(Icons.visibility), icon: const LosIcon(),
onPressed: () { onPressed: () {
final candidates = <LineOfSightEndpoint>[]; final candidates = <LineOfSightEndpoint>[];
if (connector.selfLatitude != null && if (connector.selfLatitude != null &&
+72
View File
@@ -0,0 +1,72 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../storage/prefs_manager.dart';
/// Client-side accessibility/UI service that exposes a persistent shared text scale
/// factor. No MeshCoreConnector/RoomServer or protocol interaction occurs, and the
/// value is saved locally via SharedPreferences so it can be reused in Markdown
/// viewers, log panels, or other text-heavy widgets without redundant network
/// dependencies.
///
/// Widgets should scope rebuilds using the snippet below so only the scaled text
/// is rebuilt instead of the entire chat list:
/// ```dart
/// context.select<ChatTextScaleService, double>(
/// (service) => service.scale,
/// )
/// ```
class ChatTextScaleService extends ChangeNotifier {
static const _prefKey = 'chat_text_scale';
static const double _minScale = 0.8;
static const double _maxScale = 1.8;
double _scale = 1.0;
Timer? _saveTimer;
double get scale => _scale;
Future<void> initialize() async {
final stored = PrefsManager.instance.getDouble(_prefKey);
if (stored != null) {
_scale = _clamp(stored);
}
}
void setScale(double value, {bool persistImmediately = false}) {
final next = _clamp(value);
if (next == _scale) return;
_scale = next;
notifyListeners();
if (persistImmediately) {
_commitScale();
} else {
_scheduleSave();
}
}
void reset() {
setScale(1.0, persistImmediately: true);
}
void persist() => _commitScale();
@override
void dispose() {
_saveTimer?.cancel();
super.dispose();
}
void _scheduleSave() {
_saveTimer?.cancel();
_saveTimer = Timer(const Duration(milliseconds: 250), _commitScale);
}
void _commitScale() {
_saveTimer?.cancel();
PrefsManager.instance.setDouble(_prefKey, _scale);
}
double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble();
}
+44 -4
View File
@@ -12,12 +12,14 @@ class LineOfSightSample {
final double distanceMeters; final double distanceMeters;
final double terrainMeters; final double terrainMeters;
final double lineHeightMeters; final double lineHeightMeters;
final double refractedHeightMeters;
final double clearanceMeters; final double clearanceMeters;
const LineOfSightSample({ const LineOfSightSample({
required this.distanceMeters, required this.distanceMeters,
required this.terrainMeters, required this.terrainMeters,
required this.lineHeightMeters, required this.lineHeightMeters,
required this.refractedHeightMeters,
required this.clearanceMeters, required this.clearanceMeters,
}); });
} }
@@ -30,6 +32,8 @@ class LineOfSightResult {
final double? firstObstructionDistanceMeters; final double? firstObstructionDistanceMeters;
final List<LineOfSightSample> samples; final List<LineOfSightSample> samples;
final String? errorMessage; final String? errorMessage;
final double usedKFactor;
final double? frequencyMHz;
const LineOfSightResult({ const LineOfSightResult({
required this.hasData, required this.hasData,
@@ -38,12 +42,16 @@ class LineOfSightResult {
required this.maxObstructionMeters, required this.maxObstructionMeters,
required this.firstObstructionDistanceMeters, required this.firstObstructionDistanceMeters,
required this.samples, required this.samples,
required this.usedKFactor,
this.frequencyMHz,
this.errorMessage, this.errorMessage,
}); });
const LineOfSightResult.error({ const LineOfSightResult.error({
required this.totalDistanceMeters, required this.totalDistanceMeters,
required this.errorMessage, required this.errorMessage,
this.usedKFactor = 4.0 / 3.0,
this.frequencyMHz,
}) : hasData = false, }) : hasData = false,
isClear = false, isClear = false,
maxObstructionMeters = 0, maxObstructionMeters = 0,
@@ -89,6 +97,11 @@ class LineOfSightService {
static const Duration _cacheTtl = Duration(hours: 24); static const Duration _cacheTtl = Duration(hours: 24);
static const int _maxFetchAttempts = 4; // initial try + 3 retries static const int _maxFetchAttempts = 4; // initial try + 3 retries
static const Duration _initialBackoff = Duration(milliseconds: 300); static const Duration _initialBackoff = Duration(milliseconds: 300);
static const double _baselineFrequencyMHz = 915.0;
static const double _baselineKFactor = 4.0 / 3.0;
static double get baselineFrequencyMHz => _baselineFrequencyMHz;
static double get baselineKFactor => _baselineKFactor;
final http.Client _httpClient; final http.Client _httpClient;
final bool _ownsHttpClient; final bool _ownsHttpClient;
@@ -106,7 +119,7 @@ class LineOfSightService {
List<LatLng> points, { List<LatLng> points, {
double startAntennaHeightMeters = 1.5, double startAntennaHeightMeters = 1.5,
double endAntennaHeightMeters = 1.5, double endAntennaHeightMeters = 1.5,
double kFactor = 4.0 / 3.0, double? frequencyMHz,
double obstructionToleranceMeters = 0.0, double obstructionToleranceMeters = 0.0,
}) async { }) async {
if (points.length < 2) { if (points.length < 2) {
@@ -123,6 +136,7 @@ class LineOfSightService {
var blockedSegments = 0; var blockedSegments = 0;
var unknownSegments = 0; var unknownSegments = 0;
final kFactor = _kFactorForFrequency(frequencyMHz);
for (int i = 0; i < points.length - 1; i++) { for (int i = 0; i < points.length - 1; i++) {
final result = await analyzeLink( final result = await analyzeLink(
points[i], points[i],
@@ -130,6 +144,7 @@ class LineOfSightService {
startAntennaHeightMeters: startAntennaHeightMeters, startAntennaHeightMeters: startAntennaHeightMeters,
endAntennaHeightMeters: endAntennaHeightMeters, endAntennaHeightMeters: endAntennaHeightMeters,
kFactor: kFactor, kFactor: kFactor,
frequencyMHz: frequencyMHz,
obstructionToleranceMeters: obstructionToleranceMeters, obstructionToleranceMeters: obstructionToleranceMeters,
); );
segments.add( segments.add(
@@ -163,7 +178,8 @@ class LineOfSightService {
LatLng end, { LatLng end, {
double startAntennaHeightMeters = 1.5, double startAntennaHeightMeters = 1.5,
double endAntennaHeightMeters = 1.5, double endAntennaHeightMeters = 1.5,
double kFactor = 4.0 / 3.0, required double kFactor,
double? frequencyMHz,
double obstructionToleranceMeters = 0.0, double obstructionToleranceMeters = 0.0,
}) async { }) async {
final totalDistanceMeters = _distance.as(LengthUnit.Meter, start, end); final totalDistanceMeters = _distance.as(LengthUnit.Meter, start, end);
@@ -175,6 +191,8 @@ class LineOfSightService {
maxObstructionMeters: 0, maxObstructionMeters: 0,
firstObstructionDistanceMeters: null, firstObstructionDistanceMeters: null,
samples: const [], samples: const [],
usedKFactor: kFactor,
frequencyMHz: frequencyMHz,
); );
} }
@@ -185,6 +203,8 @@ class LineOfSightService {
return LineOfSightResult.error( return LineOfSightResult.error(
totalDistanceMeters: totalDistanceMeters, totalDistanceMeters: totalDistanceMeters,
errorMessage: errorElevationUnavailable, errorMessage: errorElevationUnavailable,
usedKFactor: kFactor,
frequencyMHz: frequencyMHz,
); );
} }
@@ -194,6 +214,7 @@ class LineOfSightService {
startAntennaHeightMeters: startAntennaHeightMeters, startAntennaHeightMeters: startAntennaHeightMeters,
endAntennaHeightMeters: endAntennaHeightMeters, endAntennaHeightMeters: endAntennaHeightMeters,
kFactor: kFactor, kFactor: kFactor,
frequencyMHz: frequencyMHz,
obstructionToleranceMeters: obstructionToleranceMeters, obstructionToleranceMeters: obstructionToleranceMeters,
); );
} }
@@ -203,13 +224,16 @@ class LineOfSightService {
required List<double> elevations, required List<double> elevations,
double startAntennaHeightMeters = 1.5, double startAntennaHeightMeters = 1.5,
double endAntennaHeightMeters = 1.5, double endAntennaHeightMeters = 1.5,
double kFactor = 4.0 / 3.0, required double kFactor,
double? frequencyMHz,
double obstructionToleranceMeters = 0.0, double obstructionToleranceMeters = 0.0,
}) { }) {
if (points.length < 2 || elevations.length != points.length) { if (points.length < 2 || elevations.length != points.length) {
return const LineOfSightResult.error( return LineOfSightResult.error(
totalDistanceMeters: 0, totalDistanceMeters: 0,
errorMessage: errorInvalidInput, errorMessage: errorInvalidInput,
usedKFactor: kFactor,
frequencyMHz: frequencyMHz,
); );
} }
@@ -238,6 +262,10 @@ class LineOfSightService {
(2 * effectiveEarthRadius); (2 * effectiveEarthRadius);
final terrainHeight = elevations[i] + earthBulge; final terrainHeight = elevations[i] + earthBulge;
final clearance = lineHeight - terrainHeight; final clearance = lineHeight - terrainHeight;
final unrefBulge =
(distanceFromStart * (totalDistanceMeters - distanceFromStart)) /
(2 * _earthRadiusMeters);
final refractedHeight = lineHeight + (unrefBulge - earthBulge);
if (clearance < -obstructionToleranceMeters) { if (clearance < -obstructionToleranceMeters) {
isClear = false; isClear = false;
@@ -253,6 +281,7 @@ class LineOfSightService {
distanceMeters: distanceFromStart, distanceMeters: distanceFromStart,
terrainMeters: terrainHeight, terrainMeters: terrainHeight,
lineHeightMeters: lineHeight, lineHeightMeters: lineHeight,
refractedHeightMeters: refractedHeight,
clearanceMeters: clearance, clearanceMeters: clearance,
), ),
); );
@@ -265,9 +294,20 @@ class LineOfSightService {
maxObstructionMeters: maxObstructionMeters, maxObstructionMeters: maxObstructionMeters,
firstObstructionDistanceMeters: firstObstructionDistanceMeters, firstObstructionDistanceMeters: firstObstructionDistanceMeters,
samples: samples, samples: samples,
usedKFactor: kFactor,
frequencyMHz: frequencyMHz,
); );
} }
static double _kFactorForFrequency(double? frequencyMHz) {
if (frequencyMHz == null) return _baselineKFactor;
final delta =
(frequencyMHz - _baselineFrequencyMHz) / _baselineFrequencyMHz;
final adjustment = delta * 0.15;
final scaled = _baselineKFactor * (1 + adjustment);
return scaled.clamp(1.1, 1.6).toDouble();
}
List<LatLng> _buildSamplePoints( List<LatLng> _buildSamplePoints(
LatLng start, LatLng start,
LatLng end, LatLng end,
+49
View File
@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/chat_text_scale_service.dart';
/// Gesture wrapper that exposes two-finger pinch-to-zoom for chat scrollables.
/// Double-tap resets the scale. Only the wrapper itself listens to gestures;
/// child scrollables keep their normal touch handling.
class ChatZoomWrapper extends StatefulWidget {
const ChatZoomWrapper({super.key, required this.child, this.onDoubleTap});
final Widget child;
final VoidCallback? onDoubleTap;
@override
State<ChatZoomWrapper> createState() => _ChatZoomWrapperState();
}
class _ChatZoomWrapperState extends State<ChatZoomWrapper> {
double? _startScale;
@override
Widget build(BuildContext context) {
final service = context.read<ChatTextScaleService>();
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTap: () {
service.reset();
service.persist();
widget.onDoubleTap?.call();
},
onScaleStart: (details) {
if (details.pointerCount != 2) return;
_startScale = service.scale;
},
onScaleUpdate: (details) {
if (details.pointerCount != 2) return;
final baseScale = _startScale ?? service.scale;
service.setScale(baseScale * details.scale);
},
onScaleEnd: (_) {
_startScale = null;
service.persist();
},
child: widget.child,
);
}
}
+63 -15
View File
@@ -5,10 +5,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.7" version: "4.0.9"
args: args:
dependency: transitive dependency: transitive
description: description:
@@ -347,6 +347,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.2.2" version: "8.2.2"
flutter_svg:
dependency: "direct main"
description:
name: flutter_svg
sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -401,10 +409,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.7.2" version: "4.8.0"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -417,10 +425,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: json_annotation name: json_annotation
sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.10.0" version: "4.11.0"
latlong2: latlong2:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -509,6 +517,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.13.0" version: "0.13.0"
material_symbols_icons:
dependency: "direct main"
description:
name: material_symbols_icons
sha256: c62b15f2b3de98d72cbff0148812f5ef5159f05e61fc9f9a089ec2bb234df082
url: "https://pub.dev"
source: hosted
version: "4.2906.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@@ -537,10 +553,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: mobile_scanner name: mobile_scanner
sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044 sha256: c92c26bf2231695b6d3477c8dcf435f51e28f87b1745966b1fe4c47a286171ce
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.1.4" version: "7.2.0"
native_toolchain_c: native_toolchain_c:
dependency: transitive dependency: transitive
description: description:
@@ -597,6 +613,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider: path_provider:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -649,10 +673,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: petitparser name: petitparser
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.2"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@@ -681,10 +705,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: posix name: posix
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.3" version: "6.5.0"
proj4dart: proj4dart:
dependency: transitive dependency: transitive
description: description:
@@ -1006,10 +1030,34 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: uuid name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.2" version: "4.5.3"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
url: "https://pub.dev"
source: hosted
version: "1.1.19"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
url: "https://pub.dev"
source: hosted
version: "1.1.13"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -1043,7 +1091,7 @@ packages:
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
web: web:
dependency: transitive dependency: "direct main"
description: description:
name: web name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
+1
View File
@@ -60,6 +60,7 @@ dependencies:
gpx: ^2.3.0 gpx: ^2.3.0
path_provider: ^2.1.5 path_provider: ^2.1.5
share_plus: ^12.0.1 share_plus: ^12.0.1
material_symbols_icons: ^4.2906.0
web: ^1.1.1 web: ^1.1.1
flutter_svg: ^2.0.10+1 flutter_svg: ^2.0.10+1
@@ -16,6 +16,7 @@ void main() {
elevations: elevations, elevations: elevations,
startAntennaHeightMeters: 2, startAntennaHeightMeters: 2,
endAntennaHeightMeters: 2, endAntennaHeightMeters: 2,
kFactor: 4.0 / 3.0,
); );
expect(result.hasData, isTrue); expect(result.hasData, isTrue);
@@ -36,6 +37,7 @@ void main() {
elevations: elevations, elevations: elevations,
startAntennaHeightMeters: 1.5, startAntennaHeightMeters: 1.5,
endAntennaHeightMeters: 1.5, endAntennaHeightMeters: 1.5,
kFactor: 4.0 / 3.0,
); );
expect(result.hasData, isTrue); expect(result.hasData, isTrue);