mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
feat: Add compass and region support
This commit is contained in:
@@ -2778,5 +2778,24 @@
|
|||||||
"captured": "Captured",
|
"captured": "Captured",
|
||||||
"size": "Size",
|
"size": "Size",
|
||||||
"noCustomChannelsToClear": "No custom channels to clear.",
|
"noCustomChannelsToClear": "No custom channels to clear.",
|
||||||
"noDeviceContactsToClear": "No device contacts to clear."
|
"noDeviceContactsToClear": "No device contacts to clear.",
|
||||||
|
|
||||||
|
"setRegionScope": "Set region scope",
|
||||||
|
"regionScope": "Region scope",
|
||||||
|
"regionScopeNone": "None (global)",
|
||||||
|
"clearRegionScope": "Clear scope",
|
||||||
|
"regionScopeWarning": "Only repeaters allowing this region will forward.",
|
||||||
|
"discoverRegions": "Discover from repeaters",
|
||||||
|
"discoveringRegions": "Discovering regions...",
|
||||||
|
"enterRegionName": "Region name (e.g. auckland)",
|
||||||
|
"noRegionsFound": "No regions found on this repeater.",
|
||||||
|
"regionScopeSet": "Region scope set to {name}",
|
||||||
|
"@regionScopeSet": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"regionScopeCleared": "Region scope cleared"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5009,6 +5009,72 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'No device contacts to clear.'**
|
/// **'No device contacts to clear.'**
|
||||||
String get noDeviceContactsToClear;
|
String get noDeviceContactsToClear;
|
||||||
|
|
||||||
|
/// No description provided for @setRegionScope.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Set region scope'**
|
||||||
|
String get setRegionScope;
|
||||||
|
|
||||||
|
/// No description provided for @regionScope.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Region scope'**
|
||||||
|
String get regionScope;
|
||||||
|
|
||||||
|
/// No description provided for @regionScopeNone.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'None (global)'**
|
||||||
|
String get regionScopeNone;
|
||||||
|
|
||||||
|
/// No description provided for @clearRegionScope.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Clear scope'**
|
||||||
|
String get clearRegionScope;
|
||||||
|
|
||||||
|
/// No description provided for @regionScopeWarning.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Only repeaters allowing this region will forward.'**
|
||||||
|
String get regionScopeWarning;
|
||||||
|
|
||||||
|
/// No description provided for @discoverRegions.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Discover from repeaters'**
|
||||||
|
String get discoverRegions;
|
||||||
|
|
||||||
|
/// No description provided for @discoveringRegions.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Discovering regions...'**
|
||||||
|
String get discoveringRegions;
|
||||||
|
|
||||||
|
/// No description provided for @enterRegionName.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Region name (e.g. auckland)'**
|
||||||
|
String get enterRegionName;
|
||||||
|
|
||||||
|
/// No description provided for @noRegionsFound.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No regions found on this repeater.'**
|
||||||
|
String get noRegionsFound;
|
||||||
|
|
||||||
|
/// No description provided for @regionScopeSet.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Region scope set to {name}'**
|
||||||
|
String regionScopeSet(String name);
|
||||||
|
|
||||||
|
/// No description provided for @regionScopeCleared.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Region scope cleared'**
|
||||||
|
String get regionScopeCleared;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -2705,4 +2705,40 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDeviceContactsToClear => 'Keine Gerätekontakte zum Löschen.';
|
String get noDeviceContactsToClear => 'Keine Gerätekontakte zum Löschen.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setRegionScope => 'Set region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScope => 'Region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeNone => 'None (global)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearRegionScope => 'Clear scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeWarning =>
|
||||||
|
'Only repeaters allowing this region will forward.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoverRegions => 'Discover from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveringRegions => 'Discovering regions...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRegionsFound => 'No regions found on this repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String regionScopeSet(String name) {
|
||||||
|
return 'Region scope set to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeCleared => 'Region scope cleared';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2707,4 +2707,40 @@ class AppLocalizationsEl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDeviceContactsToClear => 'Δεν υπάρχουν επαφές συσκευής.';
|
String get noDeviceContactsToClear => 'Δεν υπάρχουν επαφές συσκευής.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setRegionScope => 'Set region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScope => 'Region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeNone => 'None (global)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearRegionScope => 'Clear scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeWarning =>
|
||||||
|
'Only repeaters allowing this region will forward.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoverRegions => 'Discover from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveringRegions => 'Discovering regions...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRegionsFound => 'No regions found on this repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String regionScopeSet(String name) {
|
||||||
|
return 'Region scope set to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeCleared => 'Region scope cleared';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2669,4 +2669,40 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDeviceContactsToClear => 'No device contacts to clear.';
|
String get noDeviceContactsToClear => 'No device contacts to clear.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setRegionScope => 'Set region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScope => 'Region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeNone => 'None (global)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearRegionScope => 'Clear scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeWarning =>
|
||||||
|
'Only repeaters allowing this region will forward.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoverRegions => 'Discover from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveringRegions => 'Discovering regions...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRegionsFound => 'No regions found on this repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String regionScopeSet(String name) {
|
||||||
|
return 'Region scope set to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeCleared => 'Region scope cleared';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2709,4 +2709,40 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get noDeviceContactsToClear =>
|
String get noDeviceContactsToClear =>
|
||||||
'No hay contactos del dispositivo que borrar.';
|
'No hay contactos del dispositivo que borrar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setRegionScope => 'Set region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScope => 'Region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeNone => 'None (global)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearRegionScope => 'Clear scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeWarning =>
|
||||||
|
'Only repeaters allowing this region will forward.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoverRegions => 'Discover from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveringRegions => 'Discovering regions...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRegionsFound => 'No regions found on this repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String regionScopeSet(String name) {
|
||||||
|
return 'Region scope set to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeCleared => 'Region scope cleared';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2713,4 +2713,40 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get noDeviceContactsToClear =>
|
String get noDeviceContactsToClear =>
|
||||||
'Aucun contact de l\'appareil à effacer.';
|
'Aucun contact de l\'appareil à effacer.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setRegionScope => 'Set region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScope => 'Region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeNone => 'None (global)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearRegionScope => 'Clear scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeWarning =>
|
||||||
|
'Only repeaters allowing this region will forward.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoverRegions => 'Discover from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveringRegions => 'Discovering regions...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRegionsFound => 'No regions found on this repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String regionScopeSet(String name) {
|
||||||
|
return 'Region scope set to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeCleared => 'Region scope cleared';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2682,4 +2682,40 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDeviceContactsToClear => 'Nema kontakata uređaja za brisanje.';
|
String get noDeviceContactsToClear => 'Nema kontakata uređaja za brisanje.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setRegionScope => 'Set region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScope => 'Region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeNone => 'None (global)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearRegionScope => 'Clear scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeWarning =>
|
||||||
|
'Only repeaters allowing this region will forward.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoverRegions => 'Discover from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveringRegions => 'Discovering regions...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRegionsFound => 'No regions found on this repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String regionScopeSet(String name) {
|
||||||
|
return 'Region scope set to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeCleared => 'Region scope cleared';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2699,4 +2699,40 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get noDeviceContactsToClear =>
|
String get noDeviceContactsToClear =>
|
||||||
'Nessun contatto dispositivo da cancellare.';
|
'Nessun contatto dispositivo da cancellare.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setRegionScope => 'Set region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScope => 'Region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeNone => 'None (global)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearRegionScope => 'Clear scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeWarning =>
|
||||||
|
'Only repeaters allowing this region will forward.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoverRegions => 'Discover from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveringRegions => 'Discovering regions...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRegionsFound => 'No regions found on this repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String regionScopeSet(String name) {
|
||||||
|
return 'Region scope set to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeCleared => 'Region scope cleared';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2696,4 +2696,40 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get noDeviceContactsToClear =>
|
String get noDeviceContactsToClear =>
|
||||||
'Brak kontaktów urządzenia do wyczyszczenia.';
|
'Brak kontaktów urządzenia do wyczyszczenia.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setRegionScope => 'Set region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScope => 'Region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeNone => 'None (global)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearRegionScope => 'Clear scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeWarning =>
|
||||||
|
'Only repeaters allowing this region will forward.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoverRegions => 'Discover from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveringRegions => 'Discovering regions...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRegionsFound => 'No regions found on this repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String regionScopeSet(String name) {
|
||||||
|
return 'Region scope set to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeCleared => 'Region scope cleared';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2707,4 +2707,40 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get noDeviceContactsToClear =>
|
String get noDeviceContactsToClear =>
|
||||||
'Sem contactos do dispositivo para limpar.';
|
'Sem contactos do dispositivo para limpar.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setRegionScope => 'Set region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScope => 'Region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeNone => 'None (global)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearRegionScope => 'Clear scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeWarning =>
|
||||||
|
'Only repeaters allowing this region will forward.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoverRegions => 'Discover from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveringRegions => 'Discovering regions...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRegionsFound => 'No regions found on this repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String regionScopeSet(String name) {
|
||||||
|
return 'Region scope set to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeCleared => 'Region scope cleared';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2692,4 +2692,40 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDeviceContactsToClear => 'Нет контактов устройства для очистки.';
|
String get noDeviceContactsToClear => 'Нет контактов устройства для очистки.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setRegionScope => 'Set region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScope => 'Region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeNone => 'None (global)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearRegionScope => 'Clear scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeWarning =>
|
||||||
|
'Only repeaters allowing this region will forward.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoverRegions => 'Discover from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveringRegions => 'Discovering regions...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRegionsFound => 'No regions found on this repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String regionScopeSet(String name) {
|
||||||
|
return 'Region scope set to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeCleared => 'Region scope cleared';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2682,4 +2682,40 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDeviceContactsToClear => 'Ni stikov naprave za brisanje.';
|
String get noDeviceContactsToClear => 'Ni stikov naprave za brisanje.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setRegionScope => 'Set region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScope => 'Region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeNone => 'None (global)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearRegionScope => 'Clear scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeWarning =>
|
||||||
|
'Only repeaters allowing this region will forward.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoverRegions => 'Discover from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveringRegions => 'Discovering regions...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRegionsFound => 'No regions found on this repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String regionScopeSet(String name) {
|
||||||
|
return 'Region scope set to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeCleared => 'Region scope cleared';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2680,4 +2680,40 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDeviceContactsToClear => 'Temizlenecek cihaz kişisi yok.';
|
String get noDeviceContactsToClear => 'Temizlenecek cihaz kişisi yok.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setRegionScope => 'Set region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScope => 'Region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeNone => 'None (global)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearRegionScope => 'Clear scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeWarning =>
|
||||||
|
'Only repeaters allowing this region will forward.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoverRegions => 'Discover from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveringRegions => 'Discovering regions...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRegionsFound => 'No regions found on this repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String regionScopeSet(String name) {
|
||||||
|
return 'Region scope set to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeCleared => 'Region scope cleared';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2703,4 +2703,40 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get noDeviceContactsToClear =>
|
String get noDeviceContactsToClear =>
|
||||||
'Немає контактів пристрою для очищення.';
|
'Немає контактів пристрою для очищення.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setRegionScope => 'Set region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScope => 'Region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeNone => 'None (global)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearRegionScope => 'Clear scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeWarning =>
|
||||||
|
'Only repeaters allowing this region will forward.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoverRegions => 'Discover from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveringRegions => 'Discovering regions...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRegionsFound => 'No regions found on this repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String regionScopeSet(String name) {
|
||||||
|
return 'Region scope set to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeCleared => 'Region scope cleared';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2575,4 +2575,40 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDeviceContactsToClear => '没有设备联系人可清除。';
|
String get noDeviceContactsToClear => '没有设备联系人可清除。';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get setRegionScope => 'Set region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScope => 'Region scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeNone => 'None (global)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get clearRegionScope => 'Clear scope';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeWarning =>
|
||||||
|
'Only repeaters allowing this region will forward.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoverRegions => 'Discover from repeaters';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get discoveringRegions => 'Discovering regions...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get enterRegionName => 'Region name (e.g. auckland)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noRegionsFound => 'No regions found on this repeater.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String regionScopeSet(String name) {
|
||||||
|
return 'Region scope set to $name';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get regionScopeCleared => 'Region scope cleared';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +1,209 @@
|
|||||||
{
|
{
|
||||||
"de": [
|
"de": [
|
||||||
"postConnectDiscoveryTitle",
|
"postConnectDiscoveryTitle",
|
||||||
"postConnectDiscoveryDescription"
|
"postConnectDiscoveryDescription",
|
||||||
|
"setRegionScope",
|
||||||
|
"regionScope",
|
||||||
|
"regionScopeNone",
|
||||||
|
"clearRegionScope",
|
||||||
|
"regionScopeWarning",
|
||||||
|
"discoverRegions",
|
||||||
|
"discoveringRegions",
|
||||||
|
"enterRegionName",
|
||||||
|
"noRegionsFound",
|
||||||
|
"regionScopeSet",
|
||||||
|
"regionScopeCleared"
|
||||||
],
|
],
|
||||||
|
|
||||||
"el": [
|
"el": [
|
||||||
"postConnectDiscoveryTitle",
|
"postConnectDiscoveryTitle",
|
||||||
"postConnectDiscoveryDescription"
|
"postConnectDiscoveryDescription",
|
||||||
|
"setRegionScope",
|
||||||
|
"regionScope",
|
||||||
|
"regionScopeNone",
|
||||||
|
"clearRegionScope",
|
||||||
|
"regionScopeWarning",
|
||||||
|
"discoverRegions",
|
||||||
|
"discoveringRegions",
|
||||||
|
"enterRegionName",
|
||||||
|
"noRegionsFound",
|
||||||
|
"regionScopeSet",
|
||||||
|
"regionScopeCleared"
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
"postConnectDiscoveryTitle",
|
"postConnectDiscoveryTitle",
|
||||||
"postConnectDiscoveryDescription"
|
"postConnectDiscoveryDescription",
|
||||||
|
"setRegionScope",
|
||||||
|
"regionScope",
|
||||||
|
"regionScopeNone",
|
||||||
|
"clearRegionScope",
|
||||||
|
"regionScopeWarning",
|
||||||
|
"discoverRegions",
|
||||||
|
"discoveringRegions",
|
||||||
|
"enterRegionName",
|
||||||
|
"noRegionsFound",
|
||||||
|
"regionScopeSet",
|
||||||
|
"regionScopeCleared"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
"fr": [
|
||||||
"postConnectDiscoveryTitle",
|
"postConnectDiscoveryTitle",
|
||||||
"postConnectDiscoveryDescription"
|
"postConnectDiscoveryDescription",
|
||||||
|
"setRegionScope",
|
||||||
|
"regionScope",
|
||||||
|
"regionScopeNone",
|
||||||
|
"clearRegionScope",
|
||||||
|
"regionScopeWarning",
|
||||||
|
"discoverRegions",
|
||||||
|
"discoveringRegions",
|
||||||
|
"enterRegionName",
|
||||||
|
"noRegionsFound",
|
||||||
|
"regionScopeSet",
|
||||||
|
"regionScopeCleared"
|
||||||
],
|
],
|
||||||
|
|
||||||
"hr": [
|
"hr": [
|
||||||
"postConnectDiscoveryTitle",
|
"postConnectDiscoveryTitle",
|
||||||
"postConnectDiscoveryDescription"
|
"postConnectDiscoveryDescription",
|
||||||
|
"setRegionScope",
|
||||||
|
"regionScope",
|
||||||
|
"regionScopeNone",
|
||||||
|
"clearRegionScope",
|
||||||
|
"regionScopeWarning",
|
||||||
|
"discoverRegions",
|
||||||
|
"discoveringRegions",
|
||||||
|
"enterRegionName",
|
||||||
|
"noRegionsFound",
|
||||||
|
"regionScopeSet",
|
||||||
|
"regionScopeCleared"
|
||||||
],
|
],
|
||||||
|
|
||||||
"it": [
|
"it": [
|
||||||
"postConnectDiscoveryTitle",
|
"postConnectDiscoveryTitle",
|
||||||
"postConnectDiscoveryDescription"
|
"postConnectDiscoveryDescription",
|
||||||
|
"setRegionScope",
|
||||||
|
"regionScope",
|
||||||
|
"regionScopeNone",
|
||||||
|
"clearRegionScope",
|
||||||
|
"regionScopeWarning",
|
||||||
|
"discoverRegions",
|
||||||
|
"discoveringRegions",
|
||||||
|
"enterRegionName",
|
||||||
|
"noRegionsFound",
|
||||||
|
"regionScopeSet",
|
||||||
|
"regionScopeCleared"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pl": [
|
"pl": [
|
||||||
"postConnectDiscoveryTitle",
|
"postConnectDiscoveryTitle",
|
||||||
"postConnectDiscoveryDescription"
|
"postConnectDiscoveryDescription",
|
||||||
|
"setRegionScope",
|
||||||
|
"regionScope",
|
||||||
|
"regionScopeNone",
|
||||||
|
"clearRegionScope",
|
||||||
|
"regionScopeWarning",
|
||||||
|
"discoverRegions",
|
||||||
|
"discoveringRegions",
|
||||||
|
"enterRegionName",
|
||||||
|
"noRegionsFound",
|
||||||
|
"regionScopeSet",
|
||||||
|
"regionScopeCleared"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
"postConnectDiscoveryTitle",
|
"postConnectDiscoveryTitle",
|
||||||
"postConnectDiscoveryDescription"
|
"postConnectDiscoveryDescription",
|
||||||
|
"setRegionScope",
|
||||||
|
"regionScope",
|
||||||
|
"regionScopeNone",
|
||||||
|
"clearRegionScope",
|
||||||
|
"regionScopeWarning",
|
||||||
|
"discoverRegions",
|
||||||
|
"discoveringRegions",
|
||||||
|
"enterRegionName",
|
||||||
|
"noRegionsFound",
|
||||||
|
"regionScopeSet",
|
||||||
|
"regionScopeCleared"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
"postConnectDiscoveryTitle",
|
"postConnectDiscoveryTitle",
|
||||||
"postConnectDiscoveryDescription"
|
"postConnectDiscoveryDescription",
|
||||||
|
"setRegionScope",
|
||||||
|
"regionScope",
|
||||||
|
"regionScopeNone",
|
||||||
|
"clearRegionScope",
|
||||||
|
"regionScopeWarning",
|
||||||
|
"discoverRegions",
|
||||||
|
"discoveringRegions",
|
||||||
|
"enterRegionName",
|
||||||
|
"noRegionsFound",
|
||||||
|
"regionScopeSet",
|
||||||
|
"regionScopeCleared"
|
||||||
],
|
],
|
||||||
|
|
||||||
"sl": [
|
"sl": [
|
||||||
"postConnectDiscoveryTitle",
|
"postConnectDiscoveryTitle",
|
||||||
"postConnectDiscoveryDescription"
|
"postConnectDiscoveryDescription",
|
||||||
|
"setRegionScope",
|
||||||
|
"regionScope",
|
||||||
|
"regionScopeNone",
|
||||||
|
"clearRegionScope",
|
||||||
|
"regionScopeWarning",
|
||||||
|
"discoverRegions",
|
||||||
|
"discoveringRegions",
|
||||||
|
"enterRegionName",
|
||||||
|
"noRegionsFound",
|
||||||
|
"regionScopeSet",
|
||||||
|
"regionScopeCleared"
|
||||||
],
|
],
|
||||||
|
|
||||||
"tr": [
|
"tr": [
|
||||||
"postConnectDiscoveryTitle",
|
"postConnectDiscoveryTitle",
|
||||||
"postConnectDiscoveryDescription"
|
"postConnectDiscoveryDescription",
|
||||||
|
"setRegionScope",
|
||||||
|
"regionScope",
|
||||||
|
"regionScopeNone",
|
||||||
|
"clearRegionScope",
|
||||||
|
"regionScopeWarning",
|
||||||
|
"discoverRegions",
|
||||||
|
"discoveringRegions",
|
||||||
|
"enterRegionName",
|
||||||
|
"noRegionsFound",
|
||||||
|
"regionScopeSet",
|
||||||
|
"regionScopeCleared"
|
||||||
],
|
],
|
||||||
|
|
||||||
"uk": [
|
"uk": [
|
||||||
"postConnectDiscoveryTitle",
|
"postConnectDiscoveryTitle",
|
||||||
"postConnectDiscoveryDescription"
|
"postConnectDiscoveryDescription",
|
||||||
|
"setRegionScope",
|
||||||
|
"regionScope",
|
||||||
|
"regionScopeNone",
|
||||||
|
"clearRegionScope",
|
||||||
|
"regionScopeWarning",
|
||||||
|
"discoverRegions",
|
||||||
|
"discoveringRegions",
|
||||||
|
"enterRegionName",
|
||||||
|
"noRegionsFound",
|
||||||
|
"regionScopeSet",
|
||||||
|
"regionScopeCleared"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh": [
|
"zh": [
|
||||||
"postConnectDiscoveryTitle",
|
"postConnectDiscoveryTitle",
|
||||||
"postConnectDiscoveryDescription"
|
"postConnectDiscoveryDescription",
|
||||||
|
"setRegionScope",
|
||||||
|
"regionScope",
|
||||||
|
"regionScopeNone",
|
||||||
|
"clearRegionScope",
|
||||||
|
"regionScopeWarning",
|
||||||
|
"discoverRegions",
|
||||||
|
"discoveringRegions",
|
||||||
|
"enterRegionName",
|
||||||
|
"noRegionsFound",
|
||||||
|
"regionScopeSet",
|
||||||
|
"regionScopeCleared"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -562,6 +562,13 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
clientRepeat: deviceInfo['clientRepeat'] as bool?,
|
clientRepeat: deviceInfo['clientRepeat'] as bool?,
|
||||||
pathHashMode: deviceInfo['pathHashMode'] as int?,
|
pathHashMode: deviceInfo['pathHashMode'] as int?,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Clear any stale flood scope on connect (firmware v8+)
|
||||||
|
final fwVer = deviceInfo['firmwareVersion'] as int?;
|
||||||
|
if (fwVer != null && fwVer >= 8) {
|
||||||
|
clearFloodScope();
|
||||||
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1734,6 +1741,7 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
required int channelIdx,
|
required int channelIdx,
|
||||||
required String text,
|
required String text,
|
||||||
String? messageId,
|
String? messageId,
|
||||||
|
Uint8List? floodScopeKey,
|
||||||
}) async {
|
}) async {
|
||||||
if (!_activeService.isConnected) {
|
if (!_activeService.isConnected) {
|
||||||
_error = 'Not connected to device';
|
_error = 'Not connected to device';
|
||||||
@@ -1746,10 +1754,12 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
debugPrint(' Channel: $channelIdx');
|
debugPrint(' Channel: $channelIdx');
|
||||||
debugPrint(' Text: $text');
|
debugPrint(' Text: $text');
|
||||||
debugPrint(' MessageID: $messageId');
|
debugPrint(' MessageID: $messageId');
|
||||||
|
debugPrint(' FloodScope: ${floodScopeKey != null ? "set (${floodScopeKey.length}B)" : "none"}');
|
||||||
|
|
||||||
await _activeService.sendChannelMessage(
|
await _activeService.sendChannelMessage(
|
||||||
channelIdx: channelIdx,
|
channelIdx: channelIdx,
|
||||||
text: text,
|
text: text,
|
||||||
|
floodScopeKey: floodScopeKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
debugPrint('✅ [ConnectionProvider] BLE send completed');
|
debugPrint('✅ [ConnectionProvider] BLE send completed');
|
||||||
@@ -1814,10 +1824,21 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clear the flood scope on the device (firmware v8+).
|
||||||
|
Future<void> clearFloodScope() async {
|
||||||
|
if (!_activeService.isConnected) return;
|
||||||
|
try {
|
||||||
|
await _activeService.clearFloodScope();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('⚠️ [ConnectionProvider] Failed to clear flood scope: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> sendChannelData({
|
Future<void> sendChannelData({
|
||||||
required int channelIdx,
|
required int channelIdx,
|
||||||
required int dataType,
|
required int dataType,
|
||||||
required Uint8List payload,
|
required Uint8List payload,
|
||||||
|
Uint8List? floodScopeKey,
|
||||||
}) async {
|
}) async {
|
||||||
if (!_activeService.isConnected) {
|
if (!_activeService.isConnected) {
|
||||||
_error = 'Not connected to device';
|
_error = 'Not connected to device';
|
||||||
@@ -1830,6 +1851,7 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
channelIdx: channelIdx,
|
channelIdx: channelIdx,
|
||||||
dataType: dataType,
|
dataType: dataType,
|
||||||
payload: payload,
|
payload: payload,
|
||||||
|
floodScopeKey: floodScopeKey,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_error = 'Failed to send channel data: $e';
|
_error = 'Failed to send channel data: $e';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart' show MissingPluginException;
|
||||||
import 'package:flutter_map/flutter_map.dart' as flutter_map;
|
import 'package:flutter_map/flutter_map.dart' as flutter_map;
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
@@ -10,6 +11,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:flutter_compass/flutter_compass.dart';
|
import 'package:flutter_compass/flutter_compass.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import '../services/compass_support.dart';
|
||||||
import '../utils/slovenian_crs.dart';
|
import '../utils/slovenian_crs.dart';
|
||||||
import '../providers/contacts_provider.dart';
|
import '../providers/contacts_provider.dart';
|
||||||
import '../providers/messages_provider.dart';
|
import '../providers/messages_provider.dart';
|
||||||
@@ -200,13 +202,18 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _startCompassTracking() {
|
void _startCompassTracking() {
|
||||||
|
if (!CompassSupport.isAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final compassStream = FlutterCompass.events;
|
final compassStream = FlutterCompass.events;
|
||||||
if (compassStream == null) {
|
if (compassStream == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start listening to compass events
|
// Start listening to compass events
|
||||||
_compassStreamSubscription = compassStream.listen((CompassEvent event) {
|
_compassStreamSubscription = compassStream.listen(
|
||||||
|
(CompassEvent event) {
|
||||||
// Check if widget is disposing, mounted, and event has valid heading
|
// Check if widget is disposing, mounted, and event has valid heading
|
||||||
if (_isDisposing || !mounted || event.heading == null) return;
|
if (_isDisposing || !mounted || event.heading == null) return;
|
||||||
|
|
||||||
@@ -239,7 +246,20 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
debugPrint('Compass tracking error: $e');
|
debugPrint('Compass tracking error: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
onError: (Object error, StackTrace stackTrace) {
|
||||||
|
if (_isDisposing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error is MissingPluginException) {
|
||||||
|
debugPrint('Compass plugin is unavailable on this platform');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('Compass tracking stream error: $error');
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadSettings() async {
|
Future<void> _loadSettings() async {
|
||||||
@@ -1004,7 +1024,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
),
|
),
|
||||||
child: DetailedCompassDialog(
|
child: DetailedCompassDialog(
|
||||||
initialPosition: _locationService.currentPosition,
|
initialPosition: _locationService.currentPosition,
|
||||||
initialHeading: _currentHeading,
|
initialHeading: _compassHeading,
|
||||||
contacts: contacts,
|
contacts: contacts,
|
||||||
sarMarkers: sarMarkers,
|
sarMarkers: sarMarkers,
|
||||||
),
|
),
|
||||||
@@ -1030,7 +1050,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
),
|
),
|
||||||
child: DetailedCompassDialog(
|
child: DetailedCompassDialog(
|
||||||
initialPosition: _locationService.currentPosition,
|
initialPosition: _locationService.currentPosition,
|
||||||
initialHeading: _currentHeading,
|
initialHeading: _compassHeading,
|
||||||
contacts: contacts,
|
contacts: contacts,
|
||||||
sarMarkers: sarMarkers,
|
sarMarkers: sarMarkers,
|
||||||
preSelectedContact: selectedContact,
|
preSelectedContact: selectedContact,
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import '../utils/tictactoe_message_parser.dart';
|
|||||||
import '../providers/image_provider.dart' as ip;
|
import '../providers/image_provider.dart' as ip;
|
||||||
import '../services/image_codec_service.dart';
|
import '../services/image_codec_service.dart';
|
||||||
import '../services/image_preferences.dart';
|
import '../services/image_preferences.dart';
|
||||||
|
import '../services/region_scope_preferences.dart';
|
||||||
|
import '../services/region_discovery_service.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -153,6 +155,11 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
MessageDestinationPreferences.destinationTypeChannel;
|
MessageDestinationPreferences.destinationTypeChannel;
|
||||||
Contact? _selectedRecipient;
|
Contact? _selectedRecipient;
|
||||||
|
|
||||||
|
// Region scope state
|
||||||
|
String? _channelRegionScopeName;
|
||||||
|
Uint8List? _channelRegionScopeKey;
|
||||||
|
Map<int, String> _channelRegionScopes = {};
|
||||||
|
|
||||||
// Image sending state
|
// Image sending state
|
||||||
bool _isSendingImage = false;
|
bool _isSendingImage = false;
|
||||||
final ImagePicker _imagePicker = ImagePicker();
|
final ImagePicker _imagePicker = ImagePicker();
|
||||||
@@ -180,6 +187,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
// Load saved message destination
|
// Load saved message destination
|
||||||
_loadSavedDestination();
|
_loadSavedDestination();
|
||||||
_loadVoiceSettings();
|
_loadVoiceSettings();
|
||||||
|
_loadAllChannelRegionScopes();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_checkForNavigationRequest();
|
_checkForNavigationRequest();
|
||||||
});
|
});
|
||||||
@@ -483,6 +491,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_enforceMessageByteLimit();
|
_enforceMessageByteLimit();
|
||||||
|
|
||||||
|
// Load region scope for channel destinations
|
||||||
|
await _loadRegionScope();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show recipient selector bottom sheet
|
/// Show recipient selector bottom sheet
|
||||||
@@ -536,6 +547,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
currentDestinationType: _destinationType,
|
currentDestinationType: _destinationType,
|
||||||
currentRecipientPublicKey: _selectedRecipient?.publicKeyHex,
|
currentRecipientPublicKey: _selectedRecipient?.publicKeyHex,
|
||||||
onSelect: _onRecipientSelected,
|
onSelect: _onRecipientSelected,
|
||||||
|
channelRegionScopes: _channelRegionScopes,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -551,6 +563,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
|
|
||||||
_enforceMessageByteLimit();
|
_enforceMessageByteLimit();
|
||||||
|
|
||||||
|
// Load region scope for channel destinations
|
||||||
|
await _loadRegionScope();
|
||||||
|
|
||||||
// Save to preferences
|
// Save to preferences
|
||||||
await MessageDestinationPreferences.setDestination(
|
await MessageDestinationPreferences.setDestination(
|
||||||
type,
|
type,
|
||||||
@@ -561,6 +576,51 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadRegionScope() async {
|
||||||
|
if (_destinationType !=
|
||||||
|
MessageDestinationPreferences.destinationTypeChannel) {
|
||||||
|
if (_channelRegionScopeName != null) {
|
||||||
|
setState(() {
|
||||||
|
_channelRegionScopeName = null;
|
||||||
|
_channelRegionScopeKey = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final channelIdx = _selectedRecipient?.publicKey[1] ?? 0;
|
||||||
|
final scope = await RegionScopePreferences.getScope(channelIdx);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_channelRegionScopeName = scope?.name;
|
||||||
|
_channelRegionScopeKey = scope?.key;
|
||||||
|
if (scope != null) {
|
||||||
|
_channelRegionScopes[channelIdx] = scope.name;
|
||||||
|
} else {
|
||||||
|
_channelRegionScopes.remove(channelIdx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadAllChannelRegionScopes() async {
|
||||||
|
final contactsProvider = context.read<ContactsProvider>();
|
||||||
|
final channels = contactsProvider.chatContacts
|
||||||
|
.where((c) => c.publicKey.length > 1 && c.publicKey[0] == 0)
|
||||||
|
.toList();
|
||||||
|
final scopes = <int, String>{};
|
||||||
|
// Always check channel 0 (public)
|
||||||
|
final scope0 = await RegionScopePreferences.getScope(0);
|
||||||
|
if (scope0 != null) scopes[0] = scope0.name;
|
||||||
|
for (final ch in channels) {
|
||||||
|
final idx = ch.publicKey[1];
|
||||||
|
final scope = await RegionScopePreferences.getScope(idx);
|
||||||
|
if (scope != null) scopes[idx] = scope.name;
|
||||||
|
}
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_channelRegionScopes = scopes;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _applyPendingDestination({
|
Future<void> _applyPendingDestination({
|
||||||
required String type,
|
required String type,
|
||||||
String? recipientPublicKeyHex,
|
String? recipientPublicKeyHex,
|
||||||
@@ -827,11 +887,12 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
// Add to messages list with "sending" status
|
// Add to messages list with "sending" status
|
||||||
messagesProvider.addSentMessage(sentMessage, contact: _selectedRecipient);
|
messagesProvider.addSentMessage(sentMessage, contact: _selectedRecipient);
|
||||||
|
|
||||||
// Send to selected channel
|
// Send to selected channel (with region scope if set)
|
||||||
await connectionProvider.sendChannelMessage(
|
await connectionProvider.sendChannelMessage(
|
||||||
channelIdx: channelIdx,
|
channelIdx: channelIdx,
|
||||||
text: text,
|
text: text,
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
|
floodScopeKey: _channelRegionScopeKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1063,6 +1124,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
channelIdx: channelIdx ?? 0,
|
channelIdx: channelIdx ?? 0,
|
||||||
text: envelopeText,
|
text: envelopeText,
|
||||||
messageId: msgId,
|
messageId: msgId,
|
||||||
|
floodScopeKey: _channelRegionScopeKey,
|
||||||
);
|
);
|
||||||
} else if (recipient != null) {
|
} else if (recipient != null) {
|
||||||
final sent = await connectionProvider.sendTextMessage(
|
final sent = await connectionProvider.sendTextMessage(
|
||||||
@@ -1096,6 +1158,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
channelIdx: channelIdx ?? 0,
|
channelIdx: channelIdx ?? 0,
|
||||||
dataType: MeshCoreConstants.dataTypeDev,
|
dataType: MeshCoreConstants.dataTypeDev,
|
||||||
payload: fragment.encodeBinary(),
|
payload: fragment.encodeBinary(),
|
||||||
|
floodScopeKey: _channelRegionScopeKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1390,6 +1453,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
channelIdx: channelIdx ?? 0,
|
channelIdx: channelIdx ?? 0,
|
||||||
text: envelopeText,
|
text: envelopeText,
|
||||||
messageId: msgId,
|
messageId: msgId,
|
||||||
|
floodScopeKey: _channelRegionScopeKey,
|
||||||
);
|
);
|
||||||
} else if (recipient != null) {
|
} else if (recipient != null) {
|
||||||
final sentSuccessfully = await connectionProvider.sendTextMessage(
|
final sentSuccessfully = await connectionProvider.sendTextMessage(
|
||||||
@@ -1421,6 +1485,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
channelIdx: channelIdx ?? 0,
|
channelIdx: channelIdx ?? 0,
|
||||||
dataType: MeshCoreConstants.dataTypeDev,
|
dataType: MeshCoreConstants.dataTypeDev,
|
||||||
payload: packet.encodeBinary(),
|
payload: packet.encodeBinary(),
|
||||||
|
floodScopeKey: _channelRegionScopeKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1649,6 +1714,20 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
await _startTicTacToeGame();
|
await _startTicTacToeGame();
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
if (_destinationType ==
|
||||||
|
MessageDestinationPreferences.destinationTypeChannel)
|
||||||
|
_ComposerActionTile(
|
||||||
|
icon: _channelRegionScopeName != null
|
||||||
|
? Icons.language_rounded
|
||||||
|
: Icons.public_rounded,
|
||||||
|
title: _channelRegionScopeName != null
|
||||||
|
? '${l10n.regionScope}: $_channelRegionScopeName'
|
||||||
|
: l10n.setRegionScope,
|
||||||
|
color: const Color(0xFF7C3AED),
|
||||||
|
onTap: () => runAction(() async {
|
||||||
|
_showRegionScopeSheet();
|
||||||
|
}),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
@@ -1682,6 +1761,55 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showRegionScopeSheet() {
|
||||||
|
final channelIdx = _selectedRecipient?.publicKey[1] ?? 0;
|
||||||
|
final contactsProvider = context.read<ContactsProvider>();
|
||||||
|
final connectionProvider = context.read<ConnectionProvider>();
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final repeaters = contactsProvider.contacts
|
||||||
|
.where((c) => c.isRepeater)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
showDragHandle: true,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (sheetContext) {
|
||||||
|
return _RegionScopeSheet(
|
||||||
|
currentScopeName: _channelRegionScopeName,
|
||||||
|
repeaters: repeaters,
|
||||||
|
connectionProvider: connectionProvider,
|
||||||
|
l10n: l10n,
|
||||||
|
onScopeSelected: (String? name) async {
|
||||||
|
Navigator.of(sheetContext).pop();
|
||||||
|
if (name == null) {
|
||||||
|
// Clear scope
|
||||||
|
await RegionScopePreferences.clearScope(channelIdx);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_channelRegionScopeName = null;
|
||||||
|
_channelRegionScopeKey = null;
|
||||||
|
_channelRegionScopes.remove(channelIdx);
|
||||||
|
});
|
||||||
|
ToastLogger.success(context, l10n.regionScopeCleared);
|
||||||
|
} else {
|
||||||
|
// Set scope
|
||||||
|
final key = RegionScopePreferences.deriveRegionKey(name);
|
||||||
|
await RegionScopePreferences.setScope(channelIdx, name, key);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_channelRegionScopeName = name;
|
||||||
|
_channelRegionScopeKey = key;
|
||||||
|
_channelRegionScopes[channelIdx] = name;
|
||||||
|
});
|
||||||
|
ToastLogger.success(context, l10n.regionScopeSet(name));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _showFilteredMessageSearch() {
|
void _showFilteredMessageSearch() {
|
||||||
final messagesProvider = context.read<MessagesProvider>();
|
final messagesProvider = context.read<MessagesProvider>();
|
||||||
final scopedMessages = _getFilteredMessages(messagesProvider);
|
final scopedMessages = _getFilteredMessages(messagesProvider);
|
||||||
@@ -1968,6 +2096,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
channelIdx: 0,
|
channelIdx: 0,
|
||||||
text: sarMessage,
|
text: sarMessage,
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
|
floodScopeKey: _channelRegionScopeKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -2266,6 +2395,10 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
onStartVoiceRecording: _startVoiceRecording,
|
onStartVoiceRecording: _startVoiceRecording,
|
||||||
onStopAndSendVoice: _stopAndSendVoice,
|
onStopAndSendVoice: _stopAndSendVoice,
|
||||||
onSendMessage: _sendMessage,
|
onSendMessage: _sendMessage,
|
||||||
|
regionScopeName: _channelRegionScopeName,
|
||||||
|
onRegionScopeTap: _channelRegionScopeName != null
|
||||||
|
? _showRegionScopeSheet
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -2275,3 +2408,229 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bottom sheet for selecting a region scope for the current channel.
|
||||||
|
class _RegionScopeSheet extends StatefulWidget {
|
||||||
|
final String? currentScopeName;
|
||||||
|
final List<Contact> repeaters;
|
||||||
|
final ConnectionProvider connectionProvider;
|
||||||
|
final AppLocalizations l10n;
|
||||||
|
final ValueChanged<String?> onScopeSelected;
|
||||||
|
|
||||||
|
const _RegionScopeSheet({
|
||||||
|
required this.currentScopeName,
|
||||||
|
required this.repeaters,
|
||||||
|
required this.connectionProvider,
|
||||||
|
required this.l10n,
|
||||||
|
required this.onScopeSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_RegionScopeSheet> createState() => _RegionScopeSheetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RegionScopeSheetState extends State<_RegionScopeSheet> {
|
||||||
|
final TextEditingController _nameController = TextEditingController();
|
||||||
|
List<String> _discoveredRegions = [];
|
||||||
|
bool _isDiscovering = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _discoverRegions() async {
|
||||||
|
if (widget.repeaters.isEmpty) return;
|
||||||
|
setState(() => _isDiscovering = true);
|
||||||
|
|
||||||
|
final allRegions = <String>{};
|
||||||
|
for (final repeater in widget.repeaters) {
|
||||||
|
final regions = await RegionDiscoveryService.discoverFromRepeater(
|
||||||
|
repeaterPublicKey: repeater.publicKey,
|
||||||
|
connectionProvider: widget.connectionProvider,
|
||||||
|
);
|
||||||
|
allRegions.addAll(regions);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_discoveredRegions = allRegions.toList()..sort();
|
||||||
|
_isDiscovering = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_discoveredRegions.isEmpty && mounted) {
|
||||||
|
ToastLogger.info(context, widget.l10n.noRegionsFound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _submitManualName() {
|
||||||
|
var name = _nameController.text.trim();
|
||||||
|
if (name.isEmpty) return;
|
||||||
|
if (!name.startsWith('#')) name = '#$name';
|
||||||
|
if (utf8.encode(name).length > 30) return;
|
||||||
|
widget.onScopeSelected(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
final l10n = widget.l10n;
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: MediaQuery.of(context).size.height * 0.72,
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.regionScope,
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
l10n.regionScopeWarning,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// "None" option
|
||||||
|
_RegionOptionTile(
|
||||||
|
label: l10n.regionScopeNone,
|
||||||
|
isSelected: widget.currentScopeName == null,
|
||||||
|
onTap: () => widget.onScopeSelected(null),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Manual entry
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _nameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: l10n.enterRegionName,
|
||||||
|
isDense: true,
|
||||||
|
prefixText: '#',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onSubmitted: (_) => _submitManualName(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
FilledButton.tonal(
|
||||||
|
onPressed: _submitManualName,
|
||||||
|
child: const Icon(Icons.check_rounded, size: 20),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Discover button
|
||||||
|
if (widget.repeaters.isNotEmpty)
|
||||||
|
FilledButton.tonalIcon(
|
||||||
|
onPressed: _isDiscovering ? null : _discoverRegions,
|
||||||
|
icon: _isDiscovering
|
||||||
|
? const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.search_rounded, size: 18),
|
||||||
|
label: Text(
|
||||||
|
_isDiscovering
|
||||||
|
? l10n.discoveringRegions
|
||||||
|
: l10n.discoverRegions,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Discovered regions
|
||||||
|
if (_discoveredRegions.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
for (final region in _discoveredRegions) ...[
|
||||||
|
_RegionOptionTile(
|
||||||
|
label: region,
|
||||||
|
isSelected: widget.currentScopeName == region,
|
||||||
|
onTap: () => widget.onScopeSelected(region),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RegionOptionTile extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final bool isSelected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _RegionOptionTile({
|
||||||
|
required this.label,
|
||||||
|
required this.isSelected,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
return Material(
|
||||||
|
color: isSelected
|
||||||
|
? colorScheme.primaryContainer
|
||||||
|
: colorScheme.surfaceContainerLow,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isSelected
|
||||||
|
? Icons.radio_button_checked_rounded
|
||||||
|
: Icons.radio_button_off_rounded,
|
||||||
|
size: 20,
|
||||||
|
color: isSelected
|
||||||
|
? colorScheme.primary
|
||||||
|
: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
||||||
|
color: isSelected
|
||||||
|
? colorScheme.onPrimaryContainer
|
||||||
|
: colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -523,6 +523,7 @@ class _AddSensorSheetState extends State<AddSensorSheet> {
|
|||||||
class SensorCustomizeView extends StatelessWidget {
|
class SensorCustomizeView extends StatelessWidget {
|
||||||
final String publicKeyHex;
|
final String publicKeyHex;
|
||||||
final Contact? initialContact;
|
final Contact? initialContact;
|
||||||
|
final bool showLivePreview;
|
||||||
final Future<void> Function({
|
final Future<void> Function({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required String publicKeyHex,
|
required String publicKeyHex,
|
||||||
@@ -532,9 +533,11 @@ class SensorCustomizeView extends StatelessWidget {
|
|||||||
onRenameMetric;
|
onRenameMetric;
|
||||||
|
|
||||||
const SensorCustomizeView({
|
const SensorCustomizeView({
|
||||||
|
super.key,
|
||||||
required this.publicKeyHex,
|
required this.publicKeyHex,
|
||||||
required this.initialContact,
|
required this.initialContact,
|
||||||
required this.onRenameMetric,
|
required this.onRenameMetric,
|
||||||
|
this.showLivePreview = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -579,6 +582,7 @@ class SensorCustomizeView extends StatelessWidget {
|
|||||||
body: ListView(
|
body: ListView(
|
||||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 24),
|
padding: EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||||
children: [
|
children: [
|
||||||
|
if (showLivePreview)
|
||||||
_SensorCustomizeSectionCard(
|
_SensorCustomizeSectionCard(
|
||||||
title: AppLocalizations.of(context)!.livePreview,
|
title: AppLocalizations.of(context)!.livePreview,
|
||||||
subtitle:
|
subtitle:
|
||||||
@@ -602,7 +606,10 @@ class SensorCustomizeView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
fieldSpans: {
|
fieldSpans: {
|
||||||
for (final field in visibleFields)
|
for (final field in visibleFields)
|
||||||
field: sensorsProvider.fieldSpanFor(publicKeyHex, field),
|
field: sensorsProvider.fieldSpanFor(
|
||||||
|
publicKeyHex,
|
||||||
|
field,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
emptyMetricsMessage: 'No telemetry fields available yet.',
|
emptyMetricsMessage: 'No telemetry fields available yet.',
|
||||||
|
|||||||
11
lib/services/compass_support.dart
Normal file
11
lib/services/compass_support.dart
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import 'package:flutter/foundation.dart'
|
||||||
|
show TargetPlatform, defaultTargetPlatform, kIsWeb;
|
||||||
|
|
||||||
|
class CompassSupport {
|
||||||
|
const CompassSupport._();
|
||||||
|
|
||||||
|
static bool get isAvailable =>
|
||||||
|
!kIsWeb &&
|
||||||
|
(defaultTargetPlatform == TargetPlatform.android ||
|
||||||
|
defaultTargetPlatform == TargetPlatform.iOS);
|
||||||
|
}
|
||||||
72
lib/services/region_discovery_service.dart
Normal file
72
lib/services/region_discovery_service.dart
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import '../providers/connection_provider.dart';
|
||||||
|
|
||||||
|
/// Discovers available regions from repeater contacts via anonymous requests.
|
||||||
|
///
|
||||||
|
/// The firmware repeater responds to ANON_REQ_TYPE_REGIONS (0x01) with a
|
||||||
|
/// comma-separated list of region names that have flood allowed.
|
||||||
|
class RegionDiscoveryService {
|
||||||
|
static const int _anonReqTypeRegions = 0x01;
|
||||||
|
|
||||||
|
/// Discover regions from a single repeater.
|
||||||
|
///
|
||||||
|
/// Sends an anonymous request to the repeater and waits for the response.
|
||||||
|
/// Returns a list of region names (with `#` prefix).
|
||||||
|
/// Returns empty list on timeout or error.
|
||||||
|
static Future<List<String>> discoverFromRepeater({
|
||||||
|
required Uint8List repeaterPublicKey,
|
||||||
|
required ConnectionProvider connectionProvider,
|
||||||
|
Duration timeout = const Duration(seconds: 10),
|
||||||
|
}) async {
|
||||||
|
final result = await connectionProvider.sendAnonRequest(
|
||||||
|
contactPublicKey: repeaterPublicKey,
|
||||||
|
requestData: Uint8List.fromList([_anonReqTypeRegions]),
|
||||||
|
);
|
||||||
|
if (result == null) return [];
|
||||||
|
|
||||||
|
final tag = result.tag;
|
||||||
|
final completer = Completer<List<String>>();
|
||||||
|
|
||||||
|
void onResponse(Uint8List publicKeyPrefix, int responseTag, Uint8List data) {
|
||||||
|
if (responseTag != tag || completer.isCompleted) return;
|
||||||
|
completer.complete(_parseRegionResponse(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionProvider.onBinaryResponse = onResponse;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await completer.future.timeout(
|
||||||
|
timeout,
|
||||||
|
onTimeout: () => <String>[],
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('⚠️ [RegionDiscovery] Error discovering regions: $e');
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
// Restore previous handler — callers should re-set if needed
|
||||||
|
if (connectionProvider.onBinaryResponse == onResponse) {
|
||||||
|
connectionProvider.onBinaryResponse = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the region response payload.
|
||||||
|
///
|
||||||
|
/// Format: [4B sender_timestamp][4B repeater_clock][comma-separated names]
|
||||||
|
/// Names are returned without `#` prefix from firmware; we add it back.
|
||||||
|
static List<String> _parseRegionResponse(Uint8List data) {
|
||||||
|
if (data.length <= 8) return [];
|
||||||
|
|
||||||
|
final namesStr = utf8.decode(data.sublist(8), allowMalformed: true).trim();
|
||||||
|
if (namesStr.isEmpty || namesStr == '-none-') return [];
|
||||||
|
|
||||||
|
return namesStr
|
||||||
|
.split(',')
|
||||||
|
.map((name) => name.trim())
|
||||||
|
.where((name) => name.isNotEmpty && name != '*' && !name.startsWith('\$'))
|
||||||
|
.map((name) => name.startsWith('#') ? name : '#$name')
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
86
lib/services/region_scope_preferences.dart
Normal file
86
lib/services/region_scope_preferences.dart
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'profiles_feature_service.dart';
|
||||||
|
|
||||||
|
/// Stores per-channel region scope settings.
|
||||||
|
///
|
||||||
|
/// Each channel can optionally have a region scope assigned. When set, the app
|
||||||
|
/// will set the flood scope on the device before sending channel messages so
|
||||||
|
/// that repeaters outside the region won't forward them.
|
||||||
|
class RegionScopePreferences {
|
||||||
|
static const String _namePrefix = 'region_scope_name_';
|
||||||
|
static const String _keyPrefix = 'region_scope_key_';
|
||||||
|
|
||||||
|
/// Get the saved scope for a channel.
|
||||||
|
/// Returns null if no scope is set.
|
||||||
|
static Future<({String name, Uint8List key})?> getScope(
|
||||||
|
int channelIdx,
|
||||||
|
) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final name = prefs.getString(
|
||||||
|
ProfileStorageScope.scopedKey('$_namePrefix$channelIdx'),
|
||||||
|
);
|
||||||
|
final keyBase64 = prefs.getString(
|
||||||
|
ProfileStorageScope.scopedKey('$_keyPrefix$channelIdx'),
|
||||||
|
);
|
||||||
|
if (name == null || keyBase64 == null) return null;
|
||||||
|
try {
|
||||||
|
return (name: name, key: Uint8List.fromList(base64.decode(keyBase64)));
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save a region scope for a channel.
|
||||||
|
static Future<void> setScope(
|
||||||
|
int channelIdx,
|
||||||
|
String name,
|
||||||
|
Uint8List key,
|
||||||
|
) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(
|
||||||
|
ProfileStorageScope.scopedKey('$_namePrefix$channelIdx'),
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
await prefs.setString(
|
||||||
|
ProfileStorageScope.scopedKey('$_keyPrefix$channelIdx'),
|
||||||
|
base64.encode(key),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the region scope for a channel.
|
||||||
|
static Future<void> clearScope(int channelIdx) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove(
|
||||||
|
ProfileStorageScope.scopedKey('$_namePrefix$channelIdx'),
|
||||||
|
);
|
||||||
|
await prefs.remove(
|
||||||
|
ProfileStorageScope.scopedKey('$_keyPrefix$channelIdx'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all region scopes (all channels).
|
||||||
|
static Future<void> clearAllScopes() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final keys = prefs.getKeys();
|
||||||
|
for (final key in keys) {
|
||||||
|
if (key.contains(_namePrefix) || key.contains(_keyPrefix)) {
|
||||||
|
await prefs.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derive the 16-byte transport key for a region name.
|
||||||
|
///
|
||||||
|
/// Matches firmware `TransportKeyStore::getAutoKeyFor`:
|
||||||
|
/// `SHA256("#regionname")` → first 16 bytes.
|
||||||
|
/// The name must include the leading `#` (auto-prepended if missing).
|
||||||
|
static Uint8List deriveRegionKey(String regionName) {
|
||||||
|
final normalized =
|
||||||
|
regionName.startsWith('#') ? regionName : '#$regionName';
|
||||||
|
final digest = sha256.convert(utf8.encode(normalized));
|
||||||
|
return Uint8List.fromList(digest.bytes.sublist(0, 16));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart' show MissingPluginException;
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:flutter_compass/flutter_compass.dart';
|
import 'package:flutter_compass/flutter_compass.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import '../../services/compass_support.dart';
|
||||||
import '../../models/contact.dart';
|
import '../../models/contact.dart';
|
||||||
import '../../models/sar_marker.dart';
|
import '../../models/sar_marker.dart';
|
||||||
import '../common/location_display.dart';
|
import '../common/location_display.dart';
|
||||||
@@ -90,17 +92,33 @@ class _DetailedCompassDialogState extends State<DetailedCompassDialog> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to compass updates
|
// Subscribe to compass updates on supported targets only.
|
||||||
final compassStream = FlutterCompass.events;
|
final compassStream = CompassSupport.isAvailable
|
||||||
|
? FlutterCompass.events
|
||||||
|
: null;
|
||||||
if (compassStream != null) {
|
if (compassStream != null) {
|
||||||
_compassSubscription = compassStream.listen((event) {
|
_compassSubscription = compassStream.listen(
|
||||||
|
(event) {
|
||||||
if (mounted && event.heading != null) {
|
if (mounted && event.heading != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentHeading = event.heading;
|
_currentHeading = event.heading;
|
||||||
_compassAccuracy = event.accuracy;
|
_compassAccuracy = event.accuracy;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
onError: (Object error, StackTrace stackTrace) {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error is MissingPluginException) {
|
||||||
|
debugPrint('Compass plugin is unavailable on this platform');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('Compass dialog stream error: $error');
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to position updates
|
// Subscribe to position updates
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ class MessagesComposer extends StatelessWidget {
|
|||||||
final Future<void> Function() onStartVoiceRecording;
|
final Future<void> Function() onStartVoiceRecording;
|
||||||
final Future<void> Function() onStopAndSendVoice;
|
final Future<void> Function() onStopAndSendVoice;
|
||||||
final Future<void> Function() onSendMessage;
|
final Future<void> Function() onSendMessage;
|
||||||
|
final String? regionScopeName;
|
||||||
|
final VoidCallback? onRegionScopeTap;
|
||||||
|
|
||||||
const MessagesComposer({
|
const MessagesComposer({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -49,6 +51,8 @@ class MessagesComposer extends StatelessWidget {
|
|||||||
required this.onStartVoiceRecording,
|
required this.onStartVoiceRecording,
|
||||||
required this.onStopAndSendVoice,
|
required this.onStopAndSendVoice,
|
||||||
required this.onSendMessage,
|
required this.onSendMessage,
|
||||||
|
this.regionScopeName,
|
||||||
|
this.onRegionScopeTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -102,6 +106,14 @@ class MessagesComposer extends StatelessWidget {
|
|||||||
? onStopAndSendVoice
|
? onStopAndSendVoice
|
||||||
: onShowComposerActions,
|
: onShowComposerActions,
|
||||||
),
|
),
|
||||||
|
if (regionScopeName != null &&
|
||||||
|
onRegionScopeTap != null) ...[
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
_RegionScopeChip(
|
||||||
|
name: regionScopeName!,
|
||||||
|
onTap: onRegionScopeTap!,
|
||||||
|
),
|
||||||
|
],
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _DestinationSelector(
|
child: _DestinationSelector(
|
||||||
@@ -534,3 +546,52 @@ class _SendButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _RegionScopeChip extends StatelessWidget {
|
||||||
|
final String name;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _RegionScopeChip({required this.name, required this.onTap});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
height: 40,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.tertiaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: colorScheme.tertiary.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.language_rounded,
|
||||||
|
size: 16,
|
||||||
|
color: colorScheme.onTertiaryContainer,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 80),
|
||||||
|
child: Text(
|
||||||
|
name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colorScheme.onTertiaryContainer,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ class RecipientSelectorSheet extends StatefulWidget {
|
|||||||
final bool showAllOption;
|
final bool showAllOption;
|
||||||
final Function(String type, Contact? recipient) onSelect;
|
final Function(String type, Contact? recipient) onSelect;
|
||||||
|
|
||||||
|
/// Region scope names per channel index (e.g. {0: "#auckland"}).
|
||||||
|
final Map<int, String> channelRegionScopes;
|
||||||
|
|
||||||
const RecipientSelectorSheet({
|
const RecipientSelectorSheet({
|
||||||
super.key,
|
super.key,
|
||||||
required this.contacts,
|
required this.contacts,
|
||||||
@@ -29,6 +32,7 @@ class RecipientSelectorSheet extends StatefulWidget {
|
|||||||
this.currentRecipientPublicKey,
|
this.currentRecipientPublicKey,
|
||||||
this.showAllOption = true,
|
this.showAllOption = true,
|
||||||
required this.onSelect,
|
required this.onSelect,
|
||||||
|
this.channelRegionScopes = const {},
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -165,13 +169,18 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
|||||||
|
|
||||||
String _channelSubtitle(BuildContext context, Contact channel) {
|
String _channelSubtitle(BuildContext context, Contact channel) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final channelIdx = channel.publicKey.length > 1 ? channel.publicKey[1] : 0;
|
||||||
|
final scopeName = widget.channelRegionScopes[channelIdx];
|
||||||
|
|
||||||
if (channel.isPublicChannel) {
|
if (channel.isPublicChannel) {
|
||||||
return l10n.broadcastToAllNearby;
|
return scopeName != null
|
||||||
|
? '${l10n.broadcastToAllNearby} • $scopeName'
|
||||||
|
: l10n.broadcastToAllNearby;
|
||||||
}
|
}
|
||||||
|
|
||||||
final channelIdx = channel.publicKey.length > 1 ? channel.publicKey[1] : 0;
|
|
||||||
final shortKey = channel.publicKeyShort.toUpperCase();
|
final shortKey = channel.publicKeyShort.toUpperCase();
|
||||||
return '${l10n.channel} $channelIdx • $shortKey';
|
final base = '${l10n.channel} $channelIdx • $shortKey';
|
||||||
|
return scopeName != null ? '$base • $scopeName' : base;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isDenseSection(String type) => type == 'channel' || type == 'contact';
|
bool _isDenseSection(String type) => type == 'channel' || type == 'contact';
|
||||||
|
|||||||
@@ -827,8 +827,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: dfc5b82c3f8a7f160ca77e45dc65a9eb39cebc5a
|
ref: a0deff8
|
||||||
resolved-ref: dfc5b82c3f8a7f160ca77e45dc65a9eb39cebc5a
|
resolved-ref: a0deff80fcbca974f0e18fe47971b76bec583109
|
||||||
url: "https://github.com/dz0ny/meshcore_client.git"
|
url: "https://github.com/dz0ny/meshcore_client.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ dependencies:
|
|||||||
meshcore_client:
|
meshcore_client:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/dz0ny/meshcore_client.git
|
url: https://github.com/dz0ny/meshcore_client.git
|
||||||
ref: dfc5b82c3f8a7f160ca77e45dc65a9eb39cebc5a
|
ref: a0deff8
|
||||||
|
|
||||||
# Codec2 ultra-low-bitrate speech codec (FFI plugin)
|
# Codec2 ultra-low-bitrate speech codec (FFI plugin)
|
||||||
codec2_flutter:
|
codec2_flutter:
|
||||||
|
|||||||
@@ -4,25 +4,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:meshcore_sar_app/l10n/app_localizations.dart';
|
import 'package:meshcore_sar_app/l10n/app_localizations.dart';
|
||||||
import 'package:meshcore_sar_app/models/contact.dart';
|
import 'package:meshcore_sar_app/models/contact.dart';
|
||||||
import 'package:meshcore_sar_app/providers/contacts_provider.dart';
|
|
||||||
import 'package:meshcore_sar_app/providers/sensors_provider.dart';
|
|
||||||
import 'package:meshcore_sar_app/screens/sensors_tab.dart';
|
import 'package:meshcore_sar_app/screens/sensors_tab.dart';
|
||||||
import 'package:meshcore_sar_app/widgets/sensors/sensor_telemetry_card.dart';
|
import 'package:meshcore_sar_app/widgets/sensors/sensor_telemetry_card.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
setUp(() {
|
|
||||||
SharedPreferences.setMockInitialValues({});
|
|
||||||
});
|
|
||||||
|
|
||||||
Future<void> waitUntilLoaded(SensorsProvider provider) async {
|
|
||||||
for (var i = 0; i < 20 && !provider.isLoaded; i++) {
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
|
||||||
}
|
|
||||||
expect(provider.isLoaded, isTrue);
|
|
||||||
}
|
|
||||||
|
|
||||||
Contact buildSensorContact({
|
Contact buildSensorContact({
|
||||||
int firstByte = 0x44,
|
int firstByte = 0x44,
|
||||||
String name = 'WX Station',
|
String name = 'WX Station',
|
||||||
@@ -54,49 +39,37 @@ void main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
testWidgets('customize action opens full customization view', (tester) async {
|
testWidgets('metric selector item shows channel-specific preview', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
final contact = buildSensorContact();
|
final contact = buildSensorContact();
|
||||||
final sensorsProvider = SensorsProvider();
|
final option = sensorMetricOptionsFor(contact, labelOverrides: const {})
|
||||||
final contactsProvider = ContactsProvider();
|
.firstWhere(
|
||||||
|
(entry) => entry.key.contains('illuminance') && entry.channel == 2,
|
||||||
await waitUntilLoaded(sensorsProvider);
|
);
|
||||||
contactsProvider.addOrUpdateContact(contact);
|
|
||||||
await sensorsProvider.addSensor(contact);
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MultiProvider(
|
MaterialApp(
|
||||||
providers: [
|
|
||||||
ChangeNotifierProvider<ContactsProvider>.value(
|
|
||||||
value: contactsProvider,
|
|
||||||
),
|
|
||||||
ChangeNotifierProvider<SensorsProvider>.value(value: sensorsProvider),
|
|
||||||
],
|
|
||||||
child: MaterialApp(
|
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
home: SensorCustomizeView(
|
home: Scaffold(
|
||||||
publicKeyHex: contact.publicKeyHex,
|
body: SensorMetricSelectorItem(
|
||||||
initialContact: contact,
|
option: option,
|
||||||
onRenameMetric:
|
visible: true,
|
||||||
({
|
span: 1,
|
||||||
required BuildContext context,
|
canMoveUp: false,
|
||||||
required String publicKeyHex,
|
canMoveDown: false,
|
||||||
required SensorMetricOption option,
|
onToggle: (_) {},
|
||||||
required SensorsProvider sensorsProvider,
|
onRename: () {},
|
||||||
}) async {},
|
onSpanChanged: (_) {},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(find.text('Customize WX Station'), findsOneWidget);
|
|
||||||
expect(find.text('Live preview'), findsOneWidget);
|
|
||||||
expect(find.text('Refresh schedule'), findsOneWidget);
|
|
||||||
expect(
|
expect(
|
||||||
find.byKey(
|
find.byKey(ValueKey('sensor_selector_metric_${option.key}')),
|
||||||
const ValueKey('sensor_selector_metric_channel_extra:illuminance_2'),
|
|
||||||
),
|
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
);
|
);
|
||||||
expect(find.text('Channel 2'), findsOneWidget);
|
expect(find.text('Channel 2'), findsOneWidget);
|
||||||
|
|||||||
23
test/services/compass_support_test.dart
Normal file
23
test/services/compass_support_test.dart
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:meshcore_sar_app/services/compass_support.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
tearDown(() {
|
||||||
|
debugDefaultTargetPlatformOverride = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reports compass support only on mobile targets', () {
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||||
|
expect(CompassSupport.isAvailable, isTrue);
|
||||||
|
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||||
|
expect(CompassSupport.isAvailable, isTrue);
|
||||||
|
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.macOS;
|
||||||
|
expect(CompassSupport.isAvailable, isFalse);
|
||||||
|
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.windows;
|
||||||
|
expect(CompassSupport.isAvailable, isFalse);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user