mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-15 10:20:33 +00:00
Compare commits
15 Commits
v2026.0308
...
v2026.0308
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d113f7a881 | ||
|
|
2ab8681ac4 | ||
|
|
de5fb1e164 | ||
|
|
dae7e67a8a | ||
|
|
d51924021f | ||
|
|
0f91292fd7 | ||
|
|
b471ab0990 | ||
|
|
771cddb80d | ||
|
|
bfd15065f0 | ||
|
|
f72db76538 | ||
|
|
a96f96ca4c | ||
|
|
9f62055eef | ||
|
|
6b2f18130c | ||
|
|
922b40b755 | ||
|
|
17d7c43745 |
@@ -489,7 +489,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 105;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -511,7 +511,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 105;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -530,7 +530,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 105;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -547,7 +547,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 105;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -679,7 +679,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 105;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -702,7 +702,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 105;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>105</string>
|
||||
<string>106</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
1398
lib/l10n/app_de.arb
1398
lib/l10n/app_de.arb
File diff suppressed because it is too large
Load Diff
2619
lib/l10n/app_el.arb
2619
lib/l10n/app_el.arb
File diff suppressed because it is too large
Load Diff
1728
lib/l10n/app_en.arb
1728
lib/l10n/app_en.arb
File diff suppressed because it is too large
Load Diff
1387
lib/l10n/app_es.arb
1387
lib/l10n/app_es.arb
File diff suppressed because it is too large
Load Diff
1392
lib/l10n/app_fr.arb
1392
lib/l10n/app_fr.arb
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1392
lib/l10n/app_it.arb
1392
lib/l10n/app_it.arb
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -29,9 +29,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get disconnect => 'Trennen';
|
||||
|
||||
@override
|
||||
String get scanningForDevices => 'Suche nach Geräten...';
|
||||
|
||||
@override
|
||||
String get noDevicesFound => 'Keine Geräte gefunden';
|
||||
|
||||
@@ -63,27 +60,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get failedToGetGpsLocation =>
|
||||
'GPS-Position konnte nicht abgerufen werden';
|
||||
|
||||
@override
|
||||
String advertisedAtLocation(String latitude, String longitude) {
|
||||
return 'Position gesendet bei $latitude, $longitude';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToAdvertise(String error) {
|
||||
return 'Senden fehlgeschlagen: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String reconnecting(int attempt, int max) {
|
||||
return 'Wiederverbindung... ($attempt/$max)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelReconnection => 'Wiederverbindung abbrechen';
|
||||
|
||||
@override
|
||||
String get mapManagement => 'Kartenverwaltung';
|
||||
|
||||
@override
|
||||
String get general => 'Allgemein';
|
||||
|
||||
@@ -130,13 +114,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get displayPacketActivity =>
|
||||
'Paketaktivitätsindikatoren in der oberen Leiste anzeigen';
|
||||
|
||||
@override
|
||||
String get simpleMode => 'Einfacher Modus';
|
||||
|
||||
@override
|
||||
String get simpleModeDescription =>
|
||||
'Nicht wesentliche Informationen in Nachrichten und Kontakten ausblenden';
|
||||
|
||||
@override
|
||||
String get disableMap => 'Karte deaktivieren';
|
||||
|
||||
@@ -150,74 +127,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get chooseLanguage => 'Sprache auswählen';
|
||||
|
||||
@override
|
||||
String get english => 'Englisch';
|
||||
|
||||
@override
|
||||
String get slovenian => 'Slowenisch';
|
||||
|
||||
@override
|
||||
String get croatian => 'Kroatisch';
|
||||
|
||||
@override
|
||||
String get german => 'Deutsch';
|
||||
|
||||
@override
|
||||
String get spanish => 'Spanisch';
|
||||
|
||||
@override
|
||||
String get french => 'Französisch';
|
||||
|
||||
@override
|
||||
String get italian => 'Italienisch';
|
||||
|
||||
@override
|
||||
String get locationBroadcasting => 'Standortübertragung';
|
||||
|
||||
@override
|
||||
String get autoLocationTracking => 'Automatisches Standort-Tracking';
|
||||
|
||||
@override
|
||||
String get automaticallyBroadcastPosition =>
|
||||
'Positionsaktualisierungen automatisch übertragen';
|
||||
|
||||
@override
|
||||
String get configureTracking => 'Tracking konfigurieren';
|
||||
|
||||
@override
|
||||
String get distanceAndTimeThresholds => 'Entfernungs- und Zeitschwellenwerte';
|
||||
|
||||
@override
|
||||
String get locationTrackingConfiguration => 'Standort-Tracking-Konfiguration';
|
||||
|
||||
@override
|
||||
String get configureWhenLocationBroadcasts =>
|
||||
'Konfigurieren Sie, wann Standortübertragungen an das Mesh-Netzwerk gesendet werden';
|
||||
|
||||
@override
|
||||
String get minimumDistance => 'Mindestentfernung';
|
||||
|
||||
@override
|
||||
String broadcastAfterMoving(String distance) {
|
||||
return 'Nur nach Bewegung von $distance Metern übertragen';
|
||||
}
|
||||
|
||||
@override
|
||||
String get maximumDistance => 'Maximale Entfernung';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastAfterMoving(String distance) {
|
||||
return 'Immer nach Bewegung von $distance Metern übertragen';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minimumTimeInterval => 'Minimales Zeitintervall';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastEvery(String duration) {
|
||||
return 'Immer alle $duration übertragen';
|
||||
}
|
||||
|
||||
@override
|
||||
String get save => 'Speichern';
|
||||
|
||||
@@ -253,12 +162,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get moreInfo => 'Mehr Info';
|
||||
|
||||
@override
|
||||
String get learnMoreAbout => 'Erfahren Sie mehr über MeshCore SAR';
|
||||
|
||||
@override
|
||||
String get developer => 'Entwickler';
|
||||
|
||||
@override
|
||||
String get packageName => 'Paketname';
|
||||
|
||||
@@ -335,9 +238,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get refresh => 'Aktualisieren';
|
||||
|
||||
@override
|
||||
String get sendDirectMessage => 'Senden';
|
||||
|
||||
@override
|
||||
String get resetPath => 'Pfad zurücksetzen (Umleitung)';
|
||||
|
||||
@@ -457,21 +357,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return '$count Zeichnung$plural teilen';
|
||||
}
|
||||
|
||||
@override
|
||||
String sentDrawingsToRoom(int count, String plural, String roomName) {
|
||||
return '$count Kartenzeichnung$plural an $roomName gesendet';
|
||||
}
|
||||
|
||||
@override
|
||||
String sharedDrawingsToRoom(
|
||||
int success,
|
||||
int total,
|
||||
String plural,
|
||||
String roomName,
|
||||
) {
|
||||
return '$success/$total Zeichnung$plural mit $roomName geteilt';
|
||||
}
|
||||
|
||||
@override
|
||||
String get showReceivedDrawings => 'Empfangene Zeichnungen anzeigen';
|
||||
|
||||
@@ -493,9 +378,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get clearAll => 'Alle löschen';
|
||||
|
||||
@override
|
||||
String get noLocalDrawings => 'Keine lokalen Zeichnungen zum Teilen';
|
||||
|
||||
@override
|
||||
String get publicChannel => 'Öffentlicher Kanal';
|
||||
|
||||
@@ -505,37 +387,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get storedPermanently => 'Dauerhaft im Raum gespeichert';
|
||||
|
||||
@override
|
||||
String drawingsSentToPublicChannel(int count, String plural) {
|
||||
return '$count Kartenzeichnung$plural an öffentlichen Kanal gesendet';
|
||||
}
|
||||
|
||||
@override
|
||||
String drawingsSharedToPublicChannel(int success, int total) {
|
||||
return '$success/$total Zeichnungen mit öffentlichem Kanal geteilt';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedToDevice => 'Nicht mit Gerät verbunden';
|
||||
|
||||
@override
|
||||
String get directMessage => 'Direktnachricht';
|
||||
|
||||
@override
|
||||
String directMessageSentTo(String contactName) {
|
||||
return 'Direktnachricht an $contactName gesendet';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToSend(String error) {
|
||||
return 'Senden fehlgeschlagen: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String directMessageInfo(String contactName) {
|
||||
return 'Diese Nachricht wird direkt an $contactName gesendet. Sie erscheint auch im Hauptnachrichten-Feed.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get typeYourMessage => 'Geben Sie Ihre Nachricht ein...';
|
||||
|
||||
@@ -565,9 +419,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get location => 'Standort';
|
||||
|
||||
@override
|
||||
String get myLocation => 'Mein Standort';
|
||||
|
||||
@override
|
||||
String get fromMap => 'Von Karte';
|
||||
|
||||
@@ -630,9 +481,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Flut';
|
||||
|
||||
@override
|
||||
String get admin => 'Admin';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Angemeldet';
|
||||
|
||||
@@ -642,29 +490,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get distance => 'Entfernung';
|
||||
|
||||
@override
|
||||
String pingingDirect(String name) {
|
||||
return 'Pinge $name (direkt über Pfad)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingingFlood(String name) {
|
||||
return 'Pinge $name (Flutung - kein Pfad)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String directPingTimeout(String name) {
|
||||
return 'Direkter Ping-Timeout - wiederhole $name mit Flutung...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingSuccessful(String name, String fallback) {
|
||||
return 'Ping erfolgreich an $name$fallback';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viaFloodingFallback => ' (über Flutungs-Fallback)';
|
||||
|
||||
@override
|
||||
String pingFailed(String name) {
|
||||
return 'Ping fehlgeschlagen an $name - keine Antwort erhalten';
|
||||
@@ -675,16 +505,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return 'Sind Sie sicher, dass Sie \"$name\" löschen möchten?\n\nDies entfernt den Kontakt sowohl aus der App als auch vom Begleitfunkgerät.';
|
||||
}
|
||||
|
||||
@override
|
||||
String removingContact(String name) {
|
||||
return 'Entferne $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactRemoved(String name) {
|
||||
return 'Kontakt \"$name\" entfernt';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToRemoveContact(String error) {
|
||||
return 'Fehler beim Entfernen des Kontakts: $error';
|
||||
@@ -729,11 +549,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get telemetry => 'Telemetrie';
|
||||
|
||||
@override
|
||||
String requestingTelemetry(String name) {
|
||||
return 'Fordere Telemetrie von $name an...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get voltage => 'Spannung';
|
||||
|
||||
@@ -805,9 +620,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get showAll => 'Alle anzeigen';
|
||||
|
||||
@override
|
||||
String get nearbyContacts => 'Kontakte in der Nähe';
|
||||
|
||||
@override
|
||||
String get locationUnavailable => 'Standort nicht verfügbar';
|
||||
|
||||
@@ -826,7 +638,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String latLonFormat(String latitude, String longitude) {
|
||||
return 'Lat: $latitude Lon: $longitude';
|
||||
return 'Breite: $latitude Lange: $longitude';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -848,175 +660,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get channels => 'Kanäle';
|
||||
|
||||
@override
|
||||
String get cacheStatistics => 'Cache-Statistiken';
|
||||
|
||||
@override
|
||||
String get totalTiles => 'Gesamte Tiles';
|
||||
|
||||
@override
|
||||
String get cacheSize => 'Cache-Größe';
|
||||
|
||||
@override
|
||||
String get storeName => 'Speichername';
|
||||
|
||||
@override
|
||||
String get noCacheStatistics => 'Keine Cache-Statistiken verfügbar';
|
||||
|
||||
@override
|
||||
String get downloadRegion => 'Region herunterladen';
|
||||
|
||||
@override
|
||||
String get mapLayer => 'Kartenebene';
|
||||
|
||||
@override
|
||||
String get regionBounds => 'Regionsgrenzen';
|
||||
|
||||
@override
|
||||
String get north => 'Nord';
|
||||
|
||||
@override
|
||||
String get south => 'Süd';
|
||||
|
||||
@override
|
||||
String get east => 'Ost';
|
||||
|
||||
@override
|
||||
String get west => 'West';
|
||||
|
||||
@override
|
||||
String get zoomLevels => 'Zoom-Stufen';
|
||||
|
||||
@override
|
||||
String minZoom(int zoom) {
|
||||
return 'Min: $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoom(int zoom) {
|
||||
return 'Max: $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String get downloadingDots => 'Lädt herunter...';
|
||||
|
||||
@override
|
||||
String get cancelDownload => 'Download abbrechen';
|
||||
|
||||
@override
|
||||
String get downloadRegionButton => 'Region herunterladen';
|
||||
|
||||
@override
|
||||
String get downloadNote =>
|
||||
'Hinweis: Große Regionen oder hohe Zoom-Stufen können erhebliche Zeit und Speicherplatz benötigen.';
|
||||
|
||||
@override
|
||||
String get cacheManagement => 'Cache-Verwaltung';
|
||||
|
||||
@override
|
||||
String get clearAllMaps => 'Alle Karten löschen';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmTitle => 'Alle Karten löschen';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmMessage =>
|
||||
'Sind Sie sicher, dass Sie alle heruntergeladenen Karten löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.';
|
||||
|
||||
@override
|
||||
String get mapDownloadCompleted => 'Karten-Download abgeschlossen!';
|
||||
|
||||
@override
|
||||
String get cacheClearedSuccessfully => 'Cache erfolgreich gelöscht!';
|
||||
|
||||
@override
|
||||
String get downloadCancelled => 'Download abgebrochen';
|
||||
|
||||
@override
|
||||
String get startingDownload => 'Starte Download...';
|
||||
|
||||
@override
|
||||
String get downloadingMapTiles => 'Lade Karten-Tiles herunter...';
|
||||
|
||||
@override
|
||||
String get downloadCompletedSuccessfully =>
|
||||
'Download erfolgreich abgeschlossen!';
|
||||
|
||||
@override
|
||||
String get cancellingDownload => 'Breche Download ab...';
|
||||
|
||||
@override
|
||||
String errorLoadingStats(String error) {
|
||||
return 'Fehler beim Laden der Statistiken: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String downloadFailed(String error) {
|
||||
return 'Download fehlgeschlagen: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String cancelFailed(String error) {
|
||||
return 'Abbruch fehlgeschlagen: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String clearCacheFailed(String error) {
|
||||
return 'Löschen des Caches fehlgeschlagen: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String minZoomError(String error) {
|
||||
return 'Min-Zoom: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoomError(String error) {
|
||||
return 'Max-Zoom: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minZoomGreaterThanMax =>
|
||||
'Minimaler Zoom muss kleiner oder gleich dem maximalen Zoom sein';
|
||||
|
||||
@override
|
||||
String get selectMapLayer => 'Kartenebene auswählen';
|
||||
|
||||
@override
|
||||
String get mapOptions => 'Kartenoptionen';
|
||||
|
||||
@override
|
||||
String get showLegend => 'Legende anzeigen';
|
||||
|
||||
@override
|
||||
String get displayMarkerTypeCounts => 'Markierungstypen-Anzahl anzeigen';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Karte mit Kurs drehen';
|
||||
|
||||
@override
|
||||
String get mapFollowsDirection =>
|
||||
'Karte folgt Ihrer Richtung während der Bewegung';
|
||||
|
||||
@override
|
||||
String get resetMapRotation => 'Drehung zurücksetzen';
|
||||
|
||||
@override
|
||||
String get resetMapRotationTooltip => 'Karte nach Norden zurücksetzen';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Karten-Debug-Info anzeigen';
|
||||
|
||||
@override
|
||||
String get displayZoomLevelBounds => 'Zoom-Stufe und Grenzen anzeigen';
|
||||
|
||||
@override
|
||||
String get fullscreenMode => 'Vollbildmodus';
|
||||
|
||||
@override
|
||||
String get hideUiFullMapView =>
|
||||
'Alle UI-Steuerelemente für volle Kartenansicht ausblenden';
|
||||
|
||||
@override
|
||||
String get openStreetMap => 'OpenStreetMap';
|
||||
|
||||
@@ -1035,12 +681,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get googleTerrain => 'Google Gelände';
|
||||
|
||||
@override
|
||||
String get downloadVisibleArea => 'Sichtbaren Bereich herunterladen';
|
||||
|
||||
@override
|
||||
String get initializingMap => 'Initialisiere Karte...';
|
||||
|
||||
@override
|
||||
String get dragToPosition => 'Zur Position ziehen';
|
||||
|
||||
@@ -1056,53 +696,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get sarAlert => 'SAR-ALARM';
|
||||
|
||||
@override
|
||||
String get messageSentToPublicChannel =>
|
||||
'Nachricht an öffentlichen Kanal gesendet';
|
||||
|
||||
@override
|
||||
String get pleaseSelectRoomToSendSar =>
|
||||
'Bitte wählen Sie einen Raum zum Senden der SAR-Markierung';
|
||||
|
||||
@override
|
||||
String failedToSendSarMarker(String error) {
|
||||
return 'Fehler beim Senden der SAR-Markierung: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String sarMarkerSentTo(String roomName) {
|
||||
return 'SAR-Markierung an $roomName gesendet';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedCannotSync =>
|
||||
'Nicht verbunden - Nachrichten können nicht synchronisiert werden';
|
||||
|
||||
@override
|
||||
String syncedMessageCount(int count) {
|
||||
return '$count Nachricht(en) synchronisiert';
|
||||
}
|
||||
|
||||
@override
|
||||
String get noNewMessages => 'Keine neuen Nachrichten';
|
||||
|
||||
@override
|
||||
String syncFailed(String error) {
|
||||
return 'Synchronisation fehlgeschlagen: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get failedToResendMessage =>
|
||||
'Fehler beim erneuten Senden der Nachricht';
|
||||
|
||||
@override
|
||||
String get retryingMessage => 'Wiederhole Nachricht...';
|
||||
|
||||
@override
|
||||
String retryFailed(String error) {
|
||||
return 'Wiederholen fehlgeschlagen: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get textCopiedToClipboard => 'Text in Zwischenablage kopiert';
|
||||
|
||||
@@ -1114,9 +707,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get cannotReplyContactNotFound =>
|
||||
'Antwort nicht möglich: Kontakt nicht gefunden';
|
||||
|
||||
@override
|
||||
String get messageDeleted => 'Nachricht gelöscht';
|
||||
|
||||
@override
|
||||
String get copyText => 'Text kopieren';
|
||||
|
||||
@@ -1153,12 +743,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get sarLocationShare => 'SAR Standort';
|
||||
|
||||
@override
|
||||
String get locationShared => 'Standort geteilt';
|
||||
|
||||
@override
|
||||
String get refreshedContacts => 'Kontakte aktualisiert';
|
||||
|
||||
@override
|
||||
String get justNow => 'Gerade eben';
|
||||
|
||||
@@ -1200,7 +784,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get failed => 'Fehlgeschlagen';
|
||||
|
||||
@override
|
||||
String get broadcast => 'Broadcast';
|
||||
String get broadcast => 'Rundspruch';
|
||||
|
||||
@override
|
||||
String deliveredToContacts(int delivered, int total) {
|
||||
@@ -1260,130 +844,28 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String maxPowerDbm(int power) {
|
||||
return 'Max: $power dBm';
|
||||
return 'Max.: $power dBm';
|
||||
}
|
||||
|
||||
@override
|
||||
String get you => 'Du';
|
||||
|
||||
@override
|
||||
String get offlineVectorMaps => 'Offline-Vektorkarten';
|
||||
|
||||
@override
|
||||
String get offlineVectorMapsDescription =>
|
||||
'Importieren und verwalten Sie Offline-Vektorkarten-Tiles (MBTiles-Format) zur Verwendung ohne Internetverbindung';
|
||||
|
||||
@override
|
||||
String get importMbtiles => 'MBTiles-Datei importieren';
|
||||
|
||||
@override
|
||||
String get importMbtilesNote =>
|
||||
'Unterstützt MBTiles-Dateien mit Vektor-Tiles (PBF/MVT-Format). Geofabrik-Auszüge funktionieren hervorragend!';
|
||||
|
||||
@override
|
||||
String get noMbtilesFiles => 'Keine Offline-Vektorkarten gefunden';
|
||||
|
||||
@override
|
||||
String get mbtilesImportedSuccessfully =>
|
||||
'MBTiles-Datei erfolgreich importiert';
|
||||
|
||||
@override
|
||||
String get failedToImportMbtiles =>
|
||||
'Fehler beim Importieren der MBTiles-Datei';
|
||||
|
||||
@override
|
||||
String get deleteMbtilesConfirmTitle => 'Offline-Karte löschen';
|
||||
|
||||
@override
|
||||
String deleteMbtilesConfirmMessage(String name) {
|
||||
return 'Sind Sie sicher, dass Sie \"$name\" löschen möchten? Dies entfernt die Offline-Karte dauerhaft.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mbtilesDeletedSuccessfully => 'Offline-Karte erfolgreich gelöscht';
|
||||
|
||||
@override
|
||||
String get failedToDeleteMbtiles => 'Fehler beim Löschen der Offline-Karte';
|
||||
|
||||
@override
|
||||
String get importExportCachedTiles => 'Import/Export gecachter Kacheln';
|
||||
|
||||
@override
|
||||
String get importExportDescription =>
|
||||
'Sichern, teilen und wiederherstellen Sie heruntergeladene Kartenkacheln zwischen Geräten';
|
||||
|
||||
@override
|
||||
String get exportTilesToFile => 'Kacheln in Datei exportieren';
|
||||
|
||||
@override
|
||||
String get importTilesFromFile => 'Kacheln aus Datei importieren';
|
||||
|
||||
@override
|
||||
String get selectExportLocation => 'Exportspeicherort wählen';
|
||||
|
||||
@override
|
||||
String get selectImportFile => 'Kachel-Archiv auswählen';
|
||||
|
||||
@override
|
||||
String get exportingTiles => 'Exportiere Kacheln...';
|
||||
|
||||
@override
|
||||
String get importingTiles => 'Importiere Kacheln...';
|
||||
|
||||
@override
|
||||
String exportSuccess(int count) {
|
||||
return '$count Kacheln erfolgreich exportiert';
|
||||
}
|
||||
|
||||
@override
|
||||
String importSuccess(int count) {
|
||||
return '$count Speicher erfolgreich importiert';
|
||||
}
|
||||
|
||||
@override
|
||||
String exportFailed(String error) {
|
||||
return 'Export failed: $error';
|
||||
return 'Export fehlgeschlagen: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String importFailed(String error) {
|
||||
return 'Import failed: $error';
|
||||
return 'Import fehlgeschlagen: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get exportNote =>
|
||||
'Erstellt eine komprimierte Archivdatei (.fmtc), die auf anderen Geräten geteilt und importiert werden kann.';
|
||||
|
||||
@override
|
||||
String get importNote =>
|
||||
'Importiert Kartenkacheln aus einer zuvor exportierten Archivdatei. Kacheln werden mit dem vorhandenen Cache zusammengeführt.';
|
||||
|
||||
@override
|
||||
String get noTilesToExport => 'Keine Kacheln zum Exportieren verfügbar';
|
||||
|
||||
@override
|
||||
String archiveContainsStores(int count) {
|
||||
return 'Archiv enthält $count Speicher';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vectorTiles => 'Vektor-Tiles';
|
||||
|
||||
@override
|
||||
String get schema => 'Schema';
|
||||
|
||||
@override
|
||||
String get unknown => 'Unbekannt';
|
||||
|
||||
@override
|
||||
String get bounds => 'Grenzen';
|
||||
|
||||
@override
|
||||
String get onlineLayers => 'Online-Ebenen';
|
||||
|
||||
@override
|
||||
String get offlineLayers => 'Offline-Ebenen';
|
||||
|
||||
@override
|
||||
String get locationTrail => 'Standortverlauf';
|
||||
|
||||
@@ -1423,47 +905,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get trailControls => 'Verlaufssteuerung';
|
||||
|
||||
@override
|
||||
String get exportTrailToGpx => 'Verlauf als GPX exportieren';
|
||||
|
||||
@override
|
||||
String get importTrailFromGpx => 'Verlauf aus GPX importieren';
|
||||
|
||||
@override
|
||||
String get trailExportedSuccessfully => 'Verlauf erfolgreich exportiert!';
|
||||
|
||||
@override
|
||||
String get failedToExportTrail => 'Exportieren des Verlaufs fehlgeschlagen';
|
||||
|
||||
@override
|
||||
String failedToImportTrail(String error) {
|
||||
return 'Importieren des Verlaufs fehlgeschlagen: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get importTrail => 'Verlauf importieren';
|
||||
|
||||
@override
|
||||
String importTrailQuestion(int pointCount) {
|
||||
return 'Verlauf mit $pointCount Punkten importieren?\n\nSie können Ihren aktuellen Verlauf ersetzen oder daneben anzeigen.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viewAlongside => 'Daneben anzeigen';
|
||||
|
||||
@override
|
||||
String get replaceCurrent => 'Aktuellen ersetzen';
|
||||
|
||||
@override
|
||||
String trailImported(int pointCount) {
|
||||
return 'Verlauf importiert! ($pointCount Punkte)';
|
||||
}
|
||||
|
||||
@override
|
||||
String trailReplaced(int pointCount) {
|
||||
return 'Verlauf ersetzt! ($pointCount Punkte)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactTrails => 'Kontaktverläufe';
|
||||
|
||||
@@ -1526,10 +967,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get telemetryAndLocationSharing => 'Telemetrie & Standortfreigabe';
|
||||
|
||||
@override
|
||||
String get lat => 'Lat';
|
||||
String get lat => 'Breite';
|
||||
|
||||
@override
|
||||
String get lon => 'Lon';
|
||||
String get lon => 'Lange';
|
||||
|
||||
@override
|
||||
String get useCurrentLocation => 'Aktuellen Standort verwenden';
|
||||
@@ -1541,7 +982,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get chatNode => 'Chat-Knoten';
|
||||
|
||||
@override
|
||||
String get repeater => 'Repeater';
|
||||
String get repeater => 'Relais';
|
||||
|
||||
@override
|
||||
String get roomChannel => 'Raum/Kanal';
|
||||
@@ -1585,7 +1026,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get templateName => 'Vorlagenname';
|
||||
|
||||
@override
|
||||
String get templateNameHint => 'e.g. Found Person';
|
||||
String get templateNameHint => 'z. B. Gefundene Person';
|
||||
|
||||
@override
|
||||
String get templateEmoji => 'Emoji';
|
||||
@@ -1597,16 +1038,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get nameRequired => 'Name ist erforderlich';
|
||||
|
||||
@override
|
||||
String get templateDescription => 'Description (Optional)';
|
||||
String get templateDescription => 'Beschreibung (optional)';
|
||||
|
||||
@override
|
||||
String get templateDescriptionHint => 'Add additional context...';
|
||||
String get templateDescriptionHint => 'Zusatzlichen Kontext hinzufugen...';
|
||||
|
||||
@override
|
||||
String get templateColor => 'Color';
|
||||
String get templateColor => 'Farbe';
|
||||
|
||||
@override
|
||||
String get previewFormat => 'Preview (SAR Message Format)';
|
||||
String get previewFormat => 'Vorschau (SAR-Nachrichtenformat)';
|
||||
|
||||
@override
|
||||
String get importFromClipboard => 'Importieren';
|
||||
@@ -1616,26 +1057,26 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String deleteTemplateConfirmation(String name) {
|
||||
return 'Delete template \'$name\'?';
|
||||
return 'Vorlage \'$name\' loschen?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get templateAdded => 'Template added';
|
||||
String get templateAdded => 'Vorlage hinzugefugt';
|
||||
|
||||
@override
|
||||
String get templateUpdated => 'Template updated';
|
||||
String get templateUpdated => 'Vorlage aktualisiert';
|
||||
|
||||
@override
|
||||
String get templateDeleted => 'Template deleted';
|
||||
String get templateDeleted => 'Vorlage geloscht';
|
||||
|
||||
@override
|
||||
String templatesImported(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Imported $count templates',
|
||||
one: 'Imported 1 template',
|
||||
zero: 'No templates imported',
|
||||
other: '$count Vorlagen importiert',
|
||||
one: '1 Vorlage importiert',
|
||||
zero: 'Keine Vorlagen importiert',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -1645,8 +1086,8 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Exported $count templates to clipboard',
|
||||
one: 'Exported 1 template to clipboard',
|
||||
other: '$count Vorlagen in die Zwischenablage exportiert',
|
||||
one: '1 Vorlage in die Zwischenablage exportiert',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -1665,10 +1106,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get resetComplete => 'Vorlagen auf Standard zurückgesetzt';
|
||||
|
||||
@override
|
||||
String get noTemplates => 'No templates available';
|
||||
String get noTemplates => 'Keine Vorlagen verfugbar';
|
||||
|
||||
@override
|
||||
String get tapAddToCreate => 'Tap + to create your first template';
|
||||
String get tapAddToCreate =>
|
||||
'Tippen Sie auf +, um Ihre erste Vorlage zu erstellen';
|
||||
|
||||
@override
|
||||
String get ok => 'OK';
|
||||
@@ -1736,20 +1178,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get noRoomsFound => 'Keine Räume gefunden';
|
||||
|
||||
@override
|
||||
String get noContactsOrRoomsAvailable =>
|
||||
'Keine Kontakte oder Räume verfügbar';
|
||||
|
||||
@override
|
||||
String get noRecipientsAvailable => 'Keine Empfänger verfügbar';
|
||||
|
||||
@override
|
||||
String get noChannelsFound => 'Keine Kanäle gefunden';
|
||||
|
||||
@override
|
||||
String get messagesWillBeSentToPublicChannel =>
|
||||
'Nachrichten werden an öffentlichen Kanal gesendet';
|
||||
|
||||
@override
|
||||
String get newMessage => 'Neue Nachricht';
|
||||
|
||||
@@ -1932,10 +1366,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get rectangleDrawing => 'Rechteck';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard =>
|
||||
'Koordinaten in Zwischenablage kopiert';
|
||||
|
||||
@override
|
||||
String get manualCoordinates => 'Manuelle Koordinaten';
|
||||
|
||||
@@ -1948,31 +1378,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get longitudeLabel => 'Längengrad';
|
||||
|
||||
@override
|
||||
String get invalidLatitude => 'Ungültiger Breitengrad (-90 bis 90)';
|
||||
|
||||
@override
|
||||
String get invalidLongitude => 'Ungültiger Längengrad (-180 bis 180)';
|
||||
|
||||
@override
|
||||
String get exampleCoordinates => 'Beispiel: 46.0569, 14.5058';
|
||||
|
||||
@override
|
||||
String get drawingShared => 'Kartenzeichnung';
|
||||
|
||||
@override
|
||||
String get drawingHidden => 'Zeichnung von Karte ausgeblendet';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
return '$count bereits geteilt';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return '$count neue Zeichnung(en) geteilt';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareDrawing => 'Zeichnung teilen';
|
||||
|
||||
@@ -2028,12 +1436,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get forestRoads => 'Waldwege';
|
||||
|
||||
@override
|
||||
String get showCadastralParcels => 'Katasterparzellen anzeigen';
|
||||
|
||||
@override
|
||||
String get showForestRoads => 'Waldwege anzeigen';
|
||||
|
||||
@override
|
||||
String get wmsOverlays => 'WMS Überlagerungen';
|
||||
|
||||
@@ -2138,10 +1540,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return 'Kanal konnte nicht gelöscht werden: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get allChannelSlotsInUse =>
|
||||
'Alle Kanalplätze sind belegt (maximal 39 benutzerdefinierte Kanäle)';
|
||||
|
||||
@override
|
||||
String get createChannel => 'Kanal erstellen';
|
||||
|
||||
@@ -2183,21 +1581,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get wizardConnectingFeature3 =>
|
||||
'Funktioniert vollständig offline - kein Internet erforderlich';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeTitle => 'Einfacher Modus';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeDescription =>
|
||||
'Neu im Mesh-Netzwerk? Aktivieren Sie den einfachen Modus für eine optimierte Benutzeroberfläche mit nur wesentlichen Funktionen.';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature1 =>
|
||||
'Anfängerfreundliche Benutzeroberfläche mit Kernfunktionen';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature2 =>
|
||||
'Jederzeit in den erweiterten Modus in den Einstellungen wechseln';
|
||||
|
||||
@override
|
||||
String get wizardChannelTitle => 'Kanäle';
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,9 +29,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get disconnect => 'Disconnect';
|
||||
|
||||
@override
|
||||
String get scanningForDevices => 'Scanning for devices...';
|
||||
|
||||
@override
|
||||
String get noDevicesFound => 'No devices found';
|
||||
|
||||
@@ -62,27 +59,14 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get failedToGetGpsLocation => 'Failed to get GPS location';
|
||||
|
||||
@override
|
||||
String advertisedAtLocation(String latitude, String longitude) {
|
||||
return 'Advertised at $latitude, $longitude';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToAdvertise(String error) {
|
||||
return 'Failed to advertise: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String reconnecting(int attempt, int max) {
|
||||
return 'Reconnecting... ($attempt/$max)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelReconnection => 'Cancel reconnection';
|
||||
|
||||
@override
|
||||
String get mapManagement => 'Map Management';
|
||||
|
||||
@override
|
||||
String get general => 'General';
|
||||
|
||||
@@ -129,13 +113,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get displayPacketActivity =>
|
||||
'Display packet activity indicators in top bar';
|
||||
|
||||
@override
|
||||
String get simpleMode => 'Simple Mode';
|
||||
|
||||
@override
|
||||
String get simpleModeDescription =>
|
||||
'Hide non-essential information in messages and contacts';
|
||||
|
||||
@override
|
||||
String get disableMap => 'Disable Map';
|
||||
|
||||
@@ -149,74 +126,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get chooseLanguage => 'Choose Language';
|
||||
|
||||
@override
|
||||
String get english => 'English';
|
||||
|
||||
@override
|
||||
String get slovenian => 'Slovenian';
|
||||
|
||||
@override
|
||||
String get croatian => 'Croatian';
|
||||
|
||||
@override
|
||||
String get german => 'German';
|
||||
|
||||
@override
|
||||
String get spanish => 'Spanish';
|
||||
|
||||
@override
|
||||
String get french => 'French';
|
||||
|
||||
@override
|
||||
String get italian => 'Italian';
|
||||
|
||||
@override
|
||||
String get locationBroadcasting => 'Location Broadcasting';
|
||||
|
||||
@override
|
||||
String get autoLocationTracking => 'Auto Location Tracking';
|
||||
|
||||
@override
|
||||
String get automaticallyBroadcastPosition =>
|
||||
'Automatically broadcast position updates';
|
||||
|
||||
@override
|
||||
String get configureTracking => 'Configure Tracking';
|
||||
|
||||
@override
|
||||
String get distanceAndTimeThresholds => 'Distance and time thresholds';
|
||||
|
||||
@override
|
||||
String get locationTrackingConfiguration => 'Location Tracking Configuration';
|
||||
|
||||
@override
|
||||
String get configureWhenLocationBroadcasts =>
|
||||
'Configure when location broadcasts are sent to the mesh network';
|
||||
|
||||
@override
|
||||
String get minimumDistance => 'Minimum Distance';
|
||||
|
||||
@override
|
||||
String broadcastAfterMoving(String distance) {
|
||||
return 'Broadcast only after moving $distance meters';
|
||||
}
|
||||
|
||||
@override
|
||||
String get maximumDistance => 'Maximum Distance';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastAfterMoving(String distance) {
|
||||
return 'Always broadcast after moving $distance meters';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minimumTimeInterval => 'Minimum Time Interval';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastEvery(String duration) {
|
||||
return 'Always broadcast every $duration';
|
||||
}
|
||||
|
||||
@override
|
||||
String get save => 'Save';
|
||||
|
||||
@@ -252,12 +161,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get moreInfo => 'More Info';
|
||||
|
||||
@override
|
||||
String get learnMoreAbout => 'Learn more about MeshCore SAR';
|
||||
|
||||
@override
|
||||
String get developer => 'Developer';
|
||||
|
||||
@override
|
||||
String get packageName => 'Package Name';
|
||||
|
||||
@@ -333,9 +236,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get refresh => 'Refresh';
|
||||
|
||||
@override
|
||||
String get sendDirectMessage => 'Send';
|
||||
|
||||
@override
|
||||
String get resetPath => 'Reset Path (Re-route)';
|
||||
|
||||
@@ -455,21 +355,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Share $count Drawing$plural';
|
||||
}
|
||||
|
||||
@override
|
||||
String sentDrawingsToRoom(int count, String plural, String roomName) {
|
||||
return 'Sent $count map drawing$plural to $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String sharedDrawingsToRoom(
|
||||
int success,
|
||||
int total,
|
||||
String plural,
|
||||
String roomName,
|
||||
) {
|
||||
return 'Shared $success/$total drawing$plural to $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get showReceivedDrawings => 'Show Received Drawings';
|
||||
|
||||
@@ -491,9 +376,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get clearAll => 'Clear All';
|
||||
|
||||
@override
|
||||
String get noLocalDrawings => 'No local drawings to share';
|
||||
|
||||
@override
|
||||
String get publicChannel => 'Public Channel';
|
||||
|
||||
@@ -503,37 +385,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get storedPermanently => 'Stored permanently in room';
|
||||
|
||||
@override
|
||||
String drawingsSentToPublicChannel(int count, String plural) {
|
||||
return 'Sent $count map drawing$plural to Public Channel';
|
||||
}
|
||||
|
||||
@override
|
||||
String drawingsSharedToPublicChannel(int success, int total) {
|
||||
return 'Shared $success/$total drawings to Public Channel';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedToDevice => 'Not connected to device';
|
||||
|
||||
@override
|
||||
String get directMessage => 'Direct Message';
|
||||
|
||||
@override
|
||||
String directMessageSentTo(String contactName) {
|
||||
return 'Direct message sent to $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToSend(String error) {
|
||||
return 'Failed to send: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String directMessageInfo(String contactName) {
|
||||
return 'This message will be sent directly to $contactName. It will also appear in the main messages feed.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get typeYourMessage => 'Type your message...';
|
||||
|
||||
@@ -563,9 +417,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get location => 'Location';
|
||||
|
||||
@override
|
||||
String get myLocation => 'My Location';
|
||||
|
||||
@override
|
||||
String get fromMap => 'From Map';
|
||||
|
||||
@@ -627,9 +478,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Flood';
|
||||
|
||||
@override
|
||||
String get admin => 'Admin';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Logged In';
|
||||
|
||||
@@ -639,29 +487,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get distance => 'Distance';
|
||||
|
||||
@override
|
||||
String pingingDirect(String name) {
|
||||
return 'Pinging $name (direct via path)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingingFlood(String name) {
|
||||
return 'Pinging $name (flooding - no path)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String directPingTimeout(String name) {
|
||||
return 'Direct ping timeout - retrying $name with flooding...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingSuccessful(String name, String fallback) {
|
||||
return 'Ping successful to $name$fallback';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viaFloodingFallback => ' (via flooding fallback)';
|
||||
|
||||
@override
|
||||
String pingFailed(String name) {
|
||||
return 'Ping failed to $name - no response received';
|
||||
@@ -672,16 +502,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Are you sure you want to delete \"$name\"?\n\nThis will remove the contact from both the app and the companion radio device.';
|
||||
}
|
||||
|
||||
@override
|
||||
String removingContact(String name) {
|
||||
return 'Removing $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactRemoved(String name) {
|
||||
return 'Contact \"$name\" removed';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToRemoveContact(String error) {
|
||||
return 'Failed to remove contact: $error';
|
||||
@@ -726,11 +546,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get telemetry => 'Telemetry';
|
||||
|
||||
@override
|
||||
String requestingTelemetry(String name) {
|
||||
return 'Requesting telemetry from $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get voltage => 'Voltage';
|
||||
|
||||
@@ -802,9 +617,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get showAll => 'Show All';
|
||||
|
||||
@override
|
||||
String get nearbyContacts => 'Nearby Contacts';
|
||||
|
||||
@override
|
||||
String get locationUnavailable => 'Location unavailable';
|
||||
|
||||
@@ -845,173 +657,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get channels => 'Channels';
|
||||
|
||||
@override
|
||||
String get cacheStatistics => 'Cache Statistics';
|
||||
|
||||
@override
|
||||
String get totalTiles => 'Total Tiles';
|
||||
|
||||
@override
|
||||
String get cacheSize => 'Cache Size';
|
||||
|
||||
@override
|
||||
String get storeName => 'Store Name';
|
||||
|
||||
@override
|
||||
String get noCacheStatistics => 'No cache statistics available';
|
||||
|
||||
@override
|
||||
String get downloadRegion => 'Download Region';
|
||||
|
||||
@override
|
||||
String get mapLayer => 'Map Layer';
|
||||
|
||||
@override
|
||||
String get regionBounds => 'Region Bounds';
|
||||
|
||||
@override
|
||||
String get north => 'North';
|
||||
|
||||
@override
|
||||
String get south => 'South';
|
||||
|
||||
@override
|
||||
String get east => 'East';
|
||||
|
||||
@override
|
||||
String get west => 'West';
|
||||
|
||||
@override
|
||||
String get zoomLevels => 'Zoom Levels';
|
||||
|
||||
@override
|
||||
String minZoom(int zoom) {
|
||||
return 'Min: $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoom(int zoom) {
|
||||
return 'Max: $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String get downloadingDots => 'Downloading...';
|
||||
|
||||
@override
|
||||
String get cancelDownload => 'Cancel Download';
|
||||
|
||||
@override
|
||||
String get downloadRegionButton => 'Download Region';
|
||||
|
||||
@override
|
||||
String get downloadNote =>
|
||||
'Note: Large regions or high zoom levels may take significant time and storage.';
|
||||
|
||||
@override
|
||||
String get cacheManagement => 'Cache Management';
|
||||
|
||||
@override
|
||||
String get clearAllMaps => 'Clear All Maps';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmTitle => 'Clear All Maps';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmMessage =>
|
||||
'Are you sure you want to delete all downloaded maps? This action cannot be undone.';
|
||||
|
||||
@override
|
||||
String get mapDownloadCompleted => 'Map download completed!';
|
||||
|
||||
@override
|
||||
String get cacheClearedSuccessfully => 'Cache cleared successfully!';
|
||||
|
||||
@override
|
||||
String get downloadCancelled => 'Download cancelled';
|
||||
|
||||
@override
|
||||
String get startingDownload => 'Starting download...';
|
||||
|
||||
@override
|
||||
String get downloadingMapTiles => 'Downloading map tiles...';
|
||||
|
||||
@override
|
||||
String get downloadCompletedSuccessfully =>
|
||||
'Download completed successfully!';
|
||||
|
||||
@override
|
||||
String get cancellingDownload => 'Cancelling download...';
|
||||
|
||||
@override
|
||||
String errorLoadingStats(String error) {
|
||||
return 'Error loading stats: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String downloadFailed(String error) {
|
||||
return 'Download failed: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String cancelFailed(String error) {
|
||||
return 'Cancel failed: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String clearCacheFailed(String error) {
|
||||
return 'Clear cache failed: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String minZoomError(String error) {
|
||||
return 'Min zoom: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoomError(String error) {
|
||||
return 'Max zoom: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minZoomGreaterThanMax =>
|
||||
'Minimum zoom must be less than or equal to maximum zoom';
|
||||
|
||||
@override
|
||||
String get selectMapLayer => 'Select Map Layer';
|
||||
|
||||
@override
|
||||
String get mapOptions => 'Map Options';
|
||||
|
||||
@override
|
||||
String get showLegend => 'Show Legend';
|
||||
|
||||
@override
|
||||
String get displayMarkerTypeCounts => 'Display marker type counts';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Rotate Map with Heading';
|
||||
|
||||
@override
|
||||
String get mapFollowsDirection => 'Map follows your direction when moving';
|
||||
|
||||
@override
|
||||
String get resetMapRotation => 'Reset Rotation';
|
||||
|
||||
@override
|
||||
String get resetMapRotationTooltip => 'Reset map to north';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Show Map Debug Info';
|
||||
|
||||
@override
|
||||
String get displayZoomLevelBounds => 'Display zoom level and bounds';
|
||||
|
||||
@override
|
||||
String get fullscreenMode => 'Fullscreen Mode';
|
||||
|
||||
@override
|
||||
String get hideUiFullMapView => 'Hide all UI controls for full map view';
|
||||
|
||||
@override
|
||||
String get openStreetMap => 'OpenStreetMap';
|
||||
|
||||
@@ -1030,12 +678,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get googleTerrain => 'Google Terrain';
|
||||
|
||||
@override
|
||||
String get downloadVisibleArea => 'Download visible area';
|
||||
|
||||
@override
|
||||
String get initializingMap => 'Initializing map...';
|
||||
|
||||
@override
|
||||
String get dragToPosition => 'Drag to Position';
|
||||
|
||||
@@ -1051,50 +693,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get sarAlert => 'SAR ALERT';
|
||||
|
||||
@override
|
||||
String get messageSentToPublicChannel => 'Message sent to public channel';
|
||||
|
||||
@override
|
||||
String get pleaseSelectRoomToSendSar =>
|
||||
'Please select a room to send SAR marker';
|
||||
|
||||
@override
|
||||
String failedToSendSarMarker(String error) {
|
||||
return 'Failed to send SAR marker: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String sarMarkerSentTo(String roomName) {
|
||||
return 'SAR marker sent to $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedCannotSync => 'Not connected - cannot sync messages';
|
||||
|
||||
@override
|
||||
String syncedMessageCount(int count) {
|
||||
return 'Synced $count message(s)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get noNewMessages => 'No new messages';
|
||||
|
||||
@override
|
||||
String syncFailed(String error) {
|
||||
return 'Sync failed: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get failedToResendMessage => 'Failed to resend message';
|
||||
|
||||
@override
|
||||
String get retryingMessage => 'Retrying message...';
|
||||
|
||||
@override
|
||||
String retryFailed(String error) {
|
||||
return 'Retry failed: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get textCopiedToClipboard => 'Text copied to clipboard';
|
||||
|
||||
@@ -1105,9 +703,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get cannotReplyContactNotFound => 'Cannot reply: contact not found';
|
||||
|
||||
@override
|
||||
String get messageDeleted => 'Message deleted';
|
||||
|
||||
@override
|
||||
String get copyText => 'Copy text';
|
||||
|
||||
@@ -1143,12 +738,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get sarLocationShare => 'SAR Location';
|
||||
|
||||
@override
|
||||
String get locationShared => 'Location shared';
|
||||
|
||||
@override
|
||||
String get refreshedContacts => 'Refreshed contacts';
|
||||
|
||||
@override
|
||||
String get justNow => 'Just now';
|
||||
|
||||
@@ -1256,79 +845,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get you => 'You';
|
||||
|
||||
@override
|
||||
String get offlineVectorMaps => 'Offline Vector Maps';
|
||||
|
||||
@override
|
||||
String get offlineVectorMapsDescription =>
|
||||
'Import and manage offline vector map tiles (MBTiles format) for use without internet connection';
|
||||
|
||||
@override
|
||||
String get importMbtiles => 'Import MBTiles File';
|
||||
|
||||
@override
|
||||
String get importMbtilesNote =>
|
||||
'Supports MBTiles files with vector tiles (PBF/MVT format). Geofabrik extracts work great!';
|
||||
|
||||
@override
|
||||
String get noMbtilesFiles => 'No offline vector maps found';
|
||||
|
||||
@override
|
||||
String get mbtilesImportedSuccessfully =>
|
||||
'MBTiles file imported successfully';
|
||||
|
||||
@override
|
||||
String get failedToImportMbtiles => 'Failed to import MBTiles file';
|
||||
|
||||
@override
|
||||
String get deleteMbtilesConfirmTitle => 'Delete Offline Map';
|
||||
|
||||
@override
|
||||
String deleteMbtilesConfirmMessage(String name) {
|
||||
return 'Are you sure you want to delete \"$name\"? This will permanently remove the offline map.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mbtilesDeletedSuccessfully => 'Offline map deleted successfully';
|
||||
|
||||
@override
|
||||
String get failedToDeleteMbtiles => 'Failed to delete offline map';
|
||||
|
||||
@override
|
||||
String get importExportCachedTiles => 'Import/Export Cached Tiles';
|
||||
|
||||
@override
|
||||
String get importExportDescription =>
|
||||
'Backup, share, and restore downloaded map tiles between devices';
|
||||
|
||||
@override
|
||||
String get exportTilesToFile => 'Export Tiles to File';
|
||||
|
||||
@override
|
||||
String get importTilesFromFile => 'Import Tiles from File';
|
||||
|
||||
@override
|
||||
String get selectExportLocation => 'Select Export Location';
|
||||
|
||||
@override
|
||||
String get selectImportFile => 'Select Tile Archive';
|
||||
|
||||
@override
|
||||
String get exportingTiles => 'Exporting tiles...';
|
||||
|
||||
@override
|
||||
String get importingTiles => 'Importing tiles...';
|
||||
|
||||
@override
|
||||
String exportSuccess(int count) {
|
||||
return 'Exported $count tiles successfully';
|
||||
}
|
||||
|
||||
@override
|
||||
String importSuccess(int count) {
|
||||
return 'Imported $count stores successfully';
|
||||
}
|
||||
|
||||
@override
|
||||
String exportFailed(String error) {
|
||||
return 'Export failed: $error';
|
||||
@@ -1339,40 +855,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Import failed: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get exportNote =>
|
||||
'Creates a compressed archive (.fmtc) file that can be shared and imported on other devices.';
|
||||
|
||||
@override
|
||||
String get importNote =>
|
||||
'Imports map tiles from a previously exported archive file. Tiles will be merged with existing cache.';
|
||||
|
||||
@override
|
||||
String get noTilesToExport => 'No tiles available to export';
|
||||
|
||||
@override
|
||||
String archiveContainsStores(int count) {
|
||||
return 'Archive contains $count stores';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vectorTiles => 'Vector Tiles';
|
||||
|
||||
@override
|
||||
String get schema => 'Schema';
|
||||
|
||||
@override
|
||||
String get unknown => 'Unknown';
|
||||
|
||||
@override
|
||||
String get bounds => 'Bounds';
|
||||
|
||||
@override
|
||||
String get onlineLayers => 'Online Layers';
|
||||
|
||||
@override
|
||||
String get offlineLayers => 'Offline Layers';
|
||||
|
||||
@override
|
||||
String get locationTrail => 'Location Trail';
|
||||
|
||||
@@ -1411,47 +899,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get trailControls => 'Trail Controls';
|
||||
|
||||
@override
|
||||
String get exportTrailToGpx => 'Export Trail to GPX';
|
||||
|
||||
@override
|
||||
String get importTrailFromGpx => 'Import Trail from GPX';
|
||||
|
||||
@override
|
||||
String get trailExportedSuccessfully => 'Trail exported successfully!';
|
||||
|
||||
@override
|
||||
String get failedToExportTrail => 'Failed to export trail';
|
||||
|
||||
@override
|
||||
String failedToImportTrail(String error) {
|
||||
return 'Failed to import trail: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get importTrail => 'Import Trail';
|
||||
|
||||
@override
|
||||
String importTrailQuestion(int pointCount) {
|
||||
return 'Import trail with $pointCount points?\n\nYou can replace your current trail or view it alongside.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viewAlongside => 'View Alongside';
|
||||
|
||||
@override
|
||||
String get replaceCurrent => 'Replace Current';
|
||||
|
||||
@override
|
||||
String trailImported(int pointCount) {
|
||||
return 'Trail imported! ($pointCount points)';
|
||||
}
|
||||
|
||||
@override
|
||||
String trailReplaced(int pointCount) {
|
||||
return 'Trail replaced! ($pointCount points)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactTrails => 'Contact Trails';
|
||||
|
||||
@@ -1721,19 +1168,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get noRoomsFound => 'No rooms found';
|
||||
|
||||
@override
|
||||
String get noContactsOrRoomsAvailable => 'No contacts or rooms available';
|
||||
|
||||
@override
|
||||
String get noRecipientsAvailable => 'No recipients available';
|
||||
|
||||
@override
|
||||
String get noChannelsFound => 'No channels found';
|
||||
|
||||
@override
|
||||
String get messagesWillBeSentToPublicChannel =>
|
||||
'Messages will be sent to public channel';
|
||||
|
||||
@override
|
||||
String get newMessage => 'New message';
|
||||
|
||||
@@ -1908,9 +1348,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get rectangleDrawing => 'Rectangle Drawing';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard => 'Coordinates copied to clipboard';
|
||||
|
||||
@override
|
||||
String get manualCoordinates => 'Manual Coordinates';
|
||||
|
||||
@@ -1923,37 +1360,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get longitudeLabel => 'Longitude';
|
||||
|
||||
@override
|
||||
String get invalidLatitude => 'Invalid latitude (-90 to 90)';
|
||||
|
||||
@override
|
||||
String get invalidLongitude => 'Invalid longitude (-180 to 180)';
|
||||
|
||||
@override
|
||||
String get exampleCoordinates => 'Example: 46.0569, 14.5058';
|
||||
|
||||
@override
|
||||
String get drawingShared => 'Map Drawing';
|
||||
|
||||
@override
|
||||
String get drawingHidden => 'Drawing hidden from map';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count already shared',
|
||||
one: '1 already shared',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return 'Shared $count new drawing$plural';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareDrawing => 'Share Drawing';
|
||||
|
||||
@@ -2008,12 +1417,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get forestRoads => 'Forest Roads';
|
||||
|
||||
@override
|
||||
String get showCadastralParcels => 'Show Cadastral Parcels';
|
||||
|
||||
@override
|
||||
String get showForestRoads => 'Show Forest Roads';
|
||||
|
||||
@override
|
||||
String get wmsOverlays => 'WMS Overlays';
|
||||
|
||||
@@ -2117,10 +1520,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Failed to delete channel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get allChannelSlotsInUse =>
|
||||
'All channel slots are in use (maximum 39 custom channels)';
|
||||
|
||||
@override
|
||||
String get createChannel => 'Create Channel';
|
||||
|
||||
@@ -2160,21 +1559,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get wizardConnectingFeature3 =>
|
||||
'Works completely offline - no internet required';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeTitle => 'Simple Mode';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeDescription =>
|
||||
'New to mesh networking? Enable Simple Mode for a streamlined interface with essential features only.';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature1 =>
|
||||
'Beginner-friendly interface with core functions';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature2 =>
|
||||
'Switch to Advanced Mode anytime in Settings';
|
||||
|
||||
@override
|
||||
String get wizardChannelTitle => 'Channels';
|
||||
|
||||
|
||||
@@ -29,9 +29,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get disconnect => 'Desconectar';
|
||||
|
||||
@override
|
||||
String get scanningForDevices => 'Buscando dispositivos...';
|
||||
|
||||
@override
|
||||
String get noDevicesFound => 'No se encontraron dispositivos';
|
||||
|
||||
@@ -62,27 +59,14 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get failedToGetGpsLocation => 'Error al obtener la ubicación GPS';
|
||||
|
||||
@override
|
||||
String advertisedAtLocation(String latitude, String longitude) {
|
||||
return 'Anunciado en $latitude, $longitude';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToAdvertise(String error) {
|
||||
return 'Error al anunciar: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String reconnecting(int attempt, int max) {
|
||||
return 'Reconectando... ($attempt/$max)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelReconnection => 'Cancelar reconexión';
|
||||
|
||||
@override
|
||||
String get mapManagement => 'Gestión de mapas';
|
||||
|
||||
@override
|
||||
String get general => 'General';
|
||||
|
||||
@@ -129,13 +113,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get displayPacketActivity =>
|
||||
'Mostrar indicadores de actividad de paquetes en la barra superior';
|
||||
|
||||
@override
|
||||
String get simpleMode => 'Modo Simple';
|
||||
|
||||
@override
|
||||
String get simpleModeDescription =>
|
||||
'Ocultar información no esencial en mensajes y contactos';
|
||||
|
||||
@override
|
||||
String get disableMap => 'Desactivar mapa';
|
||||
|
||||
@@ -149,75 +126,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get chooseLanguage => 'Elegir idioma';
|
||||
|
||||
@override
|
||||
String get english => 'Inglés';
|
||||
|
||||
@override
|
||||
String get slovenian => 'Esloveno';
|
||||
|
||||
@override
|
||||
String get croatian => 'Croata';
|
||||
|
||||
@override
|
||||
String get german => 'Alemán';
|
||||
|
||||
@override
|
||||
String get spanish => 'Español';
|
||||
|
||||
@override
|
||||
String get french => 'Francés';
|
||||
|
||||
@override
|
||||
String get italian => 'Italiano';
|
||||
|
||||
@override
|
||||
String get locationBroadcasting => 'Difusión de ubicación';
|
||||
|
||||
@override
|
||||
String get autoLocationTracking => 'Seguimiento automático de ubicación';
|
||||
|
||||
@override
|
||||
String get automaticallyBroadcastPosition =>
|
||||
'Difundir automáticamente actualizaciones de posición';
|
||||
|
||||
@override
|
||||
String get configureTracking => 'Configurar seguimiento';
|
||||
|
||||
@override
|
||||
String get distanceAndTimeThresholds => 'Umbrales de distancia y tiempo';
|
||||
|
||||
@override
|
||||
String get locationTrackingConfiguration =>
|
||||
'Configuración de seguimiento de ubicación';
|
||||
|
||||
@override
|
||||
String get configureWhenLocationBroadcasts =>
|
||||
'Configurar cuándo se envían difusiones de ubicación a la red mesh';
|
||||
|
||||
@override
|
||||
String get minimumDistance => 'Distancia mínima';
|
||||
|
||||
@override
|
||||
String broadcastAfterMoving(String distance) {
|
||||
return 'Difundir solo después de moverse $distance metros';
|
||||
}
|
||||
|
||||
@override
|
||||
String get maximumDistance => 'Distancia máxima';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastAfterMoving(String distance) {
|
||||
return 'Siempre difundir después de moverse $distance metros';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minimumTimeInterval => 'Intervalo de tiempo mínimo';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastEvery(String duration) {
|
||||
return 'Siempre difundir cada $duration';
|
||||
}
|
||||
|
||||
@override
|
||||
String get save => 'Guardar';
|
||||
|
||||
@@ -253,12 +161,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get moreInfo => 'Más información';
|
||||
|
||||
@override
|
||||
String get learnMoreAbout => 'Más información sobre MeshCore SAR';
|
||||
|
||||
@override
|
||||
String get developer => 'Desarrollador';
|
||||
|
||||
@override
|
||||
String get packageName => 'Nombre del paquete';
|
||||
|
||||
@@ -334,9 +236,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get refresh => 'Actualizar';
|
||||
|
||||
@override
|
||||
String get sendDirectMessage => 'Enviar';
|
||||
|
||||
@override
|
||||
String get resetPath => 'Restablecer ruta (Re-enrutar)';
|
||||
|
||||
@@ -459,21 +358,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return 'Compartir $count dibujo$plural';
|
||||
}
|
||||
|
||||
@override
|
||||
String sentDrawingsToRoom(int count, String plural, String roomName) {
|
||||
return 'Enviados $count dibujo$plural de mapa a $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String sharedDrawingsToRoom(
|
||||
int success,
|
||||
int total,
|
||||
String plural,
|
||||
String roomName,
|
||||
) {
|
||||
return 'Compartidos $success/$total dibujo$plural con $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get showReceivedDrawings => 'Mostrar dibujos recibidos';
|
||||
|
||||
@@ -495,9 +379,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get clearAll => 'Borrar todo';
|
||||
|
||||
@override
|
||||
String get noLocalDrawings => 'No hay dibujos locales para compartir';
|
||||
|
||||
@override
|
||||
String get publicChannel => 'Canal público';
|
||||
|
||||
@@ -507,37 +388,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get storedPermanently => 'Almacenado permanentemente en la sala';
|
||||
|
||||
@override
|
||||
String drawingsSentToPublicChannel(int count, String plural) {
|
||||
return '$count dibujo$plural de mapa enviado al Canal Público';
|
||||
}
|
||||
|
||||
@override
|
||||
String drawingsSharedToPublicChannel(int success, int total) {
|
||||
return '$success/$total dibujos compartidos al Canal Público';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedToDevice => 'No conectado al dispositivo';
|
||||
|
||||
@override
|
||||
String get directMessage => 'Mensaje directo';
|
||||
|
||||
@override
|
||||
String directMessageSentTo(String contactName) {
|
||||
return 'Mensaje directo enviado a $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToSend(String error) {
|
||||
return 'Error al enviar: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String directMessageInfo(String contactName) {
|
||||
return 'Este mensaje se enviará directamente a $contactName. También aparecerá en el feed de mensajes principal.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get typeYourMessage => 'Escribe tu mensaje...';
|
||||
|
||||
@@ -567,9 +420,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get location => 'Ubicación';
|
||||
|
||||
@override
|
||||
String get myLocation => 'Mi ubicación';
|
||||
|
||||
@override
|
||||
String get fromMap => 'Desde el mapa';
|
||||
|
||||
@@ -631,9 +481,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Inundación';
|
||||
|
||||
@override
|
||||
String get admin => 'Admin';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Sesión iniciada';
|
||||
|
||||
@@ -643,29 +490,11 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get distance => 'Distancia';
|
||||
|
||||
@override
|
||||
String pingingDirect(String name) {
|
||||
return 'Haciendo ping a $name (directo vía ruta)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingingFlood(String name) {
|
||||
return 'Haciendo ping a $name (inundación - sin ruta)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String directPingTimeout(String name) {
|
||||
return 'Tiempo de espera de ping directo agotado - reintentando $name con inundación...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingSuccessful(String name, String fallback) {
|
||||
return 'Ping exitoso a $name$fallback';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viaFloodingFallback => ' (vía respaldo de inundación)';
|
||||
|
||||
@override
|
||||
String pingFailed(String name) {
|
||||
return 'Ping fallido a $name - no se recibió respuesta';
|
||||
@@ -676,16 +505,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return '¿Estás seguro de que quieres eliminar \"$name\"?\n\nEsto eliminará el contacto tanto de la aplicación como del dispositivo de radio compañero.';
|
||||
}
|
||||
|
||||
@override
|
||||
String removingContact(String name) {
|
||||
return 'Eliminando $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactRemoved(String name) {
|
||||
return 'Contacto \"$name\" eliminado';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToRemoveContact(String error) {
|
||||
return 'Error al eliminar contacto: $error';
|
||||
@@ -730,11 +549,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get telemetry => 'Telemetría';
|
||||
|
||||
@override
|
||||
String requestingTelemetry(String name) {
|
||||
return 'Solicitando telemetría de $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get voltage => 'Voltaje';
|
||||
|
||||
@@ -806,9 +620,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get showAll => 'Mostrar todo';
|
||||
|
||||
@override
|
||||
String get nearbyContacts => 'Contactos cercanos';
|
||||
|
||||
@override
|
||||
String get locationUnavailable => 'Ubicación no disponible';
|
||||
|
||||
@@ -849,176 +660,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get channels => 'Canales';
|
||||
|
||||
@override
|
||||
String get cacheStatistics => 'Estadísticas de caché';
|
||||
|
||||
@override
|
||||
String get totalTiles => 'Total de teselas';
|
||||
|
||||
@override
|
||||
String get cacheSize => 'Tamaño de caché';
|
||||
|
||||
@override
|
||||
String get storeName => 'Nombre del almacén';
|
||||
|
||||
@override
|
||||
String get noCacheStatistics => 'No hay estadísticas de caché disponibles';
|
||||
|
||||
@override
|
||||
String get downloadRegion => 'Descargar región';
|
||||
|
||||
@override
|
||||
String get mapLayer => 'Capa de mapa';
|
||||
|
||||
@override
|
||||
String get regionBounds => 'Límites de región';
|
||||
|
||||
@override
|
||||
String get north => 'Norte';
|
||||
|
||||
@override
|
||||
String get south => 'Sur';
|
||||
|
||||
@override
|
||||
String get east => 'Este';
|
||||
|
||||
@override
|
||||
String get west => 'Oeste';
|
||||
|
||||
@override
|
||||
String get zoomLevels => 'Niveles de zoom';
|
||||
|
||||
@override
|
||||
String minZoom(int zoom) {
|
||||
return 'Mín: $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoom(int zoom) {
|
||||
return 'Máx: $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String get downloadingDots => 'Descargando...';
|
||||
|
||||
@override
|
||||
String get cancelDownload => 'Cancelar descarga';
|
||||
|
||||
@override
|
||||
String get downloadRegionButton => 'Descargar región';
|
||||
|
||||
@override
|
||||
String get downloadNote =>
|
||||
'Nota: Las regiones grandes o niveles de zoom altos pueden tomar un tiempo y espacio de almacenamiento significativos.';
|
||||
|
||||
@override
|
||||
String get cacheManagement => 'Gestión de caché';
|
||||
|
||||
@override
|
||||
String get clearAllMaps => 'Borrar todos los mapas';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmTitle => 'Borrar todos los mapas';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmMessage =>
|
||||
'¿Estás seguro de que quieres eliminar todos los mapas descargados? Esta acción no se puede deshacer.';
|
||||
|
||||
@override
|
||||
String get mapDownloadCompleted => '¡Descarga de mapa completada!';
|
||||
|
||||
@override
|
||||
String get cacheClearedSuccessfully => '¡Caché borrada exitosamente!';
|
||||
|
||||
@override
|
||||
String get downloadCancelled => 'Descarga cancelada';
|
||||
|
||||
@override
|
||||
String get startingDownload => 'Iniciando descarga...';
|
||||
|
||||
@override
|
||||
String get downloadingMapTiles => 'Descargando teselas de mapa...';
|
||||
|
||||
@override
|
||||
String get downloadCompletedSuccessfully =>
|
||||
'¡Descarga completada exitosamente!';
|
||||
|
||||
@override
|
||||
String get cancellingDownload => 'Cancelando descarga...';
|
||||
|
||||
@override
|
||||
String errorLoadingStats(String error) {
|
||||
return 'Error al cargar estadísticas: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String downloadFailed(String error) {
|
||||
return 'Error en la descarga: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String cancelFailed(String error) {
|
||||
return 'Error al cancelar: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String clearCacheFailed(String error) {
|
||||
return 'Error al borrar caché: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String minZoomError(String error) {
|
||||
return 'Zoom mín: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoomError(String error) {
|
||||
return 'Zoom máx: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minZoomGreaterThanMax =>
|
||||
'El zoom mínimo debe ser menor o igual al zoom máximo';
|
||||
|
||||
@override
|
||||
String get selectMapLayer => 'Seleccionar capa de mapa';
|
||||
|
||||
@override
|
||||
String get mapOptions => 'Opciones de mapa';
|
||||
|
||||
@override
|
||||
String get showLegend => 'Mostrar leyenda';
|
||||
|
||||
@override
|
||||
String get displayMarkerTypeCounts =>
|
||||
'Mostrar recuentos de tipos de marcadores';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Rotar mapa con rumbo';
|
||||
|
||||
@override
|
||||
String get mapFollowsDirection =>
|
||||
'El mapa sigue tu dirección cuando te mueves';
|
||||
|
||||
@override
|
||||
String get resetMapRotation => 'Restablecer rotación';
|
||||
|
||||
@override
|
||||
String get resetMapRotationTooltip => 'Restablecer mapa al norte';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Mostrar información de depuración del mapa';
|
||||
|
||||
@override
|
||||
String get displayZoomLevelBounds => 'Mostrar nivel de zoom y límites';
|
||||
|
||||
@override
|
||||
String get fullscreenMode => 'Modo de pantalla completa';
|
||||
|
||||
@override
|
||||
String get hideUiFullMapView =>
|
||||
'Ocultar todos los controles de IU para vista de mapa completo';
|
||||
|
||||
@override
|
||||
String get openStreetMap => 'OpenStreetMap';
|
||||
|
||||
@@ -1037,12 +681,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get googleTerrain => 'Google Terreno';
|
||||
|
||||
@override
|
||||
String get downloadVisibleArea => 'Descargar área visible';
|
||||
|
||||
@override
|
||||
String get initializingMap => 'Inicializando mapa...';
|
||||
|
||||
@override
|
||||
String get dragToPosition => 'Arrastrar a posición';
|
||||
|
||||
@@ -1058,51 +696,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get sarAlert => 'ALERTA SAR';
|
||||
|
||||
@override
|
||||
String get messageSentToPublicChannel => 'Mensaje enviado al canal público';
|
||||
|
||||
@override
|
||||
String get pleaseSelectRoomToSendSar =>
|
||||
'Por favor, selecciona una sala para enviar marcador SAR';
|
||||
|
||||
@override
|
||||
String failedToSendSarMarker(String error) {
|
||||
return 'Error al enviar marcador SAR: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String sarMarkerSentTo(String roomName) {
|
||||
return 'Marcador SAR enviado a $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedCannotSync =>
|
||||
'No conectado - no se pueden sincronizar mensajes';
|
||||
|
||||
@override
|
||||
String syncedMessageCount(int count) {
|
||||
return 'Sincronizados $count mensaje(s)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get noNewMessages => 'No hay mensajes nuevos';
|
||||
|
||||
@override
|
||||
String syncFailed(String error) {
|
||||
return 'Error de sincronización: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get failedToResendMessage => 'Error al reenviar mensaje';
|
||||
|
||||
@override
|
||||
String get retryingMessage => 'Reintentando mensaje...';
|
||||
|
||||
@override
|
||||
String retryFailed(String error) {
|
||||
return 'Error de reintento: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get textCopiedToClipboard => 'Texto copiado al portapapeles';
|
||||
|
||||
@@ -1114,9 +707,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get cannotReplyContactNotFound =>
|
||||
'No se puede responder: contacto no encontrado';
|
||||
|
||||
@override
|
||||
String get messageDeleted => 'Mensaje eliminado';
|
||||
|
||||
@override
|
||||
String get copyText => 'Copiar texto';
|
||||
|
||||
@@ -1152,12 +742,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get sarLocationShare => 'Ubicación SAR';
|
||||
|
||||
@override
|
||||
String get locationShared => 'Ubicación compartida';
|
||||
|
||||
@override
|
||||
String get refreshedContacts => 'Contactos actualizados';
|
||||
|
||||
@override
|
||||
String get justNow => 'Justo ahora';
|
||||
|
||||
@@ -1265,125 +849,22 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get you => 'Tú';
|
||||
|
||||
@override
|
||||
String get offlineVectorMaps => 'Mapas vectoriales sin conexión';
|
||||
|
||||
@override
|
||||
String get offlineVectorMapsDescription =>
|
||||
'Importar y gestionar teselas de mapas vectoriales sin conexión (formato MBTiles) para usar sin conexión a internet';
|
||||
|
||||
@override
|
||||
String get importMbtiles => 'Importar archivo MBTiles';
|
||||
|
||||
@override
|
||||
String get importMbtilesNote =>
|
||||
'Compatible con archivos MBTiles con teselas vectoriales (formato PBF/MVT). ¡Los extractos de Geofabrik funcionan muy bien!';
|
||||
|
||||
@override
|
||||
String get noMbtilesFiles =>
|
||||
'No se encontraron mapas vectoriales sin conexión';
|
||||
|
||||
@override
|
||||
String get mbtilesImportedSuccessfully =>
|
||||
'Archivo MBTiles importado exitosamente';
|
||||
|
||||
@override
|
||||
String get failedToImportMbtiles => 'Error al importar archivo MBTiles';
|
||||
|
||||
@override
|
||||
String get deleteMbtilesConfirmTitle => 'Eliminar mapa sin conexión';
|
||||
|
||||
@override
|
||||
String deleteMbtilesConfirmMessage(String name) {
|
||||
return '¿Estás seguro de que quieres eliminar \"$name\"? Esto eliminará permanentemente el mapa sin conexión.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mbtilesDeletedSuccessfully =>
|
||||
'Mapa sin conexión eliminado exitosamente';
|
||||
|
||||
@override
|
||||
String get failedToDeleteMbtiles => 'Error al eliminar mapa sin conexión';
|
||||
|
||||
@override
|
||||
String get importExportCachedTiles => 'Importar/Exportar teselas en caché';
|
||||
|
||||
@override
|
||||
String get importExportDescription =>
|
||||
'Realice copias de seguridad, comparta y restaure teselas de mapas descargadas entre dispositivos';
|
||||
|
||||
@override
|
||||
String get exportTilesToFile => 'Exportar teselas a archivo';
|
||||
|
||||
@override
|
||||
String get importTilesFromFile => 'Importar teselas desde archivo';
|
||||
|
||||
@override
|
||||
String get selectExportLocation => 'Seleccionar ubicación de exportación';
|
||||
|
||||
@override
|
||||
String get selectImportFile => 'Seleccionar archivo de teselas';
|
||||
|
||||
@override
|
||||
String get exportingTiles => 'Exportando teselas...';
|
||||
|
||||
@override
|
||||
String get importingTiles => 'Importando teselas...';
|
||||
|
||||
@override
|
||||
String exportSuccess(int count) {
|
||||
return '$count teselas exportadas exitosamente';
|
||||
}
|
||||
|
||||
@override
|
||||
String importSuccess(int count) {
|
||||
return '$count almacenes importados exitosamente';
|
||||
}
|
||||
|
||||
@override
|
||||
String exportFailed(String error) {
|
||||
return 'Export failed: $error';
|
||||
return 'Error al exportar: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String importFailed(String error) {
|
||||
return 'Import failed: $error';
|
||||
return 'Error al importar: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get exportNote =>
|
||||
'Crea un archivo comprimido (.fmtc) que se puede compartir e importar en otros dispositivos.';
|
||||
|
||||
@override
|
||||
String get importNote =>
|
||||
'Importa teselas de mapa desde un archivo previamente exportado. Las teselas se fusionarán con la caché existente.';
|
||||
|
||||
@override
|
||||
String get noTilesToExport => 'No hay teselas para exportar';
|
||||
|
||||
@override
|
||||
String archiveContainsStores(int count) {
|
||||
return 'El archivo contiene $count almacenes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vectorTiles => 'Teselas vectoriales';
|
||||
|
||||
@override
|
||||
String get schema => 'Esquema';
|
||||
|
||||
@override
|
||||
String get unknown => 'Desconocido';
|
||||
|
||||
@override
|
||||
String get bounds => 'Límites';
|
||||
|
||||
@override
|
||||
String get onlineLayers => 'Capas en línea';
|
||||
|
||||
@override
|
||||
String get offlineLayers => 'Capas sin conexión';
|
||||
|
||||
@override
|
||||
String get locationTrail => 'Rastro de ubicación';
|
||||
|
||||
@@ -1422,47 +903,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get trailControls => 'Controles del rastro';
|
||||
|
||||
@override
|
||||
String get exportTrailToGpx => 'Exportar rastro a GPX';
|
||||
|
||||
@override
|
||||
String get importTrailFromGpx => 'Importar rastro desde GPX';
|
||||
|
||||
@override
|
||||
String get trailExportedSuccessfully => '¡Rastro exportado exitosamente!';
|
||||
|
||||
@override
|
||||
String get failedToExportTrail => 'Error al exportar el rastro';
|
||||
|
||||
@override
|
||||
String failedToImportTrail(String error) {
|
||||
return 'Error al importar el rastro: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get importTrail => 'Importar rastro';
|
||||
|
||||
@override
|
||||
String importTrailQuestion(int pointCount) {
|
||||
return '¿Importar rastro con $pointCount puntos?\n\nPuede reemplazar su rastro actual o verlo junto a él.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viewAlongside => 'Ver junto';
|
||||
|
||||
@override
|
||||
String get replaceCurrent => 'Reemplazar actual';
|
||||
|
||||
@override
|
||||
String trailImported(int pointCount) {
|
||||
return '¡Rastro importado! ($pointCount puntos)';
|
||||
}
|
||||
|
||||
@override
|
||||
String trailReplaced(int pointCount) {
|
||||
return '¡Rastro reemplazado! ($pointCount puntos)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactTrails => 'Rastros de contactos';
|
||||
|
||||
@@ -1583,7 +1023,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get templateName => 'Nombre de plantilla';
|
||||
|
||||
@override
|
||||
String get templateNameHint => 'e.g. Found Person';
|
||||
String get templateNameHint => 'p. ej., Persona encontrada';
|
||||
|
||||
@override
|
||||
String get templateEmoji => 'Emoji';
|
||||
@@ -1595,16 +1035,16 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get nameRequired => 'Se requiere nombre';
|
||||
|
||||
@override
|
||||
String get templateDescription => 'Description (Optional)';
|
||||
String get templateDescription => 'Descripcion (opcional)';
|
||||
|
||||
@override
|
||||
String get templateDescriptionHint => 'Add additional context...';
|
||||
String get templateDescriptionHint => 'Anade contexto adicional...';
|
||||
|
||||
@override
|
||||
String get templateColor => 'Color';
|
||||
|
||||
@override
|
||||
String get previewFormat => 'Preview (SAR Message Format)';
|
||||
String get previewFormat => 'Vista previa (formato de mensaje SAR)';
|
||||
|
||||
@override
|
||||
String get importFromClipboard => 'Importar';
|
||||
@@ -1614,26 +1054,26 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String deleteTemplateConfirmation(String name) {
|
||||
return 'Delete template \'$name\'?';
|
||||
return '¿Eliminar la plantilla \'$name\'?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get templateAdded => 'Template added';
|
||||
String get templateAdded => 'Plantilla anadida';
|
||||
|
||||
@override
|
||||
String get templateUpdated => 'Template updated';
|
||||
String get templateUpdated => 'Plantilla actualizada';
|
||||
|
||||
@override
|
||||
String get templateDeleted => 'Template deleted';
|
||||
String get templateDeleted => 'Plantilla eliminada';
|
||||
|
||||
@override
|
||||
String templatesImported(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Imported $count templates',
|
||||
one: 'Imported 1 template',
|
||||
zero: 'No templates imported',
|
||||
other: 'Se importaron $count plantillas',
|
||||
one: 'Se importo 1 plantilla',
|
||||
zero: 'No se importaron plantillas',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -1643,8 +1083,8 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Exported $count templates to clipboard',
|
||||
one: 'Exported 1 template to clipboard',
|
||||
other: 'Se exportaron $count plantillas al portapapeles',
|
||||
one: 'Se exporto 1 plantilla al portapapeles',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -1664,10 +1104,10 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
'Plantillas restablecidas a valores predeterminados';
|
||||
|
||||
@override
|
||||
String get noTemplates => 'No templates available';
|
||||
String get noTemplates => 'No hay plantillas disponibles';
|
||||
|
||||
@override
|
||||
String get tapAddToCreate => 'Tap + to create your first template';
|
||||
String get tapAddToCreate => 'Toca + para crear tu primera plantilla';
|
||||
|
||||
@override
|
||||
String get ok => 'OK';
|
||||
@@ -1735,20 +1175,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get noRoomsFound => 'No se encontraron salas';
|
||||
|
||||
@override
|
||||
String get noContactsOrRoomsAvailable =>
|
||||
'No hay contactos o salas disponibles';
|
||||
|
||||
@override
|
||||
String get noRecipientsAvailable => 'No hay destinatarios disponibles';
|
||||
|
||||
@override
|
||||
String get noChannelsFound => 'No se encontraron canales';
|
||||
|
||||
@override
|
||||
String get messagesWillBeSentToPublicChannel =>
|
||||
'Los mensajes se enviarán al canal público';
|
||||
|
||||
@override
|
||||
String get newMessage => 'Nuevo mensaje';
|
||||
|
||||
@@ -1936,10 +1368,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get rectangleDrawing => 'Rectángulo';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard =>
|
||||
'Coordenadas copiadas al portapapeles';
|
||||
|
||||
@override
|
||||
String get manualCoordinates => 'Coordenadas Manuales';
|
||||
|
||||
@@ -1952,31 +1380,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get longitudeLabel => 'Longitud';
|
||||
|
||||
@override
|
||||
String get invalidLatitude => 'Latitud inválida (-90 a 90)';
|
||||
|
||||
@override
|
||||
String get invalidLongitude => 'Longitud inválida (-180 a 180)';
|
||||
|
||||
@override
|
||||
String get exampleCoordinates => 'Ejemplo: 46.0569, 14.5058';
|
||||
|
||||
@override
|
||||
String get drawingShared => 'Dibujo del Mapa';
|
||||
|
||||
@override
|
||||
String get drawingHidden => 'Dibujo ocultado del mapa';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
return '$count ya compartido';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return '$count nuevo(s) dibujo(s) compartido(s)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareDrawing => 'Compartir Dibujo';
|
||||
|
||||
@@ -2033,12 +1439,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get forestRoads => 'Caminos Forestales';
|
||||
|
||||
@override
|
||||
String get showCadastralParcels => 'Mostrar parcelas catastrales';
|
||||
|
||||
@override
|
||||
String get showForestRoads => 'Mostrar caminos forestales';
|
||||
|
||||
@override
|
||||
String get wmsOverlays => 'Superposiciones WMS';
|
||||
|
||||
@@ -2143,10 +1543,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return 'Error al eliminar el canal: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get allChannelSlotsInUse =>
|
||||
'Todos los espacios de canales están en uso (máximo 39 canales personalizados)';
|
||||
|
||||
@override
|
||||
String get createChannel => 'Crear Canal';
|
||||
|
||||
@@ -2187,21 +1583,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get wizardConnectingFeature3 =>
|
||||
'Funciona completamente sin conexión - no se requiere internet';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeTitle => 'Modo Simple';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeDescription =>
|
||||
'¿Nuevo en redes en malla? Habilite el modo simple para una interfaz optimizada con solo funciones esenciales.';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature1 =>
|
||||
'Interfaz amigable para principiantes con funciones principales';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature2 =>
|
||||
'Cambie al modo avanzado en cualquier momento en Configuración';
|
||||
|
||||
@override
|
||||
String get wizardChannelTitle => 'Canales';
|
||||
|
||||
|
||||
@@ -29,9 +29,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get disconnect => 'Déconnecter';
|
||||
|
||||
@override
|
||||
String get scanningForDevices => 'Recherche d\'appareils...';
|
||||
|
||||
@override
|
||||
String get noDevicesFound => 'Aucun appareil trouvé';
|
||||
|
||||
@@ -63,27 +60,14 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get failedToGetGpsLocation =>
|
||||
'Échec de l\'obtention de la position GPS';
|
||||
|
||||
@override
|
||||
String advertisedAtLocation(String latitude, String longitude) {
|
||||
return 'Annoncé à $latitude, $longitude';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToAdvertise(String error) {
|
||||
return 'Échec de l\'annonce : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String reconnecting(int attempt, int max) {
|
||||
return 'Reconnexion... ($attempt/$max)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelReconnection => 'Annuler la reconnexion';
|
||||
|
||||
@override
|
||||
String get mapManagement => 'Gestion des cartes';
|
||||
|
||||
@override
|
||||
String get general => 'Général';
|
||||
|
||||
@@ -130,13 +114,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get displayPacketActivity =>
|
||||
'Afficher les indicateurs d\'activité des paquets dans la barre supérieure';
|
||||
|
||||
@override
|
||||
String get simpleMode => 'Mode Simple';
|
||||
|
||||
@override
|
||||
String get simpleModeDescription =>
|
||||
'Masquer les informations non essentielles dans les messages et les contacts';
|
||||
|
||||
@override
|
||||
String get disableMap => 'Désactiver la carte';
|
||||
|
||||
@@ -150,75 +127,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get chooseLanguage => 'Choisir la langue';
|
||||
|
||||
@override
|
||||
String get english => 'Anglais';
|
||||
|
||||
@override
|
||||
String get slovenian => 'Slovène';
|
||||
|
||||
@override
|
||||
String get croatian => 'Croate';
|
||||
|
||||
@override
|
||||
String get german => 'Allemand';
|
||||
|
||||
@override
|
||||
String get spanish => 'Espagnol';
|
||||
|
||||
@override
|
||||
String get french => 'Français';
|
||||
|
||||
@override
|
||||
String get italian => 'Italien';
|
||||
|
||||
@override
|
||||
String get locationBroadcasting => 'Diffusion de position';
|
||||
|
||||
@override
|
||||
String get autoLocationTracking => 'Suivi automatique de position';
|
||||
|
||||
@override
|
||||
String get automaticallyBroadcastPosition =>
|
||||
'Diffuser automatiquement les mises à jour de position';
|
||||
|
||||
@override
|
||||
String get configureTracking => 'Configurer le suivi';
|
||||
|
||||
@override
|
||||
String get distanceAndTimeThresholds => 'Seuils de distance et de temps';
|
||||
|
||||
@override
|
||||
String get locationTrackingConfiguration =>
|
||||
'Configuration du suivi de position';
|
||||
|
||||
@override
|
||||
String get configureWhenLocationBroadcasts =>
|
||||
'Configurer quand les diffusions de position sont envoyées au réseau maillé';
|
||||
|
||||
@override
|
||||
String get minimumDistance => 'Distance minimale';
|
||||
|
||||
@override
|
||||
String broadcastAfterMoving(String distance) {
|
||||
return 'Diffuser uniquement après un déplacement de $distance mètres';
|
||||
}
|
||||
|
||||
@override
|
||||
String get maximumDistance => 'Distance maximale';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastAfterMoving(String distance) {
|
||||
return 'Toujours diffuser après un déplacement de $distance mètres';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minimumTimeInterval => 'Intervalle de temps minimal';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastEvery(String duration) {
|
||||
return 'Toujours diffuser toutes les $duration';
|
||||
}
|
||||
|
||||
@override
|
||||
String get save => 'Enregistrer';
|
||||
|
||||
@@ -254,12 +162,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get moreInfo => 'Plus d\'infos';
|
||||
|
||||
@override
|
||||
String get learnMoreAbout => 'En savoir plus sur MeshCore SAR';
|
||||
|
||||
@override
|
||||
String get developer => 'Développeur';
|
||||
|
||||
@override
|
||||
String get packageName => 'Nom du package';
|
||||
|
||||
@@ -336,9 +238,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get refresh => 'Actualiser';
|
||||
|
||||
@override
|
||||
String get sendDirectMessage => 'Envoyer';
|
||||
|
||||
@override
|
||||
String get resetPath => 'Réinitialiser le chemin (Re-router)';
|
||||
|
||||
@@ -459,21 +358,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
return 'Partager $count dessin$plural';
|
||||
}
|
||||
|
||||
@override
|
||||
String sentDrawingsToRoom(int count, String plural, String roomName) {
|
||||
return '$count dessin$plural de carte envoyé$plural à $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String sharedDrawingsToRoom(
|
||||
int success,
|
||||
int total,
|
||||
String plural,
|
||||
String roomName,
|
||||
) {
|
||||
return '$success/$total dessin$plural partagé$plural avec $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get showReceivedDrawings => 'Afficher les dessins reçus';
|
||||
|
||||
@@ -495,9 +379,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get clearAll => 'Tout effacer';
|
||||
|
||||
@override
|
||||
String get noLocalDrawings => 'Aucun dessin local à partager';
|
||||
|
||||
@override
|
||||
String get publicChannel => 'Canal public';
|
||||
|
||||
@@ -508,37 +389,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get storedPermanently => 'Stocké de manière permanente dans le salon';
|
||||
|
||||
@override
|
||||
String drawingsSentToPublicChannel(int count, String plural) {
|
||||
return '$count dessin$plural de carte envoyé au Canal Public';
|
||||
}
|
||||
|
||||
@override
|
||||
String drawingsSharedToPublicChannel(int success, int total) {
|
||||
return '$success/$total dessins partagés sur le Canal Public';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedToDevice => 'Non connecté à l\'appareil';
|
||||
|
||||
@override
|
||||
String get directMessage => 'Message direct';
|
||||
|
||||
@override
|
||||
String directMessageSentTo(String contactName) {
|
||||
return 'Message direct envoyé à $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToSend(String error) {
|
||||
return 'Échec de l\'envoi : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String directMessageInfo(String contactName) {
|
||||
return 'Ce message sera envoyé directement à $contactName. Il apparaîtra également dans le fil de messages principal.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get typeYourMessage => 'Saisissez votre message...';
|
||||
|
||||
@@ -568,9 +421,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get location => 'Position';
|
||||
|
||||
@override
|
||||
String get myLocation => 'Ma position';
|
||||
|
||||
@override
|
||||
String get fromMap => 'Depuis la carte';
|
||||
|
||||
@@ -633,9 +483,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Inondation';
|
||||
|
||||
@override
|
||||
String get admin => 'Admin';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Connecté';
|
||||
|
||||
@@ -645,29 +492,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get distance => 'Distance';
|
||||
|
||||
@override
|
||||
String pingingDirect(String name) {
|
||||
return 'Ping de $name (direct via chemin)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingingFlood(String name) {
|
||||
return 'Ping de $name (inondation - pas de chemin)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String directPingTimeout(String name) {
|
||||
return 'Délai d\'attente du ping direct - nouvelle tentative de $name avec inondation...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingSuccessful(String name, String fallback) {
|
||||
return 'Ping réussi vers $name$fallback';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viaFloodingFallback => ' (via repli par inondation)';
|
||||
|
||||
@override
|
||||
String pingFailed(String name) {
|
||||
return 'Échec du ping vers $name - aucune réponse reçue';
|
||||
@@ -678,16 +507,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
return 'Êtes-vous sûr de vouloir supprimer \"$name\" ?\n\nCela supprimera le contact de l\'application et de l\'appareil radio compagnon.';
|
||||
}
|
||||
|
||||
@override
|
||||
String removingContact(String name) {
|
||||
return 'Suppression de $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactRemoved(String name) {
|
||||
return 'Contact \"$name\" supprimé';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToRemoveContact(String error) {
|
||||
return 'Échec de la suppression du contact : $error';
|
||||
@@ -732,11 +551,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get telemetry => 'Télémétrie';
|
||||
|
||||
@override
|
||||
String requestingTelemetry(String name) {
|
||||
return 'Demande de télémétrie à $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get voltage => 'Tension';
|
||||
|
||||
@@ -808,9 +622,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get showAll => 'Tout afficher';
|
||||
|
||||
@override
|
||||
String get nearbyContacts => 'Contacts à proximité';
|
||||
|
||||
@override
|
||||
String get locationUnavailable => 'Position non disponible';
|
||||
|
||||
@@ -851,177 +662,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get channels => 'Canaux';
|
||||
|
||||
@override
|
||||
String get cacheStatistics => 'Statistiques du cache';
|
||||
|
||||
@override
|
||||
String get totalTiles => 'Total de tuiles';
|
||||
|
||||
@override
|
||||
String get cacheSize => 'Taille du cache';
|
||||
|
||||
@override
|
||||
String get storeName => 'Nom du magasin';
|
||||
|
||||
@override
|
||||
String get noCacheStatistics => 'Aucune statistique de cache disponible';
|
||||
|
||||
@override
|
||||
String get downloadRegion => 'Télécharger une région';
|
||||
|
||||
@override
|
||||
String get mapLayer => 'Couche de carte';
|
||||
|
||||
@override
|
||||
String get regionBounds => 'Limites de la région';
|
||||
|
||||
@override
|
||||
String get north => 'Nord';
|
||||
|
||||
@override
|
||||
String get south => 'Sud';
|
||||
|
||||
@override
|
||||
String get east => 'Est';
|
||||
|
||||
@override
|
||||
String get west => 'Ouest';
|
||||
|
||||
@override
|
||||
String get zoomLevels => 'Niveaux de zoom';
|
||||
|
||||
@override
|
||||
String minZoom(int zoom) {
|
||||
return 'Min : $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoom(int zoom) {
|
||||
return 'Max : $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String get downloadingDots => 'Téléchargement...';
|
||||
|
||||
@override
|
||||
String get cancelDownload => 'Annuler le téléchargement';
|
||||
|
||||
@override
|
||||
String get downloadRegionButton => 'Télécharger la région';
|
||||
|
||||
@override
|
||||
String get downloadNote =>
|
||||
'Remarque : Les grandes régions ou les niveaux de zoom élevés peuvent nécessiter beaucoup de temps et d\'espace de stockage.';
|
||||
|
||||
@override
|
||||
String get cacheManagement => 'Gestion du cache';
|
||||
|
||||
@override
|
||||
String get clearAllMaps => 'Effacer toutes les cartes';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmTitle => 'Effacer toutes les cartes';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmMessage =>
|
||||
'Êtes-vous sûr de vouloir supprimer toutes les cartes téléchargées ? Cette action ne peut pas être annulée.';
|
||||
|
||||
@override
|
||||
String get mapDownloadCompleted => 'Téléchargement de la carte terminé !';
|
||||
|
||||
@override
|
||||
String get cacheClearedSuccessfully => 'Cache effacé avec succès !';
|
||||
|
||||
@override
|
||||
String get downloadCancelled => 'Téléchargement annulé';
|
||||
|
||||
@override
|
||||
String get startingDownload => 'Démarrage du téléchargement...';
|
||||
|
||||
@override
|
||||
String get downloadingMapTiles => 'Téléchargement des tuiles de carte...';
|
||||
|
||||
@override
|
||||
String get downloadCompletedSuccessfully =>
|
||||
'Téléchargement terminé avec succès !';
|
||||
|
||||
@override
|
||||
String get cancellingDownload => 'Annulation du téléchargement...';
|
||||
|
||||
@override
|
||||
String errorLoadingStats(String error) {
|
||||
return 'Erreur de chargement des statistiques : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String downloadFailed(String error) {
|
||||
return 'Échec du téléchargement : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String cancelFailed(String error) {
|
||||
return 'Échec de l\'annulation : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String clearCacheFailed(String error) {
|
||||
return 'Échec de l\'effacement du cache : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String minZoomError(String error) {
|
||||
return 'Zoom min : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoomError(String error) {
|
||||
return 'Zoom max : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minZoomGreaterThanMax =>
|
||||
'Le zoom minimal doit être inférieur ou égal au zoom maximal';
|
||||
|
||||
@override
|
||||
String get selectMapLayer => 'Sélectionner la couche de carte';
|
||||
|
||||
@override
|
||||
String get mapOptions => 'Options de carte';
|
||||
|
||||
@override
|
||||
String get showLegend => 'Afficher la légende';
|
||||
|
||||
@override
|
||||
String get displayMarkerTypeCounts =>
|
||||
'Afficher les décomptes des types de marqueurs';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Faire pivoter la carte avec le cap';
|
||||
|
||||
@override
|
||||
String get mapFollowsDirection =>
|
||||
'La carte suit votre direction lorsque vous vous déplacez';
|
||||
|
||||
@override
|
||||
String get resetMapRotation => 'Réinitialiser la rotation';
|
||||
|
||||
@override
|
||||
String get resetMapRotationTooltip => 'Réinitialiser la carte vers le nord';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Afficher les infos de débogage de la carte';
|
||||
|
||||
@override
|
||||
String get displayZoomLevelBounds =>
|
||||
'Afficher le niveau de zoom et les limites';
|
||||
|
||||
@override
|
||||
String get fullscreenMode => 'Mode plein écran';
|
||||
|
||||
@override
|
||||
String get hideUiFullMapView =>
|
||||
'Masquer tous les contrôles d\'interface pour une vue de carte complète';
|
||||
|
||||
@override
|
||||
String get openStreetMap => 'OpenStreetMap';
|
||||
|
||||
@@ -1040,12 +683,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get googleTerrain => 'Google Terrain';
|
||||
|
||||
@override
|
||||
String get downloadVisibleArea => 'Télécharger la zone visible';
|
||||
|
||||
@override
|
||||
String get initializingMap => 'Initialisation de la carte...';
|
||||
|
||||
@override
|
||||
String get dragToPosition => 'Faire glisser vers la position';
|
||||
|
||||
@@ -1061,51 +698,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get sarAlert => 'ALERTE SAR';
|
||||
|
||||
@override
|
||||
String get messageSentToPublicChannel => 'Message envoyé au canal public';
|
||||
|
||||
@override
|
||||
String get pleaseSelectRoomToSendSar =>
|
||||
'Veuillez sélectionner un salon pour envoyer le marqueur SAR';
|
||||
|
||||
@override
|
||||
String failedToSendSarMarker(String error) {
|
||||
return 'Échec de l\'envoi du marqueur SAR : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String sarMarkerSentTo(String roomName) {
|
||||
return 'Marqueur SAR envoyé à $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedCannotSync =>
|
||||
'Non connecté - impossible de synchroniser les messages';
|
||||
|
||||
@override
|
||||
String syncedMessageCount(int count) {
|
||||
return 'Synchronisé $count message(s)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get noNewMessages => 'Aucun nouveau message';
|
||||
|
||||
@override
|
||||
String syncFailed(String error) {
|
||||
return 'Échec de la synchronisation : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get failedToResendMessage => 'Échec du renvoi du message';
|
||||
|
||||
@override
|
||||
String get retryingMessage => 'Nouvelle tentative de message...';
|
||||
|
||||
@override
|
||||
String retryFailed(String error) {
|
||||
return 'Échec de la nouvelle tentative : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get textCopiedToClipboard => 'Texte copié dans le presse-papiers';
|
||||
|
||||
@@ -1117,9 +709,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get cannotReplyContactNotFound =>
|
||||
'Impossible de répondre : contact non trouvé';
|
||||
|
||||
@override
|
||||
String get messageDeleted => 'Message supprimé';
|
||||
|
||||
@override
|
||||
String get copyText => 'Copier le texte';
|
||||
|
||||
@@ -1155,12 +744,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get sarLocationShare => 'Position SAR';
|
||||
|
||||
@override
|
||||
String get locationShared => 'Position partagée';
|
||||
|
||||
@override
|
||||
String get refreshedContacts => 'Contacts actualisés';
|
||||
|
||||
@override
|
||||
String get justNow => 'À l\'instant';
|
||||
|
||||
@@ -1268,127 +851,22 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get you => 'Vous';
|
||||
|
||||
@override
|
||||
String get offlineVectorMaps => 'Cartes vectorielles hors ligne';
|
||||
|
||||
@override
|
||||
String get offlineVectorMapsDescription =>
|
||||
'Importer et gérer les tuiles de cartes vectorielles hors ligne (format MBTiles) pour une utilisation sans connexion Internet';
|
||||
|
||||
@override
|
||||
String get importMbtiles => 'Importer un fichier MBTiles';
|
||||
|
||||
@override
|
||||
String get importMbtilesNote =>
|
||||
'Prend en charge les fichiers MBTiles avec tuiles vectorielles (format PBF/MVT). Les extraits Geofabrik fonctionnent très bien !';
|
||||
|
||||
@override
|
||||
String get noMbtilesFiles => 'Aucune carte vectorielle hors ligne trouvée';
|
||||
|
||||
@override
|
||||
String get mbtilesImportedSuccessfully =>
|
||||
'Fichier MBTiles importé avec succès';
|
||||
|
||||
@override
|
||||
String get failedToImportMbtiles =>
|
||||
'Échec de l\'importation du fichier MBTiles';
|
||||
|
||||
@override
|
||||
String get deleteMbtilesConfirmTitle => 'Supprimer la carte hors ligne';
|
||||
|
||||
@override
|
||||
String deleteMbtilesConfirmMessage(String name) {
|
||||
return 'Êtes-vous sûr de vouloir supprimer \"$name\" ? Cela supprimera définitivement la carte hors ligne.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mbtilesDeletedSuccessfully =>
|
||||
'Carte hors ligne supprimée avec succès';
|
||||
|
||||
@override
|
||||
String get failedToDeleteMbtiles =>
|
||||
'Échec de la suppression de la carte hors ligne';
|
||||
|
||||
@override
|
||||
String get importExportCachedTiles => 'Importer/Exporter les tuiles en cache';
|
||||
|
||||
@override
|
||||
String get importExportDescription =>
|
||||
'Sauvegarder, partager et restaurer les tuiles de carte téléchargées entre appareils';
|
||||
|
||||
@override
|
||||
String get exportTilesToFile => 'Exporter les tuiles vers fichier';
|
||||
|
||||
@override
|
||||
String get importTilesFromFile => 'Importer les tuiles depuis fichier';
|
||||
|
||||
@override
|
||||
String get selectExportLocation =>
|
||||
'Sélectionner l\'emplacement d\'exportation';
|
||||
|
||||
@override
|
||||
String get selectImportFile => 'Sélectionner l\'archive de tuiles';
|
||||
|
||||
@override
|
||||
String get exportingTiles => 'Exportation des tuiles...';
|
||||
|
||||
@override
|
||||
String get importingTiles => 'Importation des tuiles...';
|
||||
|
||||
@override
|
||||
String exportSuccess(int count) {
|
||||
return '$count tuiles exportées avec succès';
|
||||
}
|
||||
|
||||
@override
|
||||
String importSuccess(int count) {
|
||||
return '$count magasins importés avec succès';
|
||||
}
|
||||
|
||||
@override
|
||||
String exportFailed(String error) {
|
||||
return 'Export failed: $error';
|
||||
return 'Echec de l\'exportation : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String importFailed(String error) {
|
||||
return 'Import failed: $error';
|
||||
return 'Echec de l\'importation : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get exportNote =>
|
||||
'Crée un fichier d\'archive compressé (.fmtc) qui peut être partagé et importé sur d\'autres appareils.';
|
||||
|
||||
@override
|
||||
String get importNote =>
|
||||
'Importe les tuiles de carte depuis un fichier d\'archive précédemment exporté. Les tuiles seront fusionnées avec le cache existant.';
|
||||
|
||||
@override
|
||||
String get noTilesToExport => 'Aucune tuile à exporter';
|
||||
|
||||
@override
|
||||
String archiveContainsStores(int count) {
|
||||
return 'L\'archive contient $count magasins';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vectorTiles => 'Tuiles vectorielles';
|
||||
|
||||
@override
|
||||
String get schema => 'Schéma';
|
||||
|
||||
@override
|
||||
String get unknown => 'Inconnu';
|
||||
|
||||
@override
|
||||
String get bounds => 'Limites';
|
||||
|
||||
@override
|
||||
String get onlineLayers => 'Couches en ligne';
|
||||
|
||||
@override
|
||||
String get offlineLayers => 'Couches hors ligne';
|
||||
|
||||
@override
|
||||
String get locationTrail => 'Trace de déplacement';
|
||||
|
||||
@@ -1428,47 +906,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get trailControls => 'Contrôles de la trace';
|
||||
|
||||
@override
|
||||
String get exportTrailToGpx => 'Exporter la trace vers GPX';
|
||||
|
||||
@override
|
||||
String get importTrailFromGpx => 'Importer la trace depuis GPX';
|
||||
|
||||
@override
|
||||
String get trailExportedSuccessfully => 'Trace exportée avec succès!';
|
||||
|
||||
@override
|
||||
String get failedToExportTrail => 'Échec de l\'exportation de la trace';
|
||||
|
||||
@override
|
||||
String failedToImportTrail(String error) {
|
||||
return 'Échec de l\'importation de la trace: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get importTrail => 'Importer la trace';
|
||||
|
||||
@override
|
||||
String importTrailQuestion(int pointCount) {
|
||||
return 'Importer la trace avec $pointCount points?\n\nVous pouvez remplacer votre trace actuelle ou l\'afficher à côté.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viewAlongside => 'Afficher à côté';
|
||||
|
||||
@override
|
||||
String get replaceCurrent => 'Remplacer l\'actuel';
|
||||
|
||||
@override
|
||||
String trailImported(int pointCount) {
|
||||
return 'Trace importée! ($pointCount points)';
|
||||
}
|
||||
|
||||
@override
|
||||
String trailReplaced(int pointCount) {
|
||||
return 'Trace remplacée! ($pointCount points)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactTrails => 'Traces des contacts';
|
||||
|
||||
@@ -1589,7 +1026,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get templateName => 'Nom du modèle';
|
||||
|
||||
@override
|
||||
String get templateNameHint => 'e.g. Found Person';
|
||||
String get templateNameHint => 'p. ex. Personne trouvee';
|
||||
|
||||
@override
|
||||
String get templateEmoji => 'Emoji';
|
||||
@@ -1601,16 +1038,16 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get nameRequired => 'Nom est requis';
|
||||
|
||||
@override
|
||||
String get templateDescription => 'Description (Optional)';
|
||||
String get templateDescription => 'Description (facultative)';
|
||||
|
||||
@override
|
||||
String get templateDescriptionHint => 'Add additional context...';
|
||||
String get templateDescriptionHint => 'Ajoutez un contexte supplementaire...';
|
||||
|
||||
@override
|
||||
String get templateColor => 'Color';
|
||||
String get templateColor => 'Couleur';
|
||||
|
||||
@override
|
||||
String get previewFormat => 'Preview (SAR Message Format)';
|
||||
String get previewFormat => 'Apercu (format de message SAR)';
|
||||
|
||||
@override
|
||||
String get importFromClipboard => 'Importer';
|
||||
@@ -1620,26 +1057,26 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String deleteTemplateConfirmation(String name) {
|
||||
return 'Delete template \'$name\'?';
|
||||
return 'Supprimer le modele \'$name\' ?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get templateAdded => 'Template added';
|
||||
String get templateAdded => 'Modele ajoute';
|
||||
|
||||
@override
|
||||
String get templateUpdated => 'Template updated';
|
||||
String get templateUpdated => 'Modele mis a jour';
|
||||
|
||||
@override
|
||||
String get templateDeleted => 'Template deleted';
|
||||
String get templateDeleted => 'Modele supprime';
|
||||
|
||||
@override
|
||||
String templatesImported(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Imported $count templates',
|
||||
one: 'Imported 1 template',
|
||||
zero: 'No templates imported',
|
||||
other: '$count modeles importes',
|
||||
one: '1 modele importe',
|
||||
zero: 'Aucun modele importe',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -1649,8 +1086,8 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Exported $count templates to clipboard',
|
||||
one: 'Exported 1 template to clipboard',
|
||||
other: '$count modeles exportes vers le presse-papiers',
|
||||
one: '1 modele exporte vers le presse-papiers',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -1669,10 +1106,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get resetComplete => 'Modèles réinitialisés aux valeurs par défaut';
|
||||
|
||||
@override
|
||||
String get noTemplates => 'No templates available';
|
||||
String get noTemplates => 'Aucun modele disponible';
|
||||
|
||||
@override
|
||||
String get tapAddToCreate => 'Tap + to create your first template';
|
||||
String get tapAddToCreate => 'Appuyez sur + pour creer votre premier modele';
|
||||
|
||||
@override
|
||||
String get ok => 'OK';
|
||||
@@ -1741,19 +1178,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get noRoomsFound => 'Aucune salle trouvée';
|
||||
|
||||
@override
|
||||
String get noContactsOrRoomsAvailable => 'Aucun contact ou salle disponible';
|
||||
|
||||
@override
|
||||
String get noRecipientsAvailable => 'Aucun destinataire disponible';
|
||||
|
||||
@override
|
||||
String get noChannelsFound => 'Aucun canal trouvé';
|
||||
|
||||
@override
|
||||
String get messagesWillBeSentToPublicChannel =>
|
||||
'Les messages seront envoyés au canal public';
|
||||
|
||||
@override
|
||||
String get newMessage => 'Nouveau message';
|
||||
|
||||
@@ -1940,10 +1370,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get rectangleDrawing => 'Rectangle';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard =>
|
||||
'Coordonnées copiées dans le presse-papiers';
|
||||
|
||||
@override
|
||||
String get manualCoordinates => 'Coordonnées Manuelles';
|
||||
|
||||
@@ -1956,31 +1382,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get longitudeLabel => 'Longitude';
|
||||
|
||||
@override
|
||||
String get invalidLatitude => 'Latitude invalide (-90 à 90)';
|
||||
|
||||
@override
|
||||
String get invalidLongitude => 'Longitude invalide (-180 à 180)';
|
||||
|
||||
@override
|
||||
String get exampleCoordinates => 'Exemple: 46.0569, 14.5058';
|
||||
|
||||
@override
|
||||
String get drawingShared => 'Dessin de Carte';
|
||||
|
||||
@override
|
||||
String get drawingHidden => 'Dessin masqué de la carte';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
return '$count déjà partagé';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return '$count nouveau(x) dessin(s) partagé(s)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareDrawing => 'Partager le Dessin';
|
||||
|
||||
@@ -2037,12 +1441,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get forestRoads => 'Chemins Forestiers';
|
||||
|
||||
@override
|
||||
String get showCadastralParcels => 'Afficher les parcelles cadastrales';
|
||||
|
||||
@override
|
||||
String get showForestRoads => 'Afficher les chemins forestiers';
|
||||
|
||||
@override
|
||||
String get wmsOverlays => 'Superpositions WMS';
|
||||
|
||||
@@ -2148,10 +1546,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
return 'Échec de la suppression du canal : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get allChannelSlotsInUse =>
|
||||
'Tous les emplacements de canaux sont utilisés (maximum 39 canaux personnalisés)';
|
||||
|
||||
@override
|
||||
String get createChannel => 'Créer un Canal';
|
||||
|
||||
@@ -2193,21 +1587,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get wizardConnectingFeature3 =>
|
||||
'Fonctionne entièrement hors ligne - aucun internet requis';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeTitle => 'Mode Simple';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeDescription =>
|
||||
'Nouveau dans les réseaux maillés ? Activez le mode simple pour une interface simplifiée avec seulement les fonctions essentielles.';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature1 =>
|
||||
'Interface conviviale pour débutants avec les fonctions principales';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature2 =>
|
||||
'Passer en mode avancé à tout moment dans les paramètres';
|
||||
|
||||
@override
|
||||
String get wizardChannelTitle => 'Canaux';
|
||||
|
||||
|
||||
@@ -29,9 +29,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get disconnect => 'Prekini';
|
||||
|
||||
@override
|
||||
String get scanningForDevices => 'Skeniranje uređaja...';
|
||||
|
||||
@override
|
||||
String get noDevicesFound => 'Nisu pronađeni uređaji';
|
||||
|
||||
@@ -62,27 +59,14 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get failedToGetGpsLocation => 'Neuspjelo dobivanje GPS lokacije';
|
||||
|
||||
@override
|
||||
String advertisedAtLocation(String latitude, String longitude) {
|
||||
return 'Objavljeno na $latitude, $longitude';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToAdvertise(String error) {
|
||||
return 'Neuspjela objava: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String reconnecting(int attempt, int max) {
|
||||
return 'Ponovno povezivanje... ($attempt/$max)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelReconnection => 'Otkaži ponovno povezivanje';
|
||||
|
||||
@override
|
||||
String get mapManagement => 'Upravljanje kartom';
|
||||
|
||||
@override
|
||||
String get general => 'Općenito';
|
||||
|
||||
@@ -129,13 +113,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
String get displayPacketActivity =>
|
||||
'Prikaži indikatore aktivnosti paketa u gornjoj traci';
|
||||
|
||||
@override
|
||||
String get simpleMode => 'Jednostavni način';
|
||||
|
||||
@override
|
||||
String get simpleModeDescription =>
|
||||
'Sakrij nevažne informacije u porukama i kontaktima';
|
||||
|
||||
@override
|
||||
String get disableMap => 'Onemogući kartu';
|
||||
|
||||
@@ -148,74 +125,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get chooseLanguage => 'Odaberite jezik';
|
||||
|
||||
@override
|
||||
String get english => 'Engleski';
|
||||
|
||||
@override
|
||||
String get slovenian => 'Slovenski';
|
||||
|
||||
@override
|
||||
String get croatian => 'Hrvatski';
|
||||
|
||||
@override
|
||||
String get german => 'Njemački';
|
||||
|
||||
@override
|
||||
String get spanish => 'Španjolski';
|
||||
|
||||
@override
|
||||
String get french => 'Francuski';
|
||||
|
||||
@override
|
||||
String get italian => 'Talijanski';
|
||||
|
||||
@override
|
||||
String get locationBroadcasting => 'Emitiranje lokacije';
|
||||
|
||||
@override
|
||||
String get autoLocationTracking => 'Automatsko praćenje lokacije';
|
||||
|
||||
@override
|
||||
String get automaticallyBroadcastPosition =>
|
||||
'Automatski emitiraj ažuriranja pozicije';
|
||||
|
||||
@override
|
||||
String get configureTracking => 'Konfiguriraj praćenje';
|
||||
|
||||
@override
|
||||
String get distanceAndTimeThresholds => 'Pragovi udaljenosti i vremena';
|
||||
|
||||
@override
|
||||
String get locationTrackingConfiguration => 'Konfiguracija praćenja lokacije';
|
||||
|
||||
@override
|
||||
String get configureWhenLocationBroadcasts =>
|
||||
'Konfigurirajte kada se emitiranja lokacije šalju u mesh mrežu';
|
||||
|
||||
@override
|
||||
String get minimumDistance => 'Minimalna udaljenost';
|
||||
|
||||
@override
|
||||
String broadcastAfterMoving(String distance) {
|
||||
return 'Emitiraj tek nakon pomicanja $distance metara';
|
||||
}
|
||||
|
||||
@override
|
||||
String get maximumDistance => 'Maksimalna udaljenost';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastAfterMoving(String distance) {
|
||||
return 'Uvijek emitiraj nakon pomicanja $distance metara';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minimumTimeInterval => 'Minimalni vremenski interval';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastEvery(String duration) {
|
||||
return 'Uvijek emitiraj svakih $duration';
|
||||
}
|
||||
|
||||
@override
|
||||
String get save => 'Spremi';
|
||||
|
||||
@@ -251,12 +160,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get moreInfo => 'Više informacija';
|
||||
|
||||
@override
|
||||
String get learnMoreAbout => 'Saznajte više o MeshCore SAR-u';
|
||||
|
||||
@override
|
||||
String get developer => 'Programer';
|
||||
|
||||
@override
|
||||
String get packageName => 'Ime paketa';
|
||||
|
||||
@@ -332,9 +235,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get refresh => 'Osvježi';
|
||||
|
||||
@override
|
||||
String get sendDirectMessage => 'Pošalji';
|
||||
|
||||
@override
|
||||
String get resetPath => 'Resetiraj put (preusmjeri)';
|
||||
|
||||
@@ -453,21 +353,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
return 'Podijeli $count crtež$plural';
|
||||
}
|
||||
|
||||
@override
|
||||
String sentDrawingsToRoom(int count, String plural, String roomName) {
|
||||
return 'Poslano $count crtež$plural karte u $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String sharedDrawingsToRoom(
|
||||
int success,
|
||||
int total,
|
||||
String plural,
|
||||
String roomName,
|
||||
) {
|
||||
return 'Podijeljeno $success/$total crtež$plural u $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get showReceivedDrawings => 'Prikaži primljene crteže';
|
||||
|
||||
@@ -489,9 +374,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get clearAll => 'Očisti sve';
|
||||
|
||||
@override
|
||||
String get noLocalDrawings => 'Nema lokalnih crteža za dijeljenje';
|
||||
|
||||
@override
|
||||
String get publicChannel => 'Javni kanal';
|
||||
|
||||
@@ -501,37 +383,9 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get storedPermanently => 'Trajno pohranjeno u sobi';
|
||||
|
||||
@override
|
||||
String drawingsSentToPublicChannel(int count, String plural) {
|
||||
return 'Poslano $count crtež$plural na javni kanal';
|
||||
}
|
||||
|
||||
@override
|
||||
String drawingsSharedToPublicChannel(int success, int total) {
|
||||
return 'Podijeljeno $success/$total crteža na javni kanal';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedToDevice => 'Nije povezano s uređajem';
|
||||
|
||||
@override
|
||||
String get directMessage => 'Izravna poruka';
|
||||
|
||||
@override
|
||||
String directMessageSentTo(String contactName) {
|
||||
return 'Izravna poruka poslana $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToSend(String error) {
|
||||
return 'Neuspjelo slanje: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String directMessageInfo(String contactName) {
|
||||
return 'Ova poruka će biti poslana izravno $contactName. Također će se prikazati u glavnom feedu poruka.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get typeYourMessage => 'Upišite svoju poruku...';
|
||||
|
||||
@@ -561,9 +415,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get location => 'Lokacija';
|
||||
|
||||
@override
|
||||
String get myLocation => 'Moja lokacija';
|
||||
|
||||
@override
|
||||
String get fromMap => 'S karte';
|
||||
|
||||
@@ -625,9 +476,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Preplavljanje';
|
||||
|
||||
@override
|
||||
String get admin => 'Administrator';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Prijavljen';
|
||||
|
||||
@@ -637,29 +485,11 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get distance => 'Udaljenost';
|
||||
|
||||
@override
|
||||
String pingingDirect(String name) {
|
||||
return 'Pingiranje $name (izravno putem puta)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingingFlood(String name) {
|
||||
return 'Pingiranje $name (preplavljanje - nema puta)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String directPingTimeout(String name) {
|
||||
return 'Istek izravnog pinga - ponovni pokušaj $name s preplavljanjem...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingSuccessful(String name, String fallback) {
|
||||
return 'Ping uspješan prema $name$fallback';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viaFloodingFallback => ' (putem rezervnog preplavljanja)';
|
||||
|
||||
@override
|
||||
String pingFailed(String name) {
|
||||
return 'Ping neuspješan prema $name - nije primljen odgovor';
|
||||
@@ -670,16 +500,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
return 'Jeste li sigurni da želite izbrisati \"$name\"?\n\nOvo će ukloniti kontakt iz aplikacije i pratećeg radio uređaja.';
|
||||
}
|
||||
|
||||
@override
|
||||
String removingContact(String name) {
|
||||
return 'Uklanjanje $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactRemoved(String name) {
|
||||
return 'Kontakt \"$name\" uklonjen';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToRemoveContact(String error) {
|
||||
return 'Neuspjelo uklanjanje kontakta: $error';
|
||||
@@ -724,11 +544,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get telemetry => 'Telemetrija';
|
||||
|
||||
@override
|
||||
String requestingTelemetry(String name) {
|
||||
return 'Zahtijevanje telemetrije od $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get voltage => 'Napon';
|
||||
|
||||
@@ -800,9 +615,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get showAll => 'Prikaži sve';
|
||||
|
||||
@override
|
||||
String get nearbyContacts => 'Obližnji kontakti';
|
||||
|
||||
@override
|
||||
String get locationUnavailable => 'Lokacija nije dostupna';
|
||||
|
||||
@@ -843,174 +655,9 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get channels => 'Kanali';
|
||||
|
||||
@override
|
||||
String get cacheStatistics => 'Statistika predmemorije';
|
||||
|
||||
@override
|
||||
String get totalTiles => 'Ukupno pločica';
|
||||
|
||||
@override
|
||||
String get cacheSize => 'Veličina predmemorije';
|
||||
|
||||
@override
|
||||
String get storeName => 'Naziv spremišta';
|
||||
|
||||
@override
|
||||
String get noCacheStatistics => 'Statistika predmemorije nije dostupna';
|
||||
|
||||
@override
|
||||
String get downloadRegion => 'Preuzmi regiju';
|
||||
|
||||
@override
|
||||
String get mapLayer => 'Sloj karte';
|
||||
|
||||
@override
|
||||
String get regionBounds => 'Granice regije';
|
||||
|
||||
@override
|
||||
String get north => 'Sjever';
|
||||
|
||||
@override
|
||||
String get south => 'Jug';
|
||||
|
||||
@override
|
||||
String get east => 'Istok';
|
||||
|
||||
@override
|
||||
String get west => 'Zapad';
|
||||
|
||||
@override
|
||||
String get zoomLevels => 'Razine zumiranja';
|
||||
|
||||
@override
|
||||
String minZoom(int zoom) {
|
||||
return 'Min: $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoom(int zoom) {
|
||||
return 'Maks: $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String get downloadingDots => 'Preuzimanje...';
|
||||
|
||||
@override
|
||||
String get cancelDownload => 'Otkaži preuzimanje';
|
||||
|
||||
@override
|
||||
String get downloadRegionButton => 'Preuzmi regiju';
|
||||
|
||||
@override
|
||||
String get downloadNote =>
|
||||
'Napomena: Velike regije ili visoke razine zumiranja mogu zahtijevati značajno vrijeme i prostor za pohranu.';
|
||||
|
||||
@override
|
||||
String get cacheManagement => 'Upravljanje predmemorijom';
|
||||
|
||||
@override
|
||||
String get clearAllMaps => 'Očisti sve karte';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmTitle => 'Očisti sve karte';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmMessage =>
|
||||
'Jeste li sigurni da želite izbrisati sve preuzete karte? Ova radnja se ne može poništiti.';
|
||||
|
||||
@override
|
||||
String get mapDownloadCompleted => 'Preuzimanje karte završeno!';
|
||||
|
||||
@override
|
||||
String get cacheClearedSuccessfully => 'Predmemorija uspješno očišćena!';
|
||||
|
||||
@override
|
||||
String get downloadCancelled => 'Preuzimanje otkazano';
|
||||
|
||||
@override
|
||||
String get startingDownload => 'Pokretanje preuzimanja...';
|
||||
|
||||
@override
|
||||
String get downloadingMapTiles => 'Preuzimanje pločica karte...';
|
||||
|
||||
@override
|
||||
String get downloadCompletedSuccessfully => 'Preuzimanje uspješno završeno!';
|
||||
|
||||
@override
|
||||
String get cancellingDownload => 'Otkazivanje preuzimanja...';
|
||||
|
||||
@override
|
||||
String errorLoadingStats(String error) {
|
||||
return 'Greška pri učitavanju statistike: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String downloadFailed(String error) {
|
||||
return 'Preuzimanje nije uspjelo: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String cancelFailed(String error) {
|
||||
return 'Otkazivanje nije uspjelo: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String clearCacheFailed(String error) {
|
||||
return 'Čišćenje predmemorije nije uspjelo: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String minZoomError(String error) {
|
||||
return 'Min zumiranje: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoomError(String error) {
|
||||
return 'Maks zumiranje: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minZoomGreaterThanMax =>
|
||||
'Minimalno zumiranje mora biti manje ili jednako maksimalnom zumiranju';
|
||||
|
||||
@override
|
||||
String get selectMapLayer => 'Odaberite sloj karte';
|
||||
|
||||
@override
|
||||
String get mapOptions => 'Opcije karte';
|
||||
|
||||
@override
|
||||
String get showLegend => 'Prikaži legendu';
|
||||
|
||||
@override
|
||||
String get displayMarkerTypeCounts => 'Prikaži broj vrsta markera';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Rotiraj kartu sa smjerom';
|
||||
|
||||
@override
|
||||
String get mapFollowsDirection => 'Karta slijedi vaš smjer pri kretanju';
|
||||
|
||||
@override
|
||||
String get resetMapRotation => 'Resetiraj rotaciju';
|
||||
|
||||
@override
|
||||
String get resetMapRotationTooltip => 'Vrati kartu na sjever';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo =>
|
||||
'Prikaži informacije za otklanjanje pogrešaka karte';
|
||||
|
||||
@override
|
||||
String get displayZoomLevelBounds => 'Prikaži razinu zumiranja i granice';
|
||||
|
||||
@override
|
||||
String get fullscreenMode => 'Način cijelog zaslona';
|
||||
|
||||
@override
|
||||
String get hideUiFullMapView =>
|
||||
'Sakrij sve UI kontrole za prikaz cijele karte';
|
||||
|
||||
@override
|
||||
String get openStreetMap => 'OpenStreetMap';
|
||||
|
||||
@@ -1029,12 +676,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get googleTerrain => 'Google teren';
|
||||
|
||||
@override
|
||||
String get downloadVisibleArea => 'Preuzmi vidljivo područje';
|
||||
|
||||
@override
|
||||
String get initializingMap => 'Inicijalizacija karte...';
|
||||
|
||||
@override
|
||||
String get dragToPosition => 'Povuci na poziciju';
|
||||
|
||||
@@ -1050,51 +691,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get sarAlert => 'SAR UZBUNA';
|
||||
|
||||
@override
|
||||
String get messageSentToPublicChannel => 'Poruka poslana na javni kanal';
|
||||
|
||||
@override
|
||||
String get pleaseSelectRoomToSendSar =>
|
||||
'Molimo odaberite sobu za slanje SAR markera';
|
||||
|
||||
@override
|
||||
String failedToSendSarMarker(String error) {
|
||||
return 'Neuspjelo slanje SAR markera: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String sarMarkerSentTo(String roomName) {
|
||||
return 'SAR marker poslan u $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedCannotSync =>
|
||||
'Nije povezano - ne može se sinkronizirati poruke';
|
||||
|
||||
@override
|
||||
String syncedMessageCount(int count) {
|
||||
return 'Sinkronizirano $count poruka';
|
||||
}
|
||||
|
||||
@override
|
||||
String get noNewMessages => 'Nema novih poruka';
|
||||
|
||||
@override
|
||||
String syncFailed(String error) {
|
||||
return 'Sinkronizacija nije uspjela: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get failedToResendMessage => 'Neuspjelo ponovno slanje poruke';
|
||||
|
||||
@override
|
||||
String get retryingMessage => 'Ponovni pokušaj slanja poruke...';
|
||||
|
||||
@override
|
||||
String retryFailed(String error) {
|
||||
return 'Ponovni pokušaj nije uspio: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get textCopiedToClipboard => 'Tekst kopiran u međuspremnik';
|
||||
|
||||
@@ -1106,9 +702,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
String get cannotReplyContactNotFound =>
|
||||
'Ne mogu odgovoriti: kontakt nije pronađen';
|
||||
|
||||
@override
|
||||
String get messageDeleted => 'Poruka izbrisana';
|
||||
|
||||
@override
|
||||
String get copyText => 'Kopiraj tekst';
|
||||
|
||||
@@ -1144,12 +737,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get sarLocationShare => 'SAR Lokacija';
|
||||
|
||||
@override
|
||||
String get locationShared => 'Lokacija podijeljena';
|
||||
|
||||
@override
|
||||
String get refreshedContacts => 'Kontakti osvježeni';
|
||||
|
||||
@override
|
||||
String get justNow => 'Upravo sada';
|
||||
|
||||
@@ -1257,78 +844,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get you => 'Ti';
|
||||
|
||||
@override
|
||||
String get offlineVectorMaps => 'Offline vektorske karte';
|
||||
|
||||
@override
|
||||
String get offlineVectorMapsDescription =>
|
||||
'Uvezite i upravljajte offline vektorskim pločicama karata (MBTiles format) za upotrebu bez internetske veze';
|
||||
|
||||
@override
|
||||
String get importMbtiles => 'Uvezi MBTiles datoteku';
|
||||
|
||||
@override
|
||||
String get importMbtilesNote =>
|
||||
'Podržava MBTiles datoteke s vektorskim pločicama (PBF/MVT format). Geofabrik izvodi odlično rade!';
|
||||
|
||||
@override
|
||||
String get noMbtilesFiles => 'Nisu pronađene offline vektorske karte';
|
||||
|
||||
@override
|
||||
String get mbtilesImportedSuccessfully => 'MBTiles datoteka uspješno uvezena';
|
||||
|
||||
@override
|
||||
String get failedToImportMbtiles => 'Neuspjeli uvoz MBTiles datoteke';
|
||||
|
||||
@override
|
||||
String get deleteMbtilesConfirmTitle => 'Izbriši offline kartu';
|
||||
|
||||
@override
|
||||
String deleteMbtilesConfirmMessage(String name) {
|
||||
return 'Jeste li sigurni da želite izbrisati \"$name\"? Ovo će trajno ukloniti offline kartu.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mbtilesDeletedSuccessfully => 'Offline karta uspješno izbrisana';
|
||||
|
||||
@override
|
||||
String get failedToDeleteMbtiles => 'Neuspjelo brisanje offline karte';
|
||||
|
||||
@override
|
||||
String get importExportCachedTiles => 'Uvoz/Izvoz predmemoriranih pločica';
|
||||
|
||||
@override
|
||||
String get importExportDescription =>
|
||||
'Sigurnosno kopirajte, dijelite i vraćajte preuzete pločice karte između uređaja';
|
||||
|
||||
@override
|
||||
String get exportTilesToFile => 'Izvezi pločice u datoteku';
|
||||
|
||||
@override
|
||||
String get importTilesFromFile => 'Uvezi pločice iz datoteke';
|
||||
|
||||
@override
|
||||
String get selectExportLocation => 'Odaberite lokaciju izvoza';
|
||||
|
||||
@override
|
||||
String get selectImportFile => 'Odaberite arhivu pločica';
|
||||
|
||||
@override
|
||||
String get exportingTiles => 'Izvažanje pločica...';
|
||||
|
||||
@override
|
||||
String get importingTiles => 'Uvažanje pločica...';
|
||||
|
||||
@override
|
||||
String exportSuccess(int count) {
|
||||
return 'Uspješno izvezeno $count pločica';
|
||||
}
|
||||
|
||||
@override
|
||||
String importSuccess(int count) {
|
||||
return 'Uspješno uvezeno $count skladišta';
|
||||
}
|
||||
|
||||
@override
|
||||
String exportFailed(String error) {
|
||||
return 'Izvoz nije uspio: $error';
|
||||
@@ -1339,40 +854,12 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
return 'Uvoz nije uspio: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get exportNote =>
|
||||
'Stvara komprimiranu arhivsku datoteku (.fmtc) koju možete dijeliti i uvesti na drugim uređajima.';
|
||||
|
||||
@override
|
||||
String get importNote =>
|
||||
'Uvozi pločice karte iz prethodno izvezene arhivske datoteke. Pločice će biti spojene s postojećom predmemorijom.';
|
||||
|
||||
@override
|
||||
String get noTilesToExport => 'Nema pločica za izvoz';
|
||||
|
||||
@override
|
||||
String archiveContainsStores(int count) {
|
||||
return 'Arhiva sadrži $count skladišta';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vectorTiles => 'Vektorske pločice';
|
||||
|
||||
@override
|
||||
String get schema => 'Shema';
|
||||
|
||||
@override
|
||||
String get unknown => 'Nepoznato';
|
||||
|
||||
@override
|
||||
String get bounds => 'Granice';
|
||||
|
||||
@override
|
||||
String get onlineLayers => 'Mrežni slojevi';
|
||||
|
||||
@override
|
||||
String get offlineLayers => 'Offline slojevi';
|
||||
|
||||
@override
|
||||
String get locationTrail => 'Putanja lokacije';
|
||||
|
||||
@@ -1411,47 +898,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get trailControls => 'Upravljanje putanjom';
|
||||
|
||||
@override
|
||||
String get exportTrailToGpx => 'Izvezi putanju u GPX';
|
||||
|
||||
@override
|
||||
String get importTrailFromGpx => 'Uvezi putanju iz GPX';
|
||||
|
||||
@override
|
||||
String get trailExportedSuccessfully => 'Putanja uspješno izvezena!';
|
||||
|
||||
@override
|
||||
String get failedToExportTrail => 'Izvoz putanje nije uspio';
|
||||
|
||||
@override
|
||||
String failedToImportTrail(String error) {
|
||||
return 'Uvoz putanje nije uspio: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get importTrail => 'Uvezi putanju';
|
||||
|
||||
@override
|
||||
String importTrailQuestion(int pointCount) {
|
||||
return 'Uvezi putanju s $pointCount točaka?\n\nMožete zamijeniti trenutnu putanju ili je prikazati zajedno.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viewAlongside => 'Prikaži zajedno';
|
||||
|
||||
@override
|
||||
String get replaceCurrent => 'Zamijeni trenutnu';
|
||||
|
||||
@override
|
||||
String trailImported(int pointCount) {
|
||||
return 'Putanja uvezena! ($pointCount točaka)';
|
||||
}
|
||||
|
||||
@override
|
||||
String trailReplaced(int pointCount) {
|
||||
return 'Putanja zamijenjena! ($pointCount točaka)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactTrails => 'Putanje kontakata';
|
||||
|
||||
@@ -1492,7 +938,7 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
String get buildDate => 'Datum izgradnje';
|
||||
|
||||
@override
|
||||
String get firmware => 'Firmware';
|
||||
String get firmware => 'Firmver';
|
||||
|
||||
@override
|
||||
String get maxContacts => 'Maks. kontakata';
|
||||
@@ -1658,7 +1104,7 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
String get tapAddToCreate => 'Dodirnite + za izradu prvog predloška';
|
||||
|
||||
@override
|
||||
String get ok => 'OK';
|
||||
String get ok => 'U redu';
|
||||
|
||||
@override
|
||||
String get permissionsSection => 'Dozvole';
|
||||
@@ -1723,19 +1169,12 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get noRoomsFound => 'Nema soba';
|
||||
|
||||
@override
|
||||
String get noContactsOrRoomsAvailable => 'Nema dostupnih kontakata ili soba';
|
||||
|
||||
@override
|
||||
String get noRecipientsAvailable => 'Nema dostupnih primatelja';
|
||||
|
||||
@override
|
||||
String get noChannelsFound => 'Nije pronađen nijedan kanal';
|
||||
|
||||
@override
|
||||
String get messagesWillBeSentToPublicChannel =>
|
||||
'Poruke će biti poslane na javni kanal';
|
||||
|
||||
@override
|
||||
String get newMessage => 'Nova poruka';
|
||||
|
||||
@@ -1915,10 +1354,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get rectangleDrawing => 'Pravokutni crtež';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard =>
|
||||
'Koordinate kopirane u međuspremnik';
|
||||
|
||||
@override
|
||||
String get manualCoordinates => 'Ručne koordinate';
|
||||
|
||||
@@ -1931,37 +1366,9 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get longitudeLabel => 'Geografska dužina';
|
||||
|
||||
@override
|
||||
String get invalidLatitude => 'Nevažeća geografska širina (-90 do 90)';
|
||||
|
||||
@override
|
||||
String get invalidLongitude => 'Nevažeća geografska dužina (-180 do 180)';
|
||||
|
||||
@override
|
||||
String get exampleCoordinates => 'Primjer: 46.0569, 14.5058';
|
||||
|
||||
@override
|
||||
String get drawingShared => 'Crtež podijeljen';
|
||||
|
||||
@override
|
||||
String get drawingHidden => 'Crtež sakriven s karte';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count već podijeljeno',
|
||||
one: '1 već podijeljeno',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return 'Podijeljeno $count novi$plural crtež$plural';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareDrawing => 'Podijeli crtež';
|
||||
|
||||
@@ -2017,12 +1424,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
@override
|
||||
String get forestRoads => 'Šumske ceste';
|
||||
|
||||
@override
|
||||
String get showCadastralParcels => 'Prikaži katastarske čestice';
|
||||
|
||||
@override
|
||||
String get showForestRoads => 'Prikaži šumske ceste';
|
||||
|
||||
@override
|
||||
String get wmsOverlays => 'WMS prekrivanja';
|
||||
|
||||
@@ -2126,10 +1527,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
return 'Neuspješno brisanje kanala: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get allChannelSlotsInUse =>
|
||||
'Svi slotovi kanala su zauzeti (maksimalno 39 prilagođenih kanala)';
|
||||
|
||||
@override
|
||||
String get createChannel => 'Kreiraj kanal';
|
||||
|
||||
@@ -2170,21 +1567,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
String get wizardConnectingFeature3 =>
|
||||
'Radi potpuno izvan mreže — internet nije potreban';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeTitle => 'Jednostavan način';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeDescription =>
|
||||
'Prvi put koristite mesh mrežu? Uključite jednostavan način za pojednostavljeno sučelje s osnovnim funkcijama.';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature1 =>
|
||||
'Sučelje prilagođeno početnicima s osnovnim funkcijama';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature2 =>
|
||||
'U svakom trenutku prebacite na napredni način u Postavkama';
|
||||
|
||||
@override
|
||||
String get wizardChannelTitle => 'Kanali';
|
||||
|
||||
|
||||
@@ -29,9 +29,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get disconnect => 'Disconnetti';
|
||||
|
||||
@override
|
||||
String get scanningForDevices => 'Ricerca dispositivi in corso...';
|
||||
|
||||
@override
|
||||
String get noDevicesFound => 'Nessun dispositivo trovato';
|
||||
|
||||
@@ -62,27 +59,14 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get failedToGetGpsLocation => 'Impossibile ottenere la posizione GPS';
|
||||
|
||||
@override
|
||||
String advertisedAtLocation(String latitude, String longitude) {
|
||||
return 'Annunciato a $latitude, $longitude';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToAdvertise(String error) {
|
||||
return 'Annuncio fallito: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String reconnecting(int attempt, int max) {
|
||||
return 'Riconnessione in corso... ($attempt/$max)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelReconnection => 'Annulla riconnessione';
|
||||
|
||||
@override
|
||||
String get mapManagement => 'Gestione Mappa';
|
||||
|
||||
@override
|
||||
String get general => 'Generale';
|
||||
|
||||
@@ -129,13 +113,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get displayPacketActivity =>
|
||||
'Mostra indicatori di attività pacchetti nella barra superiore';
|
||||
|
||||
@override
|
||||
String get simpleMode => 'Modalità Semplice';
|
||||
|
||||
@override
|
||||
String get simpleModeDescription =>
|
||||
'Nascondi informazioni non essenziali nei messaggi e contatti';
|
||||
|
||||
@override
|
||||
String get disableMap => 'Disabilita mappa';
|
||||
|
||||
@@ -149,75 +126,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get chooseLanguage => 'Scegli Lingua';
|
||||
|
||||
@override
|
||||
String get english => 'Inglese';
|
||||
|
||||
@override
|
||||
String get slovenian => 'Sloveno';
|
||||
|
||||
@override
|
||||
String get croatian => 'Croato';
|
||||
|
||||
@override
|
||||
String get german => 'Tedesco';
|
||||
|
||||
@override
|
||||
String get spanish => 'Spagnolo';
|
||||
|
||||
@override
|
||||
String get french => 'Francese';
|
||||
|
||||
@override
|
||||
String get italian => 'Italiano';
|
||||
|
||||
@override
|
||||
String get locationBroadcasting => 'Trasmissione Posizione';
|
||||
|
||||
@override
|
||||
String get autoLocationTracking => 'Tracciamento Posizione Automatico';
|
||||
|
||||
@override
|
||||
String get automaticallyBroadcastPosition =>
|
||||
'Trasmetti automaticamente aggiornamenti di posizione';
|
||||
|
||||
@override
|
||||
String get configureTracking => 'Configura Tracciamento';
|
||||
|
||||
@override
|
||||
String get distanceAndTimeThresholds => 'Soglie di distanza e tempo';
|
||||
|
||||
@override
|
||||
String get locationTrackingConfiguration =>
|
||||
'Configurazione Tracciamento Posizione';
|
||||
|
||||
@override
|
||||
String get configureWhenLocationBroadcasts =>
|
||||
'Configura quando le trasmissioni di posizione vengono inviate alla rete mesh';
|
||||
|
||||
@override
|
||||
String get minimumDistance => 'Distanza Minima';
|
||||
|
||||
@override
|
||||
String broadcastAfterMoving(String distance) {
|
||||
return 'Trasmetti solo dopo essersi spostati di $distance metri';
|
||||
}
|
||||
|
||||
@override
|
||||
String get maximumDistance => 'Distanza Massima';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastAfterMoving(String distance) {
|
||||
return 'Trasmetti sempre dopo essersi spostati di $distance metri';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minimumTimeInterval => 'Intervallo Minimo di Tempo';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastEvery(String duration) {
|
||||
return 'Trasmetti sempre ogni $duration';
|
||||
}
|
||||
|
||||
@override
|
||||
String get save => 'Salva';
|
||||
|
||||
@@ -253,12 +161,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get moreInfo => 'Maggiori informazioni';
|
||||
|
||||
@override
|
||||
String get learnMoreAbout => 'Ulteriori informazioni su MeshCore SAR';
|
||||
|
||||
@override
|
||||
String get developer => 'Sviluppatore';
|
||||
|
||||
@override
|
||||
String get packageName => 'Nome Pacchetto';
|
||||
|
||||
@@ -335,9 +237,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get refresh => 'Aggiorna';
|
||||
|
||||
@override
|
||||
String get sendDirectMessage => 'Invia';
|
||||
|
||||
@override
|
||||
String get resetPath => 'Resetta Percorso (Ri-instrada)';
|
||||
|
||||
@@ -458,21 +357,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
return 'Condividi $count disegno$plural';
|
||||
}
|
||||
|
||||
@override
|
||||
String sentDrawingsToRoom(int count, String plural, String roomName) {
|
||||
return 'Inviati $count disegno$plural mappa a $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String sharedDrawingsToRoom(
|
||||
int success,
|
||||
int total,
|
||||
String plural,
|
||||
String roomName,
|
||||
) {
|
||||
return 'Condivisi $success/$total disegno$plural con $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get showReceivedDrawings => 'Mostra Disegni Ricevuti';
|
||||
|
||||
@@ -494,9 +378,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get clearAll => 'Cancella Tutto';
|
||||
|
||||
@override
|
||||
String get noLocalDrawings => 'Nessun disegno locale da condividere';
|
||||
|
||||
@override
|
||||
String get publicChannel => 'Canale Pubblico';
|
||||
|
||||
@@ -506,37 +387,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get storedPermanently => 'Archiviato permanentemente nella stanza';
|
||||
|
||||
@override
|
||||
String drawingsSentToPublicChannel(int count, String plural) {
|
||||
return '$count disegno$plural mappa inviato al Canale Pubblico';
|
||||
}
|
||||
|
||||
@override
|
||||
String drawingsSharedToPublicChannel(int success, int total) {
|
||||
return '$success/$total disegni condivisi sul Canale Pubblico';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedToDevice => 'Non connesso al dispositivo';
|
||||
|
||||
@override
|
||||
String get directMessage => 'Messaggio Diretto';
|
||||
|
||||
@override
|
||||
String directMessageSentTo(String contactName) {
|
||||
return 'Messaggio diretto inviato a $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToSend(String error) {
|
||||
return 'Invio fallito: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String directMessageInfo(String contactName) {
|
||||
return 'Questo messaggio verrà inviato direttamente a $contactName. Apparirà anche nel feed dei messaggi principali.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get typeYourMessage => 'Scrivi il tuo messaggio...';
|
||||
|
||||
@@ -566,9 +419,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get location => 'Posizione';
|
||||
|
||||
@override
|
||||
String get myLocation => 'La mia posizione';
|
||||
|
||||
@override
|
||||
String get fromMap => 'Dalla Mappa';
|
||||
|
||||
@@ -630,9 +480,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Flood';
|
||||
|
||||
@override
|
||||
String get admin => 'Admin';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Connesso';
|
||||
|
||||
@@ -642,29 +489,11 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get distance => 'Distanza';
|
||||
|
||||
@override
|
||||
String pingingDirect(String name) {
|
||||
return 'Ping $name (diretto via percorso)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingingFlood(String name) {
|
||||
return 'Ping $name (flooding - nessun percorso)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String directPingTimeout(String name) {
|
||||
return 'Timeout ping diretto - nuovo tentativo $name con flooding...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingSuccessful(String name, String fallback) {
|
||||
return 'Ping riuscito a $name$fallback';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viaFloodingFallback => ' (via fallback flooding)';
|
||||
|
||||
@override
|
||||
String pingFailed(String name) {
|
||||
return 'Ping fallito a $name - nessuna risposta ricevuta';
|
||||
@@ -675,16 +504,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
return 'Sei sicuro di voler eliminare \"$name\"?\n\nQuesto rimuoverà il contatto sia dall\'app che dal dispositivo radio companion.';
|
||||
}
|
||||
|
||||
@override
|
||||
String removingContact(String name) {
|
||||
return 'Rimozione di $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactRemoved(String name) {
|
||||
return 'Contatto \"$name\" rimosso';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToRemoveContact(String error) {
|
||||
return 'Impossibile rimuovere il contatto: $error';
|
||||
@@ -729,11 +548,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get telemetry => 'Telemetria';
|
||||
|
||||
@override
|
||||
String requestingTelemetry(String name) {
|
||||
return 'Richiesta telemetria da $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get voltage => 'Tensione';
|
||||
|
||||
@@ -805,9 +619,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get showAll => 'Mostra Tutto';
|
||||
|
||||
@override
|
||||
String get nearbyContacts => 'Contatti Vicini';
|
||||
|
||||
@override
|
||||
String get locationUnavailable => 'Posizione non disponibile';
|
||||
|
||||
@@ -848,176 +659,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get channels => 'Canali';
|
||||
|
||||
@override
|
||||
String get cacheStatistics => 'Statistiche Cache';
|
||||
|
||||
@override
|
||||
String get totalTiles => 'Tile Totali';
|
||||
|
||||
@override
|
||||
String get cacheSize => 'Dimensione Cache';
|
||||
|
||||
@override
|
||||
String get storeName => 'Nome Archivio';
|
||||
|
||||
@override
|
||||
String get noCacheStatistics => 'Nessuna statistica cache disponibile';
|
||||
|
||||
@override
|
||||
String get downloadRegion => 'Scarica Regione';
|
||||
|
||||
@override
|
||||
String get mapLayer => 'Livello Mappa';
|
||||
|
||||
@override
|
||||
String get regionBounds => 'Limiti Regione';
|
||||
|
||||
@override
|
||||
String get north => 'Nord';
|
||||
|
||||
@override
|
||||
String get south => 'Sud';
|
||||
|
||||
@override
|
||||
String get east => 'Est';
|
||||
|
||||
@override
|
||||
String get west => 'Ovest';
|
||||
|
||||
@override
|
||||
String get zoomLevels => 'Livelli di Zoom';
|
||||
|
||||
@override
|
||||
String minZoom(int zoom) {
|
||||
return 'Min: $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoom(int zoom) {
|
||||
return 'Max: $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String get downloadingDots => 'Scaricamento in corso...';
|
||||
|
||||
@override
|
||||
String get cancelDownload => 'Annulla Download';
|
||||
|
||||
@override
|
||||
String get downloadRegionButton => 'Scarica Regione';
|
||||
|
||||
@override
|
||||
String get downloadNote =>
|
||||
'Nota: Regioni grandi o livelli di zoom elevati possono richiedere tempo e spazio di archiviazione significativi.';
|
||||
|
||||
@override
|
||||
String get cacheManagement => 'Gestione Cache';
|
||||
|
||||
@override
|
||||
String get clearAllMaps => 'Cancella Tutte le Mappe';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmTitle => 'Cancella Tutte le Mappe';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmMessage =>
|
||||
'Sei sicuro di voler eliminare tutte le mappe scaricate? Questa azione non può essere annullata.';
|
||||
|
||||
@override
|
||||
String get mapDownloadCompleted => 'Download mappa completato!';
|
||||
|
||||
@override
|
||||
String get cacheClearedSuccessfully => 'Cache cancellata con successo!';
|
||||
|
||||
@override
|
||||
String get downloadCancelled => 'Download annullato';
|
||||
|
||||
@override
|
||||
String get startingDownload => 'Avvio download...';
|
||||
|
||||
@override
|
||||
String get downloadingMapTiles => 'Scaricamento tile mappa...';
|
||||
|
||||
@override
|
||||
String get downloadCompletedSuccessfully =>
|
||||
'Download completato con successo!';
|
||||
|
||||
@override
|
||||
String get cancellingDownload => 'Annullamento download...';
|
||||
|
||||
@override
|
||||
String errorLoadingStats(String error) {
|
||||
return 'Errore nel caricamento delle statistiche: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String downloadFailed(String error) {
|
||||
return 'Download fallito: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String cancelFailed(String error) {
|
||||
return 'Annullamento fallito: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String clearCacheFailed(String error) {
|
||||
return 'Cancellazione cache fallita: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String minZoomError(String error) {
|
||||
return 'Zoom minimo: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoomError(String error) {
|
||||
return 'Zoom massimo: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minZoomGreaterThanMax =>
|
||||
'Lo zoom minimo deve essere minore o uguale allo zoom massimo';
|
||||
|
||||
@override
|
||||
String get selectMapLayer => 'Seleziona Livello Mappa';
|
||||
|
||||
@override
|
||||
String get mapOptions => 'Opzioni Mappa';
|
||||
|
||||
@override
|
||||
String get showLegend => 'Mostra Legenda';
|
||||
|
||||
@override
|
||||
String get displayMarkerTypeCounts =>
|
||||
'Visualizza conteggio tipi di marcatori';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Ruota Mappa con Direzione';
|
||||
|
||||
@override
|
||||
String get mapFollowsDirection =>
|
||||
'La mappa segue la tua direzione quando ti muovi';
|
||||
|
||||
@override
|
||||
String get resetMapRotation => 'Ripristina Rotazione';
|
||||
|
||||
@override
|
||||
String get resetMapRotationTooltip => 'Ripristina mappa verso nord';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Mostra Info Debug Mappa';
|
||||
|
||||
@override
|
||||
String get displayZoomLevelBounds => 'Visualizza livello di zoom e limiti';
|
||||
|
||||
@override
|
||||
String get fullscreenMode => 'Modalità Schermo Intero';
|
||||
|
||||
@override
|
||||
String get hideUiFullMapView =>
|
||||
'Nascondi tutti i controlli UI per la visualizzazione completa della mappa';
|
||||
|
||||
@override
|
||||
String get openStreetMap => 'OpenStreetMap';
|
||||
|
||||
@@ -1036,12 +680,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get googleTerrain => 'Google Terreno';
|
||||
|
||||
@override
|
||||
String get downloadVisibleArea => 'Scarica area visibile';
|
||||
|
||||
@override
|
||||
String get initializingMap => 'Inizializzazione mappa...';
|
||||
|
||||
@override
|
||||
String get dragToPosition => 'Trascina per Posizionare';
|
||||
|
||||
@@ -1057,52 +695,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get sarAlert => 'ALLERTA SAR';
|
||||
|
||||
@override
|
||||
String get messageSentToPublicChannel =>
|
||||
'Messaggio inviato al canale pubblico';
|
||||
|
||||
@override
|
||||
String get pleaseSelectRoomToSendSar =>
|
||||
'Seleziona una stanza per inviare il marcatore SAR';
|
||||
|
||||
@override
|
||||
String failedToSendSarMarker(String error) {
|
||||
return 'Impossibile inviare il marcatore SAR: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String sarMarkerSentTo(String roomName) {
|
||||
return 'Marcatore SAR inviato a $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedCannotSync =>
|
||||
'Non connesso - impossibile sincronizzare i messaggi';
|
||||
|
||||
@override
|
||||
String syncedMessageCount(int count) {
|
||||
return 'Sincronizzati $count messaggio(i)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get noNewMessages => 'Nessun nuovo messaggio';
|
||||
|
||||
@override
|
||||
String syncFailed(String error) {
|
||||
return 'Sincronizzazione fallita: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get failedToResendMessage => 'Impossibile reinviare il messaggio';
|
||||
|
||||
@override
|
||||
String get retryingMessage => 'Nuovo tentativo messaggio...';
|
||||
|
||||
@override
|
||||
String retryFailed(String error) {
|
||||
return 'Nuovo tentativo fallito: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get textCopiedToClipboard => 'Testo copiato negli appunti';
|
||||
|
||||
@@ -1114,9 +706,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get cannotReplyContactNotFound =>
|
||||
'Impossibile rispondere: contatto non trovato';
|
||||
|
||||
@override
|
||||
String get messageDeleted => 'Messaggio eliminato';
|
||||
|
||||
@override
|
||||
String get copyText => 'Copia testo';
|
||||
|
||||
@@ -1152,12 +741,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get sarLocationShare => 'Posizione SAR';
|
||||
|
||||
@override
|
||||
String get locationShared => 'Posizione condivisa';
|
||||
|
||||
@override
|
||||
String get refreshedContacts => 'Contatti aggiornati';
|
||||
|
||||
@override
|
||||
String get justNow => 'Proprio ora';
|
||||
|
||||
@@ -1265,124 +848,22 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get you => 'Tu';
|
||||
|
||||
@override
|
||||
String get offlineVectorMaps => 'Mappe Vettoriali Offline';
|
||||
|
||||
@override
|
||||
String get offlineVectorMapsDescription =>
|
||||
'Importa e gestisci tile di mappe vettoriali offline (formato MBTiles) per l\'uso senza connessione internet';
|
||||
|
||||
@override
|
||||
String get importMbtiles => 'Importa File MBTiles';
|
||||
|
||||
@override
|
||||
String get importMbtilesNote =>
|
||||
'Supporta file MBTiles con tile vettoriali (formato PBF/MVT). Gli estratti Geofabrik funzionano benissimo!';
|
||||
|
||||
@override
|
||||
String get noMbtilesFiles => 'Nessuna mappa vettoriale offline trovata';
|
||||
|
||||
@override
|
||||
String get mbtilesImportedSuccessfully =>
|
||||
'File MBTiles importato con successo';
|
||||
|
||||
@override
|
||||
String get failedToImportMbtiles => 'Impossibile importare il file MBTiles';
|
||||
|
||||
@override
|
||||
String get deleteMbtilesConfirmTitle => 'Elimina Mappa Offline';
|
||||
|
||||
@override
|
||||
String deleteMbtilesConfirmMessage(String name) {
|
||||
return 'Sei sicuro di voler eliminare \"$name\"? Questo rimuoverà permanentemente la mappa offline.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mbtilesDeletedSuccessfully =>
|
||||
'Mappa offline eliminata con successo';
|
||||
|
||||
@override
|
||||
String get failedToDeleteMbtiles => 'Impossibile eliminare la mappa offline';
|
||||
|
||||
@override
|
||||
String get importExportCachedTiles => 'Importa/Esporta tile in cache';
|
||||
|
||||
@override
|
||||
String get importExportDescription =>
|
||||
'Esegui backup, condividi e ripristina tile mappa scaricati tra dispositivi';
|
||||
|
||||
@override
|
||||
String get exportTilesToFile => 'Esporta tile su file';
|
||||
|
||||
@override
|
||||
String get importTilesFromFile => 'Importa tile da file';
|
||||
|
||||
@override
|
||||
String get selectExportLocation => 'Seleziona posizione esportazione';
|
||||
|
||||
@override
|
||||
String get selectImportFile => 'Seleziona archivio tile';
|
||||
|
||||
@override
|
||||
String get exportingTiles => 'Esportazione tile...';
|
||||
|
||||
@override
|
||||
String get importingTiles => 'Importazione tile...';
|
||||
|
||||
@override
|
||||
String exportSuccess(int count) {
|
||||
return '$count tile esportati con successo';
|
||||
}
|
||||
|
||||
@override
|
||||
String importSuccess(int count) {
|
||||
return '$count archivi importati con successo';
|
||||
}
|
||||
|
||||
@override
|
||||
String exportFailed(String error) {
|
||||
return 'Export failed: $error';
|
||||
return 'Esportazione non riuscita: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String importFailed(String error) {
|
||||
return 'Import failed: $error';
|
||||
return 'Importazione non riuscita: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get exportNote =>
|
||||
'Crea un file archivio compresso (.fmtc) che può essere condiviso e importato su altri dispositivi.';
|
||||
|
||||
@override
|
||||
String get importNote =>
|
||||
'Importa tile mappa da un file archivio precedentemente esportato. I tile verranno uniti con la cache esistente.';
|
||||
|
||||
@override
|
||||
String get noTilesToExport => 'Nessun tile da esportare';
|
||||
|
||||
@override
|
||||
String archiveContainsStores(int count) {
|
||||
return 'L\'archivio contiene $count archivi';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vectorTiles => 'Tile Vettoriali';
|
||||
|
||||
@override
|
||||
String get schema => 'Schema';
|
||||
|
||||
@override
|
||||
String get unknown => 'Sconosciuto';
|
||||
|
||||
@override
|
||||
String get bounds => 'Limiti';
|
||||
|
||||
@override
|
||||
String get onlineLayers => 'Livelli Online';
|
||||
|
||||
@override
|
||||
String get offlineLayers => 'Livelli Offline';
|
||||
|
||||
@override
|
||||
String get locationTrail => 'Traccia Posizione';
|
||||
|
||||
@@ -1422,47 +903,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get trailControls => 'Controlli Traccia';
|
||||
|
||||
@override
|
||||
String get exportTrailToGpx => 'Esporta traccia in GPX';
|
||||
|
||||
@override
|
||||
String get importTrailFromGpx => 'Importa traccia da GPX';
|
||||
|
||||
@override
|
||||
String get trailExportedSuccessfully => 'Traccia esportata con successo!';
|
||||
|
||||
@override
|
||||
String get failedToExportTrail => 'Esportazione traccia fallita';
|
||||
|
||||
@override
|
||||
String failedToImportTrail(String error) {
|
||||
return 'Importazione traccia fallita: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get importTrail => 'Importa traccia';
|
||||
|
||||
@override
|
||||
String importTrailQuestion(int pointCount) {
|
||||
return 'Importare traccia con $pointCount punti?\n\nPuoi sostituire la tua traccia attuale o visualizzarla affiancata.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viewAlongside => 'Visualizza affiancata';
|
||||
|
||||
@override
|
||||
String get replaceCurrent => 'Sostituisci attuale';
|
||||
|
||||
@override
|
||||
String trailImported(int pointCount) {
|
||||
return 'Traccia importata! ($pointCount punti)';
|
||||
}
|
||||
|
||||
@override
|
||||
String trailReplaced(int pointCount) {
|
||||
return 'Traccia sostituita! ($pointCount punti)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactTrails => 'Tracce contatti';
|
||||
|
||||
@@ -1584,7 +1024,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get templateName => 'Nome modello';
|
||||
|
||||
@override
|
||||
String get templateNameHint => 'e.g. Found Person';
|
||||
String get templateNameHint => 'ad es. Persona trovata';
|
||||
|
||||
@override
|
||||
String get templateEmoji => 'Emoji';
|
||||
@@ -1596,16 +1036,16 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get nameRequired => 'Nome è obbligatorio';
|
||||
|
||||
@override
|
||||
String get templateDescription => 'Description (Optional)';
|
||||
String get templateDescription => 'Descrizione (facoltativa)';
|
||||
|
||||
@override
|
||||
String get templateDescriptionHint => 'Add additional context...';
|
||||
String get templateDescriptionHint => 'Aggiungi contesto aggiuntivo...';
|
||||
|
||||
@override
|
||||
String get templateColor => 'Color';
|
||||
String get templateColor => 'Colore';
|
||||
|
||||
@override
|
||||
String get previewFormat => 'Preview (SAR Message Format)';
|
||||
String get previewFormat => 'Anteprima (formato messaggio SAR)';
|
||||
|
||||
@override
|
||||
String get importFromClipboard => 'Importa';
|
||||
@@ -1615,26 +1055,26 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String deleteTemplateConfirmation(String name) {
|
||||
return 'Delete template \'$name\'?';
|
||||
return 'Eliminare il modello \'$name\'?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get templateAdded => 'Template added';
|
||||
String get templateAdded => 'Modello aggiunto';
|
||||
|
||||
@override
|
||||
String get templateUpdated => 'Template updated';
|
||||
String get templateUpdated => 'Modello aggiornato';
|
||||
|
||||
@override
|
||||
String get templateDeleted => 'Template deleted';
|
||||
String get templateDeleted => 'Modello eliminato';
|
||||
|
||||
@override
|
||||
String templatesImported(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Imported $count templates',
|
||||
one: 'Imported 1 template',
|
||||
zero: 'No templates imported',
|
||||
other: 'Importati $count modelli',
|
||||
one: 'Importato 1 modello',
|
||||
zero: 'Nessun modello importato',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -1644,8 +1084,8 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Exported $count templates to clipboard',
|
||||
one: 'Exported 1 template to clipboard',
|
||||
other: 'Esportati $count modelli negli appunti',
|
||||
one: 'Esportato 1 modello negli appunti',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -1664,10 +1104,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get resetComplete => 'Modelli ripristinati ai valori predefiniti';
|
||||
|
||||
@override
|
||||
String get noTemplates => 'No templates available';
|
||||
String get noTemplates => 'Nessun modello disponibile';
|
||||
|
||||
@override
|
||||
String get tapAddToCreate => 'Tap + to create your first template';
|
||||
String get tapAddToCreate => 'Tocca + per creare il tuo primo modello';
|
||||
|
||||
@override
|
||||
String get ok => 'OK';
|
||||
@@ -1734,20 +1174,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get noRoomsFound => 'Nessuna stanza trovata';
|
||||
|
||||
@override
|
||||
String get noContactsOrRoomsAvailable =>
|
||||
'Nessun contatto o stanza disponibile';
|
||||
|
||||
@override
|
||||
String get noRecipientsAvailable => 'Nessun destinatario disponibile';
|
||||
|
||||
@override
|
||||
String get noChannelsFound => 'Nessun canale trovato';
|
||||
|
||||
@override
|
||||
String get messagesWillBeSentToPublicChannel =>
|
||||
'I messaggi saranno inviati al canale pubblico';
|
||||
|
||||
@override
|
||||
String get newMessage => 'Nuovo messaggio';
|
||||
|
||||
@@ -1932,9 +1364,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get rectangleDrawing => 'Rettangolo';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard => 'Coordinate copiate negli appunti';
|
||||
|
||||
@override
|
||||
String get manualCoordinates => 'Coordinate Manuali';
|
||||
|
||||
@@ -1947,31 +1376,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get longitudeLabel => 'Longitudine';
|
||||
|
||||
@override
|
||||
String get invalidLatitude => 'Latitudine non valida (-90 a 90)';
|
||||
|
||||
@override
|
||||
String get invalidLongitude => 'Longitudine non valida (-180 a 180)';
|
||||
|
||||
@override
|
||||
String get exampleCoordinates => 'Esempio: 46.0569, 14.5058';
|
||||
|
||||
@override
|
||||
String get drawingShared => 'Disegno della Mappa';
|
||||
|
||||
@override
|
||||
String get drawingHidden => 'Disegno nascosto dalla mappa';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
return '$count già condiviso';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return '$count nuovo(i) disegno(i) condiviso(i)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareDrawing => 'Condividi Disegno';
|
||||
|
||||
@@ -2028,12 +1435,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get forestRoads => 'Strade Forestali';
|
||||
|
||||
@override
|
||||
String get showCadastralParcels => 'Mostra particelle catastali';
|
||||
|
||||
@override
|
||||
String get showForestRoads => 'Mostra strade forestali';
|
||||
|
||||
@override
|
||||
String get wmsOverlays => 'Sovrapposizioni WMS';
|
||||
|
||||
@@ -2138,10 +1539,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
return 'Eliminazione del canale fallita: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get allChannelSlotsInUse =>
|
||||
'Tutti gli slot dei canali sono in uso (massimo 39 canali personalizzati)';
|
||||
|
||||
@override
|
||||
String get createChannel => 'Crea Canale';
|
||||
|
||||
@@ -2183,21 +1580,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get wizardConnectingFeature3 =>
|
||||
'Funziona completamente offline - non è richiesta connessione internet';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeTitle => 'Modalità Semplice';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeDescription =>
|
||||
'Nuovo alle reti mesh? Abilita la modalità semplice per un\'interfaccia semplificata con solo le funzioni essenziali.';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature1 =>
|
||||
'Interfaccia intuitiva per principianti con funzioni principali';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature2 =>
|
||||
'Passa alla modalità avanzata in qualsiasi momento dalle Impostazioni';
|
||||
|
||||
@override
|
||||
String get wizardChannelTitle => 'Canali';
|
||||
|
||||
|
||||
1732
lib/l10n/app_localizations_pl.dart
Normal file
1732
lib/l10n/app_localizations_pl.dart
Normal file
File diff suppressed because it is too large
Load Diff
1739
lib/l10n/app_localizations_pt.dart
Normal file
1739
lib/l10n/app_localizations_pt.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -29,9 +29,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get disconnect => 'Отключить';
|
||||
|
||||
@override
|
||||
String get scanningForDevices => 'Поиск устройств...';
|
||||
|
||||
@override
|
||||
String get noDevicesFound => 'Устройства не найдены';
|
||||
|
||||
@@ -62,27 +59,14 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get failedToGetGpsLocation => 'Не удалось получить GPS-координаты';
|
||||
|
||||
@override
|
||||
String advertisedAtLocation(String latitude, String longitude) {
|
||||
return 'Транслируется в $latitude, $longitude';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToAdvertise(String error) {
|
||||
return 'Ошибка трансляции: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String reconnecting(int attempt, int max) {
|
||||
return 'Переподключение... ($attempt/$max)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelReconnection => 'Отменить переподключение';
|
||||
|
||||
@override
|
||||
String get mapManagement => 'Управление картой';
|
||||
|
||||
@override
|
||||
String get general => 'Основные';
|
||||
|
||||
@@ -129,13 +113,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get displayPacketActivity =>
|
||||
'Отображать активность пакетов в верхней панели';
|
||||
|
||||
@override
|
||||
String get simpleMode => 'Простой режим';
|
||||
|
||||
@override
|
||||
String get simpleModeDescription =>
|
||||
'Скрыть второстепенную информацию в сообщениях и контактах';
|
||||
|
||||
@override
|
||||
String get disableMap => 'Отключить карту';
|
||||
|
||||
@@ -149,75 +126,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get chooseLanguage => 'Выбрать язык';
|
||||
|
||||
@override
|
||||
String get english => 'Английский';
|
||||
|
||||
@override
|
||||
String get slovenian => 'Словенский';
|
||||
|
||||
@override
|
||||
String get croatian => 'Хорватский';
|
||||
|
||||
@override
|
||||
String get german => 'Немецкий';
|
||||
|
||||
@override
|
||||
String get spanish => 'Испанский';
|
||||
|
||||
@override
|
||||
String get french => 'Французский';
|
||||
|
||||
@override
|
||||
String get italian => 'Итальянский';
|
||||
|
||||
@override
|
||||
String get locationBroadcasting => 'Трансляция местоположения';
|
||||
|
||||
@override
|
||||
String get autoLocationTracking => 'Авто-отслеживание геолокации';
|
||||
|
||||
@override
|
||||
String get automaticallyBroadcastPosition =>
|
||||
'Автоматически транслировать обновления позиции';
|
||||
|
||||
@override
|
||||
String get configureTracking => 'Настроить отслеживание';
|
||||
|
||||
@override
|
||||
String get distanceAndTimeThresholds => 'Пороги расстояния и времени';
|
||||
|
||||
@override
|
||||
String get locationTrackingConfiguration =>
|
||||
'Настройка отслеживания геолокации';
|
||||
|
||||
@override
|
||||
String get configureWhenLocationBroadcasts =>
|
||||
'Настройте условия отправки обновлений геолокации в mesh-сеть';
|
||||
|
||||
@override
|
||||
String get minimumDistance => 'Минимальное расстояние';
|
||||
|
||||
@override
|
||||
String broadcastAfterMoving(String distance) {
|
||||
return 'Транслировать только после перемещения на $distance м';
|
||||
}
|
||||
|
||||
@override
|
||||
String get maximumDistance => 'Максимальное расстояние';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastAfterMoving(String distance) {
|
||||
return 'Всегда транслировать после перемещения на $distance м';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minimumTimeInterval => 'Минимальный интервал времени';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastEvery(String duration) {
|
||||
return 'Всегда транслировать каждые $duration';
|
||||
}
|
||||
|
||||
@override
|
||||
String get save => 'Сохранить';
|
||||
|
||||
@@ -253,12 +161,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get moreInfo => 'Подробнее';
|
||||
|
||||
@override
|
||||
String get learnMoreAbout => 'Узнать больше о MeshCore SAR';
|
||||
|
||||
@override
|
||||
String get developer => 'Разработчик';
|
||||
|
||||
@override
|
||||
String get packageName => 'Имя пакета';
|
||||
|
||||
@@ -334,9 +236,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get refresh => 'Обновить';
|
||||
|
||||
@override
|
||||
String get sendDirectMessage => 'Отправить';
|
||||
|
||||
@override
|
||||
String get resetPath => 'Сбросить маршрут (перепроложить)';
|
||||
|
||||
@@ -457,21 +356,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return 'Поделиться $count рисун$plural';
|
||||
}
|
||||
|
||||
@override
|
||||
String sentDrawingsToRoom(int count, String plural, String roomName) {
|
||||
return 'Отправлено $count рисун$plural в $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String sharedDrawingsToRoom(
|
||||
int success,
|
||||
int total,
|
||||
String plural,
|
||||
String roomName,
|
||||
) {
|
||||
return 'Передано $success/$total рисун$plural в $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get showReceivedDrawings => 'Показать полученные рисунки';
|
||||
|
||||
@@ -493,9 +377,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get clearAll => 'Очистить всё';
|
||||
|
||||
@override
|
||||
String get noLocalDrawings => 'Нет локальных рисунков для отправки';
|
||||
|
||||
@override
|
||||
String get publicChannel => 'Публичный канал';
|
||||
|
||||
@@ -505,37 +386,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get storedPermanently => 'Сохранено постоянно в комнате';
|
||||
|
||||
@override
|
||||
String drawingsSentToPublicChannel(int count, String plural) {
|
||||
return 'Отправлено $count рисун$plural в публичный канал';
|
||||
}
|
||||
|
||||
@override
|
||||
String drawingsSharedToPublicChannel(int success, int total) {
|
||||
return 'Передано $success/$total рисунков в публичный канал';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedToDevice => 'Устройство не подключено';
|
||||
|
||||
@override
|
||||
String get directMessage => 'Личное сообщение';
|
||||
|
||||
@override
|
||||
String directMessageSentTo(String contactName) {
|
||||
return 'Личное сообщение отправлено $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToSend(String error) {
|
||||
return 'Не удалось отправить: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String directMessageInfo(String contactName) {
|
||||
return 'Это сообщение будет отправлено напрямую $contactName. Оно также появится в общей ленте сообщений.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get typeYourMessage => 'Введите сообщение...';
|
||||
|
||||
@@ -565,9 +418,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get location => 'Местоположение';
|
||||
|
||||
@override
|
||||
String get myLocation => 'Моё местоположение';
|
||||
|
||||
@override
|
||||
String get fromMap => 'С карты';
|
||||
|
||||
@@ -630,9 +480,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Широковещательно';
|
||||
|
||||
@override
|
||||
String get admin => 'Администратор';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Вход выполнен';
|
||||
|
||||
@@ -642,29 +489,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get distance => 'Расстояние';
|
||||
|
||||
@override
|
||||
String pingingDirect(String name) {
|
||||
return 'Пинг $name (напрямую по маршруту)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingingFlood(String name) {
|
||||
return 'Пинг $name (широковещательно — нет маршрута)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String directPingTimeout(String name) {
|
||||
return 'Таймаут прямого пинга — повтор $name широковещательно...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingSuccessful(String name, String fallback) {
|
||||
return 'Пинг успешен: $name$fallback';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viaFloodingFallback => ' (через широковещательный резерв)';
|
||||
|
||||
@override
|
||||
String pingFailed(String name) {
|
||||
return 'Пинг не удался: $name — ответ не получен';
|
||||
@@ -675,16 +504,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return 'Вы уверены, что хотите удалить \"$name\"?\n\nЭто удалит контакт как из приложения, так и с сопряжённого радиоустройства.';
|
||||
}
|
||||
|
||||
@override
|
||||
String removingContact(String name) {
|
||||
return 'Удаление $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactRemoved(String name) {
|
||||
return 'Контакт \"$name\" удалён';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToRemoveContact(String error) {
|
||||
return 'Не удалось удалить контакт: $error';
|
||||
@@ -729,11 +548,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get telemetry => 'Телеметрия';
|
||||
|
||||
@override
|
||||
String requestingTelemetry(String name) {
|
||||
return 'Запрос телеметрии от $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get voltage => 'Напряжение';
|
||||
|
||||
@@ -805,9 +619,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get showAll => 'Показать все';
|
||||
|
||||
@override
|
||||
String get nearbyContacts => 'Ближайшие контакты';
|
||||
|
||||
@override
|
||||
String get locationUnavailable => 'Местоположение недоступно';
|
||||
|
||||
@@ -848,173 +659,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get channels => 'Каналы';
|
||||
|
||||
@override
|
||||
String get cacheStatistics => 'Статистика кэша';
|
||||
|
||||
@override
|
||||
String get totalTiles => 'Всего тайлов';
|
||||
|
||||
@override
|
||||
String get cacheSize => 'Размер кэша';
|
||||
|
||||
@override
|
||||
String get storeName => 'Имя хранилища';
|
||||
|
||||
@override
|
||||
String get noCacheStatistics => 'Статистика кэша недоступна';
|
||||
|
||||
@override
|
||||
String get downloadRegion => 'Скачать регион';
|
||||
|
||||
@override
|
||||
String get mapLayer => 'Слой карты';
|
||||
|
||||
@override
|
||||
String get regionBounds => 'Границы региона';
|
||||
|
||||
@override
|
||||
String get north => 'Север';
|
||||
|
||||
@override
|
||||
String get south => 'Юг';
|
||||
|
||||
@override
|
||||
String get east => 'Восток';
|
||||
|
||||
@override
|
||||
String get west => 'Запад';
|
||||
|
||||
@override
|
||||
String get zoomLevels => 'Уровни масштаба';
|
||||
|
||||
@override
|
||||
String minZoom(int zoom) {
|
||||
return 'Мин: $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoom(int zoom) {
|
||||
return 'Макс: $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String get downloadingDots => 'Загрузка...';
|
||||
|
||||
@override
|
||||
String get cancelDownload => 'Отменить загрузку';
|
||||
|
||||
@override
|
||||
String get downloadRegionButton => 'Скачать регион';
|
||||
|
||||
@override
|
||||
String get downloadNote =>
|
||||
'Внимание: большие регионы или высокие уровни масштаба могут потребовать значительного времени и места.';
|
||||
|
||||
@override
|
||||
String get cacheManagement => 'Управление кэшем';
|
||||
|
||||
@override
|
||||
String get clearAllMaps => 'Очистить все карты';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmTitle => 'Очистить все карты';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmMessage =>
|
||||
'Вы уверены, что хотите удалить все загруженные карты? Это действие нельзя отменить.';
|
||||
|
||||
@override
|
||||
String get mapDownloadCompleted => 'Загрузка карты завершена!';
|
||||
|
||||
@override
|
||||
String get cacheClearedSuccessfully => 'Кэш успешно очищен!';
|
||||
|
||||
@override
|
||||
String get downloadCancelled => 'Загрузка отменена';
|
||||
|
||||
@override
|
||||
String get startingDownload => 'Начало загрузки...';
|
||||
|
||||
@override
|
||||
String get downloadingMapTiles => 'Загрузка тайлов карты...';
|
||||
|
||||
@override
|
||||
String get downloadCompletedSuccessfully => 'Загрузка успешно завершена!';
|
||||
|
||||
@override
|
||||
String get cancellingDownload => 'Отмена загрузки...';
|
||||
|
||||
@override
|
||||
String errorLoadingStats(String error) {
|
||||
return 'Ошибка загрузки статистики: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String downloadFailed(String error) {
|
||||
return 'Ошибка загрузки: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String cancelFailed(String error) {
|
||||
return 'Ошибка отмены: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String clearCacheFailed(String error) {
|
||||
return 'Ошибка очистки кэша: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String minZoomError(String error) {
|
||||
return 'Мин. масштаб: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoomError(String error) {
|
||||
return 'Макс. масштаб: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minZoomGreaterThanMax =>
|
||||
'Минимальный масштаб должен быть меньше или равен максимальному';
|
||||
|
||||
@override
|
||||
String get selectMapLayer => 'Выбрать слой карты';
|
||||
|
||||
@override
|
||||
String get mapOptions => 'Параметры карты';
|
||||
|
||||
@override
|
||||
String get showLegend => 'Показать легенду';
|
||||
|
||||
@override
|
||||
String get displayMarkerTypeCounts => 'Отображать счётчики типов маркеров';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Поворачивать карту по курсу';
|
||||
|
||||
@override
|
||||
String get mapFollowsDirection => 'Карта следует вашему направлению движения';
|
||||
|
||||
@override
|
||||
String get resetMapRotation => 'Сбросить поворот';
|
||||
|
||||
@override
|
||||
String get resetMapRotationTooltip => 'Вернуть карту на север';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Показать отладочную информацию карты';
|
||||
|
||||
@override
|
||||
String get displayZoomLevelBounds => 'Отображать уровень масштаба и границы';
|
||||
|
||||
@override
|
||||
String get fullscreenMode => 'Полноэкранный режим';
|
||||
|
||||
@override
|
||||
String get hideUiFullMapView =>
|
||||
'Скрыть все элементы интерфейса для полного вида карты';
|
||||
|
||||
@override
|
||||
String get openStreetMap => 'OpenStreetMap';
|
||||
|
||||
@@ -1033,12 +680,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get googleTerrain => 'Google Рельеф';
|
||||
|
||||
@override
|
||||
String get downloadVisibleArea => 'Скачать видимую область';
|
||||
|
||||
@override
|
||||
String get initializingMap => 'Инициализация карты...';
|
||||
|
||||
@override
|
||||
String get dragToPosition => 'Перетащите для позиционирования';
|
||||
|
||||
@@ -1054,52 +695,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get sarAlert => 'ТРЕВОГА SAR';
|
||||
|
||||
@override
|
||||
String get messageSentToPublicChannel =>
|
||||
'Сообщение отправлено в публичный канал';
|
||||
|
||||
@override
|
||||
String get pleaseSelectRoomToSendSar =>
|
||||
'Пожалуйста, выберите комнату для отправки маркера SAR';
|
||||
|
||||
@override
|
||||
String failedToSendSarMarker(String error) {
|
||||
return 'Не удалось отправить маркер SAR: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String sarMarkerSentTo(String roomName) {
|
||||
return 'Маркер SAR отправлен в $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedCannotSync =>
|
||||
'Нет подключения — синхронизация невозможна';
|
||||
|
||||
@override
|
||||
String syncedMessageCount(int count) {
|
||||
return 'Синхронизировано $count сообщение(й)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get noNewMessages => 'Новых сообщений нет';
|
||||
|
||||
@override
|
||||
String syncFailed(String error) {
|
||||
return 'Синхронизация не удалась: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get failedToResendMessage => 'Не удалось повторно отправить сообщение';
|
||||
|
||||
@override
|
||||
String get retryingMessage => 'Повторная отправка сообщения...';
|
||||
|
||||
@override
|
||||
String retryFailed(String error) {
|
||||
return 'Повтор не удался: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get textCopiedToClipboard => 'Текст скопирован в буфер обмена';
|
||||
|
||||
@@ -1111,9 +706,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get cannotReplyContactNotFound =>
|
||||
'Не удаётся ответить: контакт не найден';
|
||||
|
||||
@override
|
||||
String get messageDeleted => 'Сообщение удалено';
|
||||
|
||||
@override
|
||||
String get copyText => 'Копировать текст';
|
||||
|
||||
@@ -1149,12 +741,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get sarLocationShare => 'Местоположение SAR';
|
||||
|
||||
@override
|
||||
String get locationShared => 'Местоположение передано';
|
||||
|
||||
@override
|
||||
String get refreshedContacts => 'Контакты обновлены';
|
||||
|
||||
@override
|
||||
String get justNow => 'Только что';
|
||||
|
||||
@@ -1262,78 +848,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get you => 'Вы';
|
||||
|
||||
@override
|
||||
String get offlineVectorMaps => 'Офлайн-векторные карты';
|
||||
|
||||
@override
|
||||
String get offlineVectorMapsDescription =>
|
||||
'Импортируйте офлайн-векторные тайлы карт (формат MBTiles) для использования без интернета';
|
||||
|
||||
@override
|
||||
String get importMbtiles => 'Импортировать файл MBTiles';
|
||||
|
||||
@override
|
||||
String get importMbtilesNote =>
|
||||
'Поддерживаются файлы MBTiles с векторными тайлами (формат PBF/MVT). Отлично подходят выгрузки Geofabrik!';
|
||||
|
||||
@override
|
||||
String get noMbtilesFiles => 'Офлайн-векторные карты не найдены';
|
||||
|
||||
@override
|
||||
String get mbtilesImportedSuccessfully => 'Файл MBTiles успешно импортирован';
|
||||
|
||||
@override
|
||||
String get failedToImportMbtiles => 'Не удалось импортировать файл MBTiles';
|
||||
|
||||
@override
|
||||
String get deleteMbtilesConfirmTitle => 'Удалить офлайн-карту';
|
||||
|
||||
@override
|
||||
String deleteMbtilesConfirmMessage(String name) {
|
||||
return 'Вы уверены, что хотите удалить \"$name\"? Офлайн-карта будет удалена безвозвратно.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mbtilesDeletedSuccessfully => 'Офлайн-карта успешно удалена';
|
||||
|
||||
@override
|
||||
String get failedToDeleteMbtiles => 'Не удалось удалить офлайн-карту';
|
||||
|
||||
@override
|
||||
String get importExportCachedTiles => 'Импорт/Экспорт кэшированных тайлов';
|
||||
|
||||
@override
|
||||
String get importExportDescription =>
|
||||
'Резервное копирование, обмен и восстановление загруженных тайлов карт между устройствами';
|
||||
|
||||
@override
|
||||
String get exportTilesToFile => 'Экспортировать тайлы в файл';
|
||||
|
||||
@override
|
||||
String get importTilesFromFile => 'Импортировать тайлы из файла';
|
||||
|
||||
@override
|
||||
String get selectExportLocation => 'Выберите место для экспорта';
|
||||
|
||||
@override
|
||||
String get selectImportFile => 'Выберите архив тайлов';
|
||||
|
||||
@override
|
||||
String get exportingTiles => 'Экспорт тайлов...';
|
||||
|
||||
@override
|
||||
String get importingTiles => 'Импорт тайлов...';
|
||||
|
||||
@override
|
||||
String exportSuccess(int count) {
|
||||
return 'Экспортировано $count тайлов';
|
||||
}
|
||||
|
||||
@override
|
||||
String importSuccess(int count) {
|
||||
return 'Импортировано $count хранилищ';
|
||||
}
|
||||
|
||||
@override
|
||||
String exportFailed(String error) {
|
||||
return 'Ошибка экспорта: $error';
|
||||
@@ -1344,40 +858,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return 'Ошибка импорта: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get exportNote =>
|
||||
'Создаёт сжатый архив (.fmtc), которым можно поделиться и импортировать на других устройствах.';
|
||||
|
||||
@override
|
||||
String get importNote =>
|
||||
'Импортирует тайлы карт из ранее экспортированного архива. Тайлы будут объединены с существующим кэшем.';
|
||||
|
||||
@override
|
||||
String get noTilesToExport => 'Нет доступных тайлов для экспорта';
|
||||
|
||||
@override
|
||||
String archiveContainsStores(int count) {
|
||||
return 'Архив содержит $count хранилищ';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vectorTiles => 'Векторные тайлы';
|
||||
|
||||
@override
|
||||
String get schema => 'Схема';
|
||||
|
||||
@override
|
||||
String get unknown => 'Неизвестно';
|
||||
|
||||
@override
|
||||
String get bounds => 'Границы';
|
||||
|
||||
@override
|
||||
String get onlineLayers => 'Онлайн-слои';
|
||||
|
||||
@override
|
||||
String get offlineLayers => 'Офлайн-слои';
|
||||
|
||||
@override
|
||||
String get locationTrail => 'Трек местоположения';
|
||||
|
||||
@@ -1416,47 +902,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get trailControls => 'Управление треком';
|
||||
|
||||
@override
|
||||
String get exportTrailToGpx => 'Экспортировать трек в GPX';
|
||||
|
||||
@override
|
||||
String get importTrailFromGpx => 'Импортировать трек из GPX';
|
||||
|
||||
@override
|
||||
String get trailExportedSuccessfully => 'Трек успешно экспортирован!';
|
||||
|
||||
@override
|
||||
String get failedToExportTrail => 'Не удалось экспортировать трек';
|
||||
|
||||
@override
|
||||
String failedToImportTrail(String error) {
|
||||
return 'Не удалось импортировать трек: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get importTrail => 'Импортировать трек';
|
||||
|
||||
@override
|
||||
String importTrailQuestion(int pointCount) {
|
||||
return 'Импортировать трек из $pointCount точек?\n\nВы можете заменить текущий трек или просмотреть его рядом.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viewAlongside => 'Просмотреть рядом';
|
||||
|
||||
@override
|
||||
String get replaceCurrent => 'Заменить текущий';
|
||||
|
||||
@override
|
||||
String trailImported(int pointCount) {
|
||||
return 'Трек импортирован! ($pointCount точек)';
|
||||
}
|
||||
|
||||
@override
|
||||
String trailReplaced(int pointCount) {
|
||||
return 'Трек заменён! ($pointCount точек)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactTrails => 'Треки контактов';
|
||||
|
||||
@@ -1729,19 +1174,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get noRoomsFound => 'Комнаты не найдены';
|
||||
|
||||
@override
|
||||
String get noContactsOrRoomsAvailable => 'Нет доступных контактов или комнат';
|
||||
|
||||
@override
|
||||
String get noRecipientsAvailable => 'Нет доступных получателей';
|
||||
|
||||
@override
|
||||
String get noChannelsFound => 'Каналы не найдены';
|
||||
|
||||
@override
|
||||
String get messagesWillBeSentToPublicChannel =>
|
||||
'Сообщения будут отправлены в публичный канал';
|
||||
|
||||
@override
|
||||
String get newMessage => 'Новое сообщение';
|
||||
|
||||
@@ -1922,10 +1360,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get rectangleDrawing => 'Прямоугольник';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard =>
|
||||
'Координаты скопированы в буфер обмена';
|
||||
|
||||
@override
|
||||
String get manualCoordinates => 'Ввод координат вручную';
|
||||
|
||||
@@ -1938,37 +1372,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get longitudeLabel => 'Долгота';
|
||||
|
||||
@override
|
||||
String get invalidLatitude => 'Неверная широта (от -90 до 90)';
|
||||
|
||||
@override
|
||||
String get invalidLongitude => 'Неверная долгота (от -180 до 180)';
|
||||
|
||||
@override
|
||||
String get exampleCoordinates => 'Пример: 55.7558, 37.6173';
|
||||
|
||||
@override
|
||||
String get drawingShared => 'Рисунок на карте';
|
||||
|
||||
@override
|
||||
String get drawingHidden => 'Рисунок скрыт с карты';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count уже передано',
|
||||
one: '1 уже передан',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return 'Передано $count новых рисунк$plural';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareDrawing => 'Поделиться рисунком';
|
||||
|
||||
@@ -2025,12 +1431,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get forestRoads => 'Лесные дороги';
|
||||
|
||||
@override
|
||||
String get showCadastralParcels => 'Показать кадастровые участки';
|
||||
|
||||
@override
|
||||
String get showForestRoads => 'Показать лесные дороги';
|
||||
|
||||
@override
|
||||
String get wmsOverlays => 'WMS-наложения';
|
||||
|
||||
@@ -2135,10 +1535,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return 'Не удалось удалить канал: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get allChannelSlotsInUse =>
|
||||
'Все слоты каналов заняты (максимум 39 пользовательских каналов)';
|
||||
|
||||
@override
|
||||
String get createChannel => 'Создать канал';
|
||||
|
||||
@@ -2179,21 +1575,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get wizardConnectingFeature3 =>
|
||||
'Работает полностью офлайн — интернет не нужен';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeTitle => 'Простой режим';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeDescription =>
|
||||
'Впервые работаете с mesh-сетью? Включите простой режим для упрощённого интерфейса с основными функциями.';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature1 =>
|
||||
'Удобный для новичков интерфейс с основными функциями';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature2 =>
|
||||
'Переключиться в расширенный режим можно в любое время в настройках';
|
||||
|
||||
@override
|
||||
String get wizardChannelTitle => 'Каналы';
|
||||
|
||||
|
||||
@@ -29,9 +29,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get disconnect => 'Prekini';
|
||||
|
||||
@override
|
||||
String get scanningForDevices => 'Iskanje naprav...';
|
||||
|
||||
@override
|
||||
String get noDevicesFound => 'Ni najdenih naprav';
|
||||
|
||||
@@ -62,27 +59,14 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get failedToGetGpsLocation => 'Pridobitev GPS lokacije ni uspela';
|
||||
|
||||
@override
|
||||
String advertisedAtLocation(String latitude, String longitude) {
|
||||
return 'Objavljeno na $latitude, $longitude';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToAdvertise(String error) {
|
||||
return 'Objava ni uspela: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String reconnecting(int attempt, int max) {
|
||||
return 'Ponovno povezovanje... ($attempt/$max)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelReconnection => 'Prekliči ponovno povezovanje';
|
||||
|
||||
@override
|
||||
String get mapManagement => 'Upravljanje zemljevida';
|
||||
|
||||
@override
|
||||
String get general => 'Splošno';
|
||||
|
||||
@@ -129,13 +113,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get displayPacketActivity =>
|
||||
'Prikaži kazalnike aktivnosti paketov v zgornji vrstici';
|
||||
|
||||
@override
|
||||
String get simpleMode => 'Preprost način';
|
||||
|
||||
@override
|
||||
String get simpleModeDescription =>
|
||||
'Skrij nepomembne informacije v sporočilih in kontaktih';
|
||||
|
||||
@override
|
||||
String get disableMap => 'Onemogoči zemljevid';
|
||||
|
||||
@@ -149,74 +126,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get chooseLanguage => 'Izberite jezik';
|
||||
|
||||
@override
|
||||
String get english => 'Angleščina';
|
||||
|
||||
@override
|
||||
String get slovenian => 'Slovenščina';
|
||||
|
||||
@override
|
||||
String get croatian => 'Hrvaščina';
|
||||
|
||||
@override
|
||||
String get german => 'Nemščina';
|
||||
|
||||
@override
|
||||
String get spanish => 'Španščina';
|
||||
|
||||
@override
|
||||
String get french => 'Francoščina';
|
||||
|
||||
@override
|
||||
String get italian => 'Italijanščina';
|
||||
|
||||
@override
|
||||
String get locationBroadcasting => 'Oddajanje lokacije';
|
||||
|
||||
@override
|
||||
String get autoLocationTracking => 'Samodejno sledenje lokaciji';
|
||||
|
||||
@override
|
||||
String get automaticallyBroadcastPosition =>
|
||||
'Samodejno oddajaj posodobitve položaja';
|
||||
|
||||
@override
|
||||
String get configureTracking => 'Konfiguriraj sledenje';
|
||||
|
||||
@override
|
||||
String get distanceAndTimeThresholds => 'Pragovi razdalje in časa';
|
||||
|
||||
@override
|
||||
String get locationTrackingConfiguration => 'Konfiguracija sledenja lokaciji';
|
||||
|
||||
@override
|
||||
String get configureWhenLocationBroadcasts =>
|
||||
'Konfigurirajte, kdaj se oddajanja lokacije pošiljajo v omrežje mesh';
|
||||
|
||||
@override
|
||||
String get minimumDistance => 'Minimalna razdalja';
|
||||
|
||||
@override
|
||||
String broadcastAfterMoving(String distance) {
|
||||
return 'Oddajaj šele po premiku $distance metrov';
|
||||
}
|
||||
|
||||
@override
|
||||
String get maximumDistance => 'Maksimalna razdalja';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastAfterMoving(String distance) {
|
||||
return 'Vedno oddajaj po premiku $distance metrov';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minimumTimeInterval => 'Minimalni časovni interval';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastEvery(String duration) {
|
||||
return 'Vedno oddajaj vsakih $duration';
|
||||
}
|
||||
|
||||
@override
|
||||
String get save => 'Shrani';
|
||||
|
||||
@@ -252,12 +161,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get moreInfo => 'Več informacij';
|
||||
|
||||
@override
|
||||
String get learnMoreAbout => 'Več o MeshCore SAR';
|
||||
|
||||
@override
|
||||
String get developer => 'Razvijalec';
|
||||
|
||||
@override
|
||||
String get packageName => 'Ime paketa';
|
||||
|
||||
@@ -333,9 +236,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get refresh => 'Osveži';
|
||||
|
||||
@override
|
||||
String get sendDirectMessage => 'Pošlji';
|
||||
|
||||
@override
|
||||
String get resetPath => 'Ponastavi pot (preusmeri)';
|
||||
|
||||
@@ -454,21 +354,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
return 'Deli $count risb$plural';
|
||||
}
|
||||
|
||||
@override
|
||||
String sentDrawingsToRoom(int count, String plural, String roomName) {
|
||||
return 'Poslanih $count risb$plural karte v $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String sharedDrawingsToRoom(
|
||||
int success,
|
||||
int total,
|
||||
String plural,
|
||||
String roomName,
|
||||
) {
|
||||
return 'Deljeno $success/$total risb$plural v $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get showReceivedDrawings => 'Prikaži prejete risbe';
|
||||
|
||||
@@ -490,9 +375,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get clearAll => 'Počisti vse';
|
||||
|
||||
@override
|
||||
String get noLocalDrawings => 'Ni lokalnih risb za deljenje';
|
||||
|
||||
@override
|
||||
String get publicChannel => 'Javni kanal';
|
||||
|
||||
@@ -502,37 +384,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get storedPermanently => 'Trajno shranjeno v sobi';
|
||||
|
||||
@override
|
||||
String drawingsSentToPublicChannel(int count, String plural) {
|
||||
return 'Poslano $count risb$plural na javni kanal';
|
||||
}
|
||||
|
||||
@override
|
||||
String drawingsSharedToPublicChannel(int success, int total) {
|
||||
return 'Deljeno $success/$total risb na javni kanal';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedToDevice => 'Ni povezano z napravo';
|
||||
|
||||
@override
|
||||
String get directMessage => 'Neposredno sporočilo';
|
||||
|
||||
@override
|
||||
String directMessageSentTo(String contactName) {
|
||||
return 'Neposredno sporočilo poslano $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToSend(String error) {
|
||||
return 'Pošiljanje ni uspelo: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String directMessageInfo(String contactName) {
|
||||
return 'To sporočilo bo poslano neposredno $contactName. Prikazalo se bo tudi v glavnem viru sporočil.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get typeYourMessage => 'Vnesite svoje sporočilo...';
|
||||
|
||||
@@ -562,9 +416,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get location => 'Lokacija';
|
||||
|
||||
@override
|
||||
String get myLocation => 'Moja lokacija';
|
||||
|
||||
@override
|
||||
String get fromMap => 'Z zemljevida';
|
||||
|
||||
@@ -626,9 +477,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get flood => 'Razpršitev';
|
||||
|
||||
@override
|
||||
String get admin => 'Administrator';
|
||||
|
||||
@override
|
||||
String get loggedIn => 'Prijavljen';
|
||||
|
||||
@@ -638,29 +486,11 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get distance => 'Razdalja';
|
||||
|
||||
@override
|
||||
String pingingDirect(String name) {
|
||||
return 'Pinganje $name (neposredno preko poti)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingingFlood(String name) {
|
||||
return 'Pinganje $name (razpršitev - brez poti)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String directPingTimeout(String name) {
|
||||
return 'Časovna omejitev neposrednega pinga - ponovni poskus $name z razprševanjem...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingSuccessful(String name, String fallback) {
|
||||
return 'Ping uspešen do $name$fallback';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viaFloodingFallback => ' (preko rezervnega razprševanja)';
|
||||
|
||||
@override
|
||||
String pingFailed(String name) {
|
||||
return 'Ping neuspešen do $name - odgovor ni prejet';
|
||||
@@ -671,16 +501,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
return 'Ste prepričani, da želite izbrisati \"$name\"?\n\nTo bo odstranilo stik iz aplikacije in spremljevalne radijske naprave.';
|
||||
}
|
||||
|
||||
@override
|
||||
String removingContact(String name) {
|
||||
return 'Odstranjevanje $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactRemoved(String name) {
|
||||
return 'Stik \"$name\" odstranjen';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToRemoveContact(String error) {
|
||||
return 'Odstranjevanje stika ni uspelo: $error';
|
||||
@@ -725,11 +545,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get telemetry => 'Telemetrija';
|
||||
|
||||
@override
|
||||
String requestingTelemetry(String name) {
|
||||
return 'Zahtevanje telemetrije od $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get voltage => 'Napetost';
|
||||
|
||||
@@ -801,9 +616,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get showAll => 'Prikaži vse';
|
||||
|
||||
@override
|
||||
String get nearbyContacts => 'Bližnji stiki';
|
||||
|
||||
@override
|
||||
String get locationUnavailable => 'Lokacija ni na voljo';
|
||||
|
||||
@@ -844,174 +656,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get channels => 'Kanali';
|
||||
|
||||
@override
|
||||
String get cacheStatistics => 'Statistika predpomnilnika';
|
||||
|
||||
@override
|
||||
String get totalTiles => 'Skupaj ploščic';
|
||||
|
||||
@override
|
||||
String get cacheSize => 'Velikost predpomnilnika';
|
||||
|
||||
@override
|
||||
String get storeName => 'Ime skladišča';
|
||||
|
||||
@override
|
||||
String get noCacheStatistics => 'Statistika predpomnilnika ni na voljo';
|
||||
|
||||
@override
|
||||
String get downloadRegion => 'Prenesi regijo';
|
||||
|
||||
@override
|
||||
String get mapLayer => 'Sloj zemljevida';
|
||||
|
||||
@override
|
||||
String get regionBounds => 'Meje regije';
|
||||
|
||||
@override
|
||||
String get north => 'Sever';
|
||||
|
||||
@override
|
||||
String get south => 'Jug';
|
||||
|
||||
@override
|
||||
String get east => 'Vzhod';
|
||||
|
||||
@override
|
||||
String get west => 'Zahod';
|
||||
|
||||
@override
|
||||
String get zoomLevels => 'Nivoji povečave';
|
||||
|
||||
@override
|
||||
String minZoom(int zoom) {
|
||||
return 'Min: $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoom(int zoom) {
|
||||
return 'Maks: $zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String get downloadingDots => 'Prenašanje...';
|
||||
|
||||
@override
|
||||
String get cancelDownload => 'Prekliči prenos';
|
||||
|
||||
@override
|
||||
String get downloadRegionButton => 'Prenesi regijo';
|
||||
|
||||
@override
|
||||
String get downloadNote =>
|
||||
'Opomba: Velike regije ali visoki nivoji povečave lahko zahtevajo veliko časa in prostora za shranjevanje.';
|
||||
|
||||
@override
|
||||
String get cacheManagement => 'Upravljanje predpomnilnika';
|
||||
|
||||
@override
|
||||
String get clearAllMaps => 'Počisti vse zemljevide';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmTitle => 'Počisti vse zemljevide';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmMessage =>
|
||||
'Ste prepričani, da želite izbrisati vse prenesene zemljevide? Tega dejanja ni mogoče razveljaviti.';
|
||||
|
||||
@override
|
||||
String get mapDownloadCompleted => 'Prenos zemljevida končan!';
|
||||
|
||||
@override
|
||||
String get cacheClearedSuccessfully => 'Predpomnilnik uspešno počiščen!';
|
||||
|
||||
@override
|
||||
String get downloadCancelled => 'Prenos preklican';
|
||||
|
||||
@override
|
||||
String get startingDownload => 'Začetek prenosa...';
|
||||
|
||||
@override
|
||||
String get downloadingMapTiles => 'Prenašanje ploščic zemljevida...';
|
||||
|
||||
@override
|
||||
String get downloadCompletedSuccessfully => 'Prenos uspešno končan!';
|
||||
|
||||
@override
|
||||
String get cancellingDownload => 'Preklic prenosa...';
|
||||
|
||||
@override
|
||||
String errorLoadingStats(String error) {
|
||||
return 'Napaka pri nalaganju statistike: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String downloadFailed(String error) {
|
||||
return 'Prenos ni uspel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String cancelFailed(String error) {
|
||||
return 'Preklic ni uspel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String clearCacheFailed(String error) {
|
||||
return 'Čiščenje predpomnilnika ni uspelo: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String minZoomError(String error) {
|
||||
return 'Min povečava: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoomError(String error) {
|
||||
return 'Maks povečava: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minZoomGreaterThanMax =>
|
||||
'Minimalna povečava mora biti manjša ali enaka maksimalni povečavi';
|
||||
|
||||
@override
|
||||
String get selectMapLayer => 'Izberite sloj zemljevida';
|
||||
|
||||
@override
|
||||
String get mapOptions => 'Možnosti zemljevida';
|
||||
|
||||
@override
|
||||
String get showLegend => 'Prikaži legendo';
|
||||
|
||||
@override
|
||||
String get displayMarkerTypeCounts => 'Prikaži število vrst označevalcev';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Rotiraj zemljevid s smerjo';
|
||||
|
||||
@override
|
||||
String get mapFollowsDirection => 'Zemljevid sledi vaši smeri pri gibanju';
|
||||
|
||||
@override
|
||||
String get resetMapRotation => 'Ponastavi rotacijo';
|
||||
|
||||
@override
|
||||
String get resetMapRotationTooltip => 'Ponastavi zemljevid na sever';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo =>
|
||||
'Prikaži informacije za razhroščevanje zemljevida';
|
||||
|
||||
@override
|
||||
String get displayZoomLevelBounds => 'Prikaži nivo povečave in meje';
|
||||
|
||||
@override
|
||||
String get fullscreenMode => 'Način celozaslonskega prikaza';
|
||||
|
||||
@override
|
||||
String get hideUiFullMapView =>
|
||||
'Skrij vse UI kontrole za poln prikaz zemljevida';
|
||||
|
||||
@override
|
||||
String get openStreetMap => 'OpenStreetMap';
|
||||
|
||||
@@ -1030,12 +677,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get googleTerrain => 'Google teren';
|
||||
|
||||
@override
|
||||
String get downloadVisibleArea => 'Prenesi vidno območje';
|
||||
|
||||
@override
|
||||
String get initializingMap => 'Inicializacija zemljevida...';
|
||||
|
||||
@override
|
||||
String get dragToPosition => 'Povleci na položaj';
|
||||
|
||||
@@ -1051,51 +692,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get sarAlert => 'SAR ALARM';
|
||||
|
||||
@override
|
||||
String get messageSentToPublicChannel => 'Sporočilo poslano na javni kanal';
|
||||
|
||||
@override
|
||||
String get pleaseSelectRoomToSendSar =>
|
||||
'Prosimo, izberite sobo za pošiljanje SAR označevalca';
|
||||
|
||||
@override
|
||||
String failedToSendSarMarker(String error) {
|
||||
return 'Pošiljanje SAR označevalca ni uspelo: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String sarMarkerSentTo(String roomName) {
|
||||
return 'SAR označevalec poslan v $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedCannotSync =>
|
||||
'Ni povezano - sporočil ni mogoče sinhronizirati';
|
||||
|
||||
@override
|
||||
String syncedMessageCount(int count) {
|
||||
return 'Sinhronizirano $count sporočil';
|
||||
}
|
||||
|
||||
@override
|
||||
String get noNewMessages => 'Ni novih sporočil';
|
||||
|
||||
@override
|
||||
String syncFailed(String error) {
|
||||
return 'Sinhronizacija ni uspela: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get failedToResendMessage => 'Ponovno pošiljanje sporočila ni uspelo';
|
||||
|
||||
@override
|
||||
String get retryingMessage => 'Ponovni poskus pošiljanja sporočila...';
|
||||
|
||||
@override
|
||||
String retryFailed(String error) {
|
||||
return 'Ponovni poskus ni uspel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get textCopiedToClipboard => 'Besedilo kopirano v odložišče';
|
||||
|
||||
@@ -1107,9 +703,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get cannotReplyContactNotFound =>
|
||||
'Ni mogoče odgovoriti: stik ni najden';
|
||||
|
||||
@override
|
||||
String get messageDeleted => 'Sporočilo izbrisano';
|
||||
|
||||
@override
|
||||
String get copyText => 'Kopiraj besedilo';
|
||||
|
||||
@@ -1145,12 +738,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get sarLocationShare => 'SAR Lokacija';
|
||||
|
||||
@override
|
||||
String get locationShared => 'Lokacija deljena';
|
||||
|
||||
@override
|
||||
String get refreshedContacts => 'Stiki osveženi';
|
||||
|
||||
@override
|
||||
String get justNow => 'Pravkar';
|
||||
|
||||
@@ -1258,81 +845,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get you => 'Ti';
|
||||
|
||||
@override
|
||||
String get offlineVectorMaps => 'Brezpovezni vektorski zemljevidi';
|
||||
|
||||
@override
|
||||
String get offlineVectorMapsDescription =>
|
||||
'Uvozite in upravljajte brezpovezne vektorske ploščice zemljevidov (format MBTiles) za uporabo brez internetne povezave';
|
||||
|
||||
@override
|
||||
String get importMbtiles => 'Uvozi MBTiles datoteko';
|
||||
|
||||
@override
|
||||
String get importMbtilesNote =>
|
||||
'Podpira MBTiles datoteke z vektorskimi ploščicami (format PBF/MVT). Geofabrik izvozi odlično delujejo!';
|
||||
|
||||
@override
|
||||
String get noMbtilesFiles =>
|
||||
'Ni najdenih brezpoveznih vektorskih zemljevidov';
|
||||
|
||||
@override
|
||||
String get mbtilesImportedSuccessfully => 'MBTiles datoteka uspešno uvožena';
|
||||
|
||||
@override
|
||||
String get failedToImportMbtiles => 'Uvoz MBTiles datoteke ni uspel';
|
||||
|
||||
@override
|
||||
String get deleteMbtilesConfirmTitle => 'Izbriši brezpovezni zemljevid';
|
||||
|
||||
@override
|
||||
String deleteMbtilesConfirmMessage(String name) {
|
||||
return 'Ste prepričani, da želite izbrisati \"$name\"? To bo trajno odstranilo brezpovezni zemljevid.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mbtilesDeletedSuccessfully =>
|
||||
'Brezpovezni zemljevid uspešno izbrisan';
|
||||
|
||||
@override
|
||||
String get failedToDeleteMbtiles =>
|
||||
'Brisanje brezpoveznega zemljevida ni uspelo';
|
||||
|
||||
@override
|
||||
String get importExportCachedTiles => 'Uvoz/Izvoz predpomnjenih ploščic';
|
||||
|
||||
@override
|
||||
String get importExportDescription =>
|
||||
'Varnostno kopirajte, delite in obnovite prenesene ploščice zemljevida med napravami';
|
||||
|
||||
@override
|
||||
String get exportTilesToFile => 'Izvozi ploščice v datoteko';
|
||||
|
||||
@override
|
||||
String get importTilesFromFile => 'Uvozi ploščice iz datoteke';
|
||||
|
||||
@override
|
||||
String get selectExportLocation => 'Izberi lokacijo izvoza';
|
||||
|
||||
@override
|
||||
String get selectImportFile => 'Izberi arhiv ploščic';
|
||||
|
||||
@override
|
||||
String get exportingTiles => 'Izvažanje ploščic...';
|
||||
|
||||
@override
|
||||
String get importingTiles => 'Uvažanje ploščic...';
|
||||
|
||||
@override
|
||||
String exportSuccess(int count) {
|
||||
return 'Uspešno izvoženih $count ploščic';
|
||||
}
|
||||
|
||||
@override
|
||||
String importSuccess(int count) {
|
||||
return 'Uspešno uvoženih $count skladišč';
|
||||
}
|
||||
|
||||
@override
|
||||
String exportFailed(String error) {
|
||||
return 'Izvoz ni uspel: $error';
|
||||
@@ -1343,40 +855,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
return 'Uvoz ni uspel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get exportNote =>
|
||||
'Ustvari stisnjeno arhivsko datoteko (.fmtc), ki jo lahko delite in uvozite na drugih napravah.';
|
||||
|
||||
@override
|
||||
String get importNote =>
|
||||
'Uvozi ploščice zemljevida iz predhodno izvožene arhivske datoteke. Ploščice bodo združene z obstoječim predpomnilnikom.';
|
||||
|
||||
@override
|
||||
String get noTilesToExport => 'Ni ploščic za izvoz';
|
||||
|
||||
@override
|
||||
String archiveContainsStores(int count) {
|
||||
return 'Arhiv vsebuje $count skladišč';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vectorTiles => 'Vektorske ploščice';
|
||||
|
||||
@override
|
||||
String get schema => 'Shema';
|
||||
|
||||
@override
|
||||
String get unknown => 'Neznano';
|
||||
|
||||
@override
|
||||
String get bounds => 'Meje';
|
||||
|
||||
@override
|
||||
String get onlineLayers => 'Spletne plasti';
|
||||
|
||||
@override
|
||||
String get offlineLayers => 'Brezpovezne plasti';
|
||||
|
||||
@override
|
||||
String get locationTrail => 'Sledilna pot';
|
||||
|
||||
@@ -1415,47 +899,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get trailControls => 'Nadzor poti';
|
||||
|
||||
@override
|
||||
String get exportTrailToGpx => 'Izvozi pot v GPX';
|
||||
|
||||
@override
|
||||
String get importTrailFromGpx => 'Uvozi pot iz GPX';
|
||||
|
||||
@override
|
||||
String get trailExportedSuccessfully => 'Pot uspešno izvožena!';
|
||||
|
||||
@override
|
||||
String get failedToExportTrail => 'Izvoz poti ni uspel';
|
||||
|
||||
@override
|
||||
String failedToImportTrail(String error) {
|
||||
return 'Uvoz poti ni uspel: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get importTrail => 'Uvozi pot';
|
||||
|
||||
@override
|
||||
String importTrailQuestion(int pointCount) {
|
||||
return 'Uvozi pot s $pointCount točkami?\n\nLahko zamenjate trenutno pot ali jo prikažete skupaj.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viewAlongside => 'Prikaži skupaj';
|
||||
|
||||
@override
|
||||
String get replaceCurrent => 'Zamenjaj trenutno';
|
||||
|
||||
@override
|
||||
String trailImported(int pointCount) {
|
||||
return 'Pot uvožena! ($pointCount točk)';
|
||||
}
|
||||
|
||||
@override
|
||||
String trailReplaced(int pointCount) {
|
||||
return 'Pot zamenjana! ($pointCount točk)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactTrails => 'Poti stikov';
|
||||
|
||||
@@ -1661,7 +1104,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get tapAddToCreate => 'Tapnite + za ustvarjanje prve predloge';
|
||||
|
||||
@override
|
||||
String get ok => 'OK';
|
||||
String get ok => 'V redu';
|
||||
|
||||
@override
|
||||
String get permissionsSection => 'Dovoljenja';
|
||||
@@ -1725,19 +1168,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get noRoomsFound => 'Ni sob';
|
||||
|
||||
@override
|
||||
String get noContactsOrRoomsAvailable => 'Ni na voljo kontaktov ali sob';
|
||||
|
||||
@override
|
||||
String get noRecipientsAvailable => 'Ni na voljo prejemnikov';
|
||||
|
||||
@override
|
||||
String get noChannelsFound => 'Ni najdenih kanalov';
|
||||
|
||||
@override
|
||||
String get messagesWillBeSentToPublicChannel =>
|
||||
'Sporočila bodo poslana na javni kanal';
|
||||
|
||||
@override
|
||||
String get newMessage => 'Novo sporočilo';
|
||||
|
||||
@@ -1918,9 +1354,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get rectangleDrawing => 'Pravokotna risba';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard => 'Koordinate kopirane v odložišče';
|
||||
|
||||
@override
|
||||
String get manualCoordinates => 'Ročne koordinate';
|
||||
|
||||
@@ -1933,37 +1366,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get longitudeLabel => 'Geografska dolžina';
|
||||
|
||||
@override
|
||||
String get invalidLatitude => 'Neveljavna geografska širina (-90 do 90)';
|
||||
|
||||
@override
|
||||
String get invalidLongitude => 'Neveljavna geografska dolžina (-180 do 180)';
|
||||
|
||||
@override
|
||||
String get exampleCoordinates => 'Primer: 46.0569, 14.5058';
|
||||
|
||||
@override
|
||||
String get drawingShared => 'Risba deljena';
|
||||
|
||||
@override
|
||||
String get drawingHidden => 'Risba skrita z zemljevida';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count že deljeno',
|
||||
one: '1 že deljeno',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return 'Deljeno $count nov$plural risb$plural';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareDrawing => 'Deli risbo';
|
||||
|
||||
@@ -2018,12 +1423,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get forestRoads => 'Gozdne ceste';
|
||||
|
||||
@override
|
||||
String get showCadastralParcels => 'Prikaži katastrske parcele';
|
||||
|
||||
@override
|
||||
String get showForestRoads => 'Prikaži gozdne ceste';
|
||||
|
||||
@override
|
||||
String get wmsOverlays => 'WMS prekrivanja';
|
||||
|
||||
@@ -2126,10 +1525,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
return 'Neuspešno brisanje kanala: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get allChannelSlotsInUse =>
|
||||
'Vsa mesta za kanale so zasedena (maksimalno 39 prilagojenih kanalov)';
|
||||
|
||||
@override
|
||||
String get createChannel => 'Ustvari kanal';
|
||||
|
||||
@@ -2170,21 +1565,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get wizardConnectingFeature3 =>
|
||||
'Deluje povsem brez povezave — internet ni potreben';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeTitle => 'Preprost način';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeDescription =>
|
||||
'Ste novi v mesh omrežju? Vključite preprost način za poenostavljen vmesnik z osnovnimi funkcijami.';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature1 =>
|
||||
'Vmesnik, prilagojen začetnikom, z osnovnimi funkcijami';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature2 =>
|
||||
'Na napredni način lahko kadarkoli preklopite v nastavitvah';
|
||||
|
||||
@override
|
||||
String get wizardChannelTitle => 'Kanali';
|
||||
|
||||
@@ -2279,7 +1659,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get delivery => 'Dostava';
|
||||
|
||||
@override
|
||||
String get status => 'Status';
|
||||
String get status => 'Stanje';
|
||||
|
||||
@override
|
||||
String get expectedAckTag => 'Pričakovana oznaka ACK';
|
||||
|
||||
1723
lib/l10n/app_localizations_tr.dart
Normal file
1723
lib/l10n/app_localizations_tr.dart
Normal file
File diff suppressed because it is too large
Load Diff
1735
lib/l10n/app_localizations_uk.dart
Normal file
1735
lib/l10n/app_localizations_uk.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -29,9 +29,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get disconnect => '断开连接';
|
||||
|
||||
@override
|
||||
String get scanningForDevices => '正在扫描设备...';
|
||||
|
||||
@override
|
||||
String get noDevicesFound => '未找到设备';
|
||||
|
||||
@@ -59,27 +56,14 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get failedToGetGpsLocation => '获取 GPS 位置失败';
|
||||
|
||||
@override
|
||||
String advertisedAtLocation(String latitude, String longitude) {
|
||||
return '已广播位置 $latitude, $longitude';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToAdvertise(String error) {
|
||||
return '广播失败: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String reconnecting(int attempt, int max) {
|
||||
return '正在重新连接... ($attempt/$max)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelReconnection => '取消重新连接';
|
||||
|
||||
@override
|
||||
String get mapManagement => '地图管理';
|
||||
|
||||
@override
|
||||
String get general => '通用';
|
||||
|
||||
@@ -125,12 +109,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get displayPacketActivity => '在顶部栏显示数据包活动指示器';
|
||||
|
||||
@override
|
||||
String get simpleMode => '简洁模式';
|
||||
|
||||
@override
|
||||
String get simpleModeDescription => '在消息和联系人中隐藏非必要信息';
|
||||
|
||||
@override
|
||||
String get disableMap => '禁用地图';
|
||||
|
||||
@@ -143,72 +121,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get chooseLanguage => '选择语言';
|
||||
|
||||
@override
|
||||
String get english => '英语';
|
||||
|
||||
@override
|
||||
String get slovenian => '斯洛文尼亚语';
|
||||
|
||||
@override
|
||||
String get croatian => '克罗地亚语';
|
||||
|
||||
@override
|
||||
String get german => '德语';
|
||||
|
||||
@override
|
||||
String get spanish => '西班牙语';
|
||||
|
||||
@override
|
||||
String get french => '法语';
|
||||
|
||||
@override
|
||||
String get italian => '意大利语';
|
||||
|
||||
@override
|
||||
String get locationBroadcasting => '位置广播';
|
||||
|
||||
@override
|
||||
String get autoLocationTracking => '自动位置跟踪';
|
||||
|
||||
@override
|
||||
String get automaticallyBroadcastPosition => '自动广播位置更新';
|
||||
|
||||
@override
|
||||
String get configureTracking => '配置跟踪';
|
||||
|
||||
@override
|
||||
String get distanceAndTimeThresholds => '距离和时间阈值';
|
||||
|
||||
@override
|
||||
String get locationTrackingConfiguration => '位置跟踪配置';
|
||||
|
||||
@override
|
||||
String get configureWhenLocationBroadcasts => '配置何时向 Mesh 网络发送位置广播';
|
||||
|
||||
@override
|
||||
String get minimumDistance => '最小距离';
|
||||
|
||||
@override
|
||||
String broadcastAfterMoving(String distance) {
|
||||
return '仅在移动 $distance 米后广播';
|
||||
}
|
||||
|
||||
@override
|
||||
String get maximumDistance => '最大距离';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastAfterMoving(String distance) {
|
||||
return '移动 $distance 米后始终广播';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minimumTimeInterval => '最小时间间隔';
|
||||
|
||||
@override
|
||||
String alwaysBroadcastEvery(String duration) {
|
||||
return '每隔 $duration 始终广播';
|
||||
}
|
||||
|
||||
@override
|
||||
String get save => '保存';
|
||||
|
||||
@@ -244,12 +156,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get moreInfo => '更多信息';
|
||||
|
||||
@override
|
||||
String get learnMoreAbout => '了解更多关于 MeshCore 搜救助手的信息';
|
||||
|
||||
@override
|
||||
String get developer => '开发者';
|
||||
|
||||
@override
|
||||
String get packageName => '包名';
|
||||
|
||||
@@ -321,9 +227,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get refresh => '刷新';
|
||||
|
||||
@override
|
||||
String get sendDirectMessage => '发送';
|
||||
|
||||
@override
|
||||
String get resetPath => '重置路径 (重新路由)';
|
||||
|
||||
@@ -441,21 +344,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
return '共享 $count 个绘图';
|
||||
}
|
||||
|
||||
@override
|
||||
String sentDrawingsToRoom(int count, String plural, String roomName) {
|
||||
return '已发送 $count 个地图绘制到 $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String sharedDrawingsToRoom(
|
||||
int success,
|
||||
int total,
|
||||
String plural,
|
||||
String roomName,
|
||||
) {
|
||||
return '已将 $success/$total 个绘图共享到 $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get showReceivedDrawings => '显示接收到的绘图';
|
||||
|
||||
@@ -477,9 +365,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get clearAll => '全部清除';
|
||||
|
||||
@override
|
||||
String get noLocalDrawings => '没有可共享的本地绘图';
|
||||
|
||||
@override
|
||||
String get publicChannel => '公共频道';
|
||||
|
||||
@@ -489,37 +374,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get storedPermanently => '永久存储在房间中';
|
||||
|
||||
@override
|
||||
String drawingsSentToPublicChannel(int count, String plural) {
|
||||
return '已将 $count 个地图绘制发送到公共频道';
|
||||
}
|
||||
|
||||
@override
|
||||
String drawingsSharedToPublicChannel(int success, int total) {
|
||||
return '已将 $success/$total 个绘图共享到公共频道';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedToDevice => '未连接到设备';
|
||||
|
||||
@override
|
||||
String get directMessage => '直连消息';
|
||||
|
||||
@override
|
||||
String directMessageSentTo(String contactName) {
|
||||
return '直连消息已发送给 $contactName';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToSend(String error) {
|
||||
return '发送失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String directMessageInfo(String contactName) {
|
||||
return '此消息将直连发送给 $contactName。它也将出现在主消息流中。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get typeYourMessage => '输入您的消息...';
|
||||
|
||||
@@ -547,9 +404,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get location => '位置';
|
||||
|
||||
@override
|
||||
String get myLocation => '我的位置';
|
||||
|
||||
@override
|
||||
String get fromMap => '来自地图';
|
||||
|
||||
@@ -610,9 +464,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get flood => '泛洪';
|
||||
|
||||
@override
|
||||
String get admin => '管理员';
|
||||
|
||||
@override
|
||||
String get loggedIn => '已登录';
|
||||
|
||||
@@ -622,29 +473,11 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get distance => '距离';
|
||||
|
||||
@override
|
||||
String pingingDirect(String name) {
|
||||
return '正在 Ping $name(通过路径直连发送)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingingFlood(String name) {
|
||||
return '正在 Ping $name(泛洪 - 无路径)...';
|
||||
}
|
||||
|
||||
@override
|
||||
String directPingTimeout(String name) {
|
||||
return '直连 Ping 超时 - 正在使用泛洪重试 $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String pingSuccessful(String name, String fallback) {
|
||||
return '成功 Ping 到 $name$fallback';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viaFloodingFallback => '(通过泛洪回退)';
|
||||
|
||||
@override
|
||||
String pingFailed(String name) {
|
||||
return 'Ping $name 失败 - 未收到响应';
|
||||
@@ -655,16 +488,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
return '您确定要删除“$name”吗?\n\n这将从应用程序和配套的无线电设备中移除该联系人。';
|
||||
}
|
||||
|
||||
@override
|
||||
String removingContact(String name) {
|
||||
return '正在移除 $name...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactRemoved(String name) {
|
||||
return '联系人“$name”已移除';
|
||||
}
|
||||
|
||||
@override
|
||||
String failedToRemoveContact(String error) {
|
||||
return '移除联系人失败:$error';
|
||||
@@ -709,11 +532,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get telemetry => '遥测';
|
||||
|
||||
@override
|
||||
String requestingTelemetry(String name) {
|
||||
return '正在向 $name 请求遥测数据...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get voltage => '电压';
|
||||
|
||||
@@ -785,9 +603,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get showAll => '显示全部';
|
||||
|
||||
@override
|
||||
String get nearbyContacts => '附近联系人';
|
||||
|
||||
@override
|
||||
String get locationUnavailable => '位置不可用';
|
||||
|
||||
@@ -827,169 +642,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get channels => '频道';
|
||||
|
||||
@override
|
||||
String get cacheStatistics => '缓存统计';
|
||||
|
||||
@override
|
||||
String get totalTiles => '总瓦片数';
|
||||
|
||||
@override
|
||||
String get cacheSize => '缓存大小';
|
||||
|
||||
@override
|
||||
String get storeName => '存储名称';
|
||||
|
||||
@override
|
||||
String get noCacheStatistics => '没有可用的缓存统计信息';
|
||||
|
||||
@override
|
||||
String get downloadRegion => '下载区域';
|
||||
|
||||
@override
|
||||
String get mapLayer => '地图图层';
|
||||
|
||||
@override
|
||||
String get regionBounds => '区域边界';
|
||||
|
||||
@override
|
||||
String get north => '北';
|
||||
|
||||
@override
|
||||
String get south => '南';
|
||||
|
||||
@override
|
||||
String get east => '东';
|
||||
|
||||
@override
|
||||
String get west => '西';
|
||||
|
||||
@override
|
||||
String get zoomLevels => '缩放级别';
|
||||
|
||||
@override
|
||||
String minZoom(int zoom) {
|
||||
return '最小:$zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoom(int zoom) {
|
||||
return '最大:$zoom';
|
||||
}
|
||||
|
||||
@override
|
||||
String get downloadingDots => '正在下载...';
|
||||
|
||||
@override
|
||||
String get cancelDownload => '取消下载';
|
||||
|
||||
@override
|
||||
String get downloadRegionButton => '下载区域';
|
||||
|
||||
@override
|
||||
String get downloadNote => '注意:大的区域或高缩放级别可能需要大量时间和存储空间。';
|
||||
|
||||
@override
|
||||
String get cacheManagement => '缓存管理';
|
||||
|
||||
@override
|
||||
String get clearAllMaps => '清除所有地图';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmTitle => '清除所有地图';
|
||||
|
||||
@override
|
||||
String get clearMapsConfirmMessage => '您确定要删除所有下载的地图吗?此操作无法撤销。';
|
||||
|
||||
@override
|
||||
String get mapDownloadCompleted => '地图下载完成!';
|
||||
|
||||
@override
|
||||
String get cacheClearedSuccessfully => '缓存清除成功!';
|
||||
|
||||
@override
|
||||
String get downloadCancelled => '下载已取消';
|
||||
|
||||
@override
|
||||
String get startingDownload => '正在开始下载...';
|
||||
|
||||
@override
|
||||
String get downloadingMapTiles => '正在下载地图瓦片...';
|
||||
|
||||
@override
|
||||
String get downloadCompletedSuccessfully => '下载成功完成!';
|
||||
|
||||
@override
|
||||
String get cancellingDownload => '正在取消下载...';
|
||||
|
||||
@override
|
||||
String errorLoadingStats(String error) {
|
||||
return '加载统计信息时出错:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String downloadFailed(String error) {
|
||||
return '下载失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String cancelFailed(String error) {
|
||||
return '取消失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String clearCacheFailed(String error) {
|
||||
return '清除缓存失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String minZoomError(String error) {
|
||||
return '最小缩放:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String maxZoomError(String error) {
|
||||
return '最大缩放:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get minZoomGreaterThanMax => '最小缩放必须小于或等于最大缩放';
|
||||
|
||||
@override
|
||||
String get selectMapLayer => '选择地图图层';
|
||||
|
||||
@override
|
||||
String get mapOptions => '地图选项';
|
||||
|
||||
@override
|
||||
String get showLegend => '显示图例';
|
||||
|
||||
@override
|
||||
String get displayMarkerTypeCounts => '显示标记类型计数';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => '随航向旋转地图';
|
||||
|
||||
@override
|
||||
String get mapFollowsDirection => '地图在移动时跟随您的方向';
|
||||
|
||||
@override
|
||||
String get resetMapRotation => '重置旋转';
|
||||
|
||||
@override
|
||||
String get resetMapRotationTooltip => '重置地图方向为北';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => '显示地图调试信息';
|
||||
|
||||
@override
|
||||
String get displayZoomLevelBounds => '显示缩放级别和边界';
|
||||
|
||||
@override
|
||||
String get fullscreenMode => '全屏模式';
|
||||
|
||||
@override
|
||||
String get hideUiFullMapView => '隐藏所有 UI 控件以获得全地图视图';
|
||||
|
||||
@override
|
||||
String get openStreetMap => 'OpenStreetMap';
|
||||
|
||||
@@ -1008,12 +663,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get googleTerrain => 'Google 地形图';
|
||||
|
||||
@override
|
||||
String get downloadVisibleArea => '下载可见区域';
|
||||
|
||||
@override
|
||||
String get initializingMap => '正在初始化地图...';
|
||||
|
||||
@override
|
||||
String get dragToPosition => '拖动以定位';
|
||||
|
||||
@@ -1029,49 +678,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get sarAlert => '搜救警报';
|
||||
|
||||
@override
|
||||
String get messageSentToPublicChannel => '消息已发送到公共频道';
|
||||
|
||||
@override
|
||||
String get pleaseSelectRoomToSendSar => '请选择一个房间发送搜救标记';
|
||||
|
||||
@override
|
||||
String failedToSendSarMarker(String error) {
|
||||
return '发送搜救标记失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String sarMarkerSentTo(String roomName) {
|
||||
return '搜救标记已发送到 $roomName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notConnectedCannotSync => '未连接 - 无法同步消息';
|
||||
|
||||
@override
|
||||
String syncedMessageCount(int count) {
|
||||
return '已同步 $count 条消息';
|
||||
}
|
||||
|
||||
@override
|
||||
String get noNewMessages => '没有新消息';
|
||||
|
||||
@override
|
||||
String syncFailed(String error) {
|
||||
return '同步失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get failedToResendMessage => '重新发送消息失败';
|
||||
|
||||
@override
|
||||
String get retryingMessage => '正在重试消息...';
|
||||
|
||||
@override
|
||||
String retryFailed(String error) {
|
||||
return '重试失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get textCopiedToClipboard => '文本已复制到剪贴板';
|
||||
|
||||
@@ -1081,9 +687,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get cannotReplyContactNotFound => '无法回复:未找到联系人';
|
||||
|
||||
@override
|
||||
String get messageDeleted => '消息已删除';
|
||||
|
||||
@override
|
||||
String get copyText => '复制文本';
|
||||
|
||||
@@ -1118,12 +721,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get sarLocationShare => '搜救位置';
|
||||
|
||||
@override
|
||||
String get locationShared => '位置已共享';
|
||||
|
||||
@override
|
||||
String get refreshedContacts => '联系人已刷新';
|
||||
|
||||
@override
|
||||
String get justNow => '刚刚';
|
||||
|
||||
@@ -1231,77 +828,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get you => '您';
|
||||
|
||||
@override
|
||||
String get offlineVectorMaps => '离线矢量地图';
|
||||
|
||||
@override
|
||||
String get offlineVectorMapsDescription =>
|
||||
'导入和管理离线矢量地图瓦片(MBTiles 格式),以便在没有互联网连接时使用';
|
||||
|
||||
@override
|
||||
String get importMbtiles => '导入 MBTiles 文件';
|
||||
|
||||
@override
|
||||
String get importMbtilesNote =>
|
||||
'支持带有矢量瓦片(PBF/MVT 格式)的 MBTiles 文件。Geofabrik 的数据提取非常好用!';
|
||||
|
||||
@override
|
||||
String get noMbtilesFiles => '未找到离线矢量地图';
|
||||
|
||||
@override
|
||||
String get mbtilesImportedSuccessfully => 'MBTiles 文件导入成功';
|
||||
|
||||
@override
|
||||
String get failedToImportMbtiles => '导入 MBTiles 文件失败';
|
||||
|
||||
@override
|
||||
String get deleteMbtilesConfirmTitle => '删除离线地图';
|
||||
|
||||
@override
|
||||
String deleteMbtilesConfirmMessage(String name) {
|
||||
return '您确定要删除“$name”吗?这将永久移除该离线地图。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mbtilesDeletedSuccessfully => '离线地图删除成功';
|
||||
|
||||
@override
|
||||
String get failedToDeleteMbtiles => '删除离线地图失败';
|
||||
|
||||
@override
|
||||
String get importExportCachedTiles => '导入/导出缓存的瓦片';
|
||||
|
||||
@override
|
||||
String get importExportDescription => '在设备之间备份、共享和恢复下载的地图瓦片';
|
||||
|
||||
@override
|
||||
String get exportTilesToFile => '导出瓦片到文件';
|
||||
|
||||
@override
|
||||
String get importTilesFromFile => '从文件导入瓦片';
|
||||
|
||||
@override
|
||||
String get selectExportLocation => '选择导出位置';
|
||||
|
||||
@override
|
||||
String get selectImportFile => '选择瓦片存档';
|
||||
|
||||
@override
|
||||
String get exportingTiles => '正在导出瓦片...';
|
||||
|
||||
@override
|
||||
String get importingTiles => '正在导入瓦片...';
|
||||
|
||||
@override
|
||||
String exportSuccess(int count) {
|
||||
return '成功导出 $count 个瓦片';
|
||||
}
|
||||
|
||||
@override
|
||||
String importSuccess(int count) {
|
||||
return '成功导入 $count 个存储';
|
||||
}
|
||||
|
||||
@override
|
||||
String exportFailed(String error) {
|
||||
return '导出失败:$error';
|
||||
@@ -1312,38 +838,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
return '导入失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get exportNote => '创建一个压缩存档(.fmtc)文件,可以共享并在其他设备上导入。';
|
||||
|
||||
@override
|
||||
String get importNote => '从先前导出的存档文件导入地图瓦片。瓦片将与现有缓存合并。';
|
||||
|
||||
@override
|
||||
String get noTilesToExport => '没有可导出的瓦片';
|
||||
|
||||
@override
|
||||
String archiveContainsStores(int count) {
|
||||
return '存档包含 $count 个存储';
|
||||
}
|
||||
|
||||
@override
|
||||
String get vectorTiles => '矢量瓦片';
|
||||
|
||||
@override
|
||||
String get schema => '模式';
|
||||
|
||||
@override
|
||||
String get unknown => '未知';
|
||||
|
||||
@override
|
||||
String get bounds => '边界';
|
||||
|
||||
@override
|
||||
String get onlineLayers => '在线图层';
|
||||
|
||||
@override
|
||||
String get offlineLayers => '离线图层';
|
||||
|
||||
@override
|
||||
String get locationTrail => '位置轨迹';
|
||||
|
||||
@@ -1380,47 +880,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get trailControls => '轨迹控制';
|
||||
|
||||
@override
|
||||
String get exportTrailToGpx => '导出轨迹到 GPX';
|
||||
|
||||
@override
|
||||
String get importTrailFromGpx => '从 GPX 导入轨迹';
|
||||
|
||||
@override
|
||||
String get trailExportedSuccessfully => '轨迹导出成功!';
|
||||
|
||||
@override
|
||||
String get failedToExportTrail => '导出轨迹失败';
|
||||
|
||||
@override
|
||||
String failedToImportTrail(String error) {
|
||||
return '导入轨迹失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get importTrail => '导入轨迹';
|
||||
|
||||
@override
|
||||
String importTrailQuestion(int pointCount) {
|
||||
return '导入包含 $pointCount 个点的轨迹吗?\n\n您可以替换当前轨迹,或将其与现有轨迹并排查看。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get viewAlongside => '并排查看';
|
||||
|
||||
@override
|
||||
String get replaceCurrent => '替换当前';
|
||||
|
||||
@override
|
||||
String trailImported(int pointCount) {
|
||||
return '轨迹已导入!($pointCount 个点)';
|
||||
}
|
||||
|
||||
@override
|
||||
String trailReplaced(int pointCount) {
|
||||
return '轨迹已替换!($pointCount 个点)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactTrails => '联系人轨迹';
|
||||
|
||||
@@ -1672,18 +1131,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get noRoomsFound => '未找到房间';
|
||||
|
||||
@override
|
||||
String get noContactsOrRoomsAvailable => '没有可用的联系人或房间';
|
||||
|
||||
@override
|
||||
String get noRecipientsAvailable => '没有可用的收件人';
|
||||
|
||||
@override
|
||||
String get noChannelsFound => '未找到频道';
|
||||
|
||||
@override
|
||||
String get messagesWillBeSentToPublicChannel => '消息将发送到公共频道';
|
||||
|
||||
@override
|
||||
String get newMessage => '新消息';
|
||||
|
||||
@@ -1855,9 +1308,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get rectangleDrawing => '矩形绘图';
|
||||
|
||||
@override
|
||||
String get coordinatesCopiedToClipboard => '坐标已复制到剪贴板';
|
||||
|
||||
@override
|
||||
String get manualCoordinates => '手动输入坐标';
|
||||
|
||||
@@ -1870,31 +1320,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get longitudeLabel => '经度';
|
||||
|
||||
@override
|
||||
String get invalidLatitude => '无效的纬度(-90 到 90)';
|
||||
|
||||
@override
|
||||
String get invalidLongitude => '无效的经度(-180 到 180)';
|
||||
|
||||
@override
|
||||
String get exampleCoordinates => '例如:46.0569, 14.5058';
|
||||
|
||||
@override
|
||||
String get drawingShared => '地图绘制';
|
||||
|
||||
@override
|
||||
String get drawingHidden => '绘图已从地图上隐藏';
|
||||
|
||||
@override
|
||||
String alreadyShared(int count) {
|
||||
return '$count 个已共享';
|
||||
}
|
||||
|
||||
@override
|
||||
String newDrawingsShared(int count, String plural) {
|
||||
return '已共享 $count 个新绘图';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareDrawing => '共享绘图';
|
||||
|
||||
@@ -1948,12 +1376,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get forestRoads => '森林道路';
|
||||
|
||||
@override
|
||||
String get showCadastralParcels => '显示地籍地块';
|
||||
|
||||
@override
|
||||
String get showForestRoads => '显示森林道路';
|
||||
|
||||
@override
|
||||
String get wmsOverlays => 'WMS 覆盖层';
|
||||
|
||||
@@ -2054,9 +1476,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
return '删除频道失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get allChannelSlotsInUse => '所有频道槽位都已在使用中(最多 39 个自定义频道)';
|
||||
|
||||
@override
|
||||
String get createChannel => '创建频道';
|
||||
|
||||
@@ -2095,19 +1514,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get wizardConnectingFeature3 => '完全离线工作 - 无需互联网';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeTitle => '简洁模式';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeDescription =>
|
||||
'刚接触 Mesh 网络?启用简洁模式以获得仅包含基本功能的简化界面。';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature1 => '对初学者友好的界面,包含核心功能';
|
||||
|
||||
@override
|
||||
String get wizardSimpleModeFeature2 => '随时在设置中切换到高级模式';
|
||||
|
||||
@override
|
||||
String get wizardChannelTitle => '频道';
|
||||
|
||||
|
||||
2280
lib/l10n/app_pl.arb
Normal file
2280
lib/l10n/app_pl.arb
Normal file
File diff suppressed because it is too large
Load Diff
2280
lib/l10n/app_pt.arb
Normal file
2280
lib/l10n/app_pt.arb
Normal file
File diff suppressed because it is too large
Load Diff
1174
lib/l10n/app_ru.arb
1174
lib/l10n/app_ru.arb
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2280
lib/l10n/app_tr.arb
Normal file
2280
lib/l10n/app_tr.arb
Normal file
File diff suppressed because it is too large
Load Diff
2280
lib/l10n/app_uk.arb
Normal file
2280
lib/l10n/app_uk.arb
Normal file
File diff suppressed because it is too large
Load Diff
1005
lib/l10n/app_zh.arb
1005
lib/l10n/app_zh.arb
File diff suppressed because it is too large
Load Diff
79
lib/models/message_route_metadata.dart
Normal file
79
lib/models/message_route_metadata.dart
Normal file
@@ -0,0 +1,79 @@
|
||||
import 'path_selection.dart';
|
||||
|
||||
class MessageRouteMetadata {
|
||||
final PathSelectionMode mode;
|
||||
final bool routerFallbackAttempted;
|
||||
final String? relayName;
|
||||
final String? relayKey6;
|
||||
final String? canonicalPath;
|
||||
final int? hopCount;
|
||||
|
||||
const MessageRouteMetadata({
|
||||
required this.mode,
|
||||
required this.routerFallbackAttempted,
|
||||
this.relayName,
|
||||
this.relayKey6,
|
||||
this.canonicalPath,
|
||||
this.hopCount,
|
||||
});
|
||||
|
||||
factory MessageRouteMetadata.fromSelection(
|
||||
PathSelection selection, {
|
||||
required bool routerFallbackAttempted,
|
||||
}) {
|
||||
return MessageRouteMetadata(
|
||||
mode: selection.mode,
|
||||
routerFallbackAttempted: routerFallbackAttempted,
|
||||
relayName: selection.relayName,
|
||||
relayKey6: selection.relayKey6,
|
||||
canonicalPath: selection.canonicalPath.isEmpty
|
||||
? null
|
||||
: selection.canonicalPath,
|
||||
hopCount: selection.hopCount > 0 ? selection.hopCount : null,
|
||||
);
|
||||
}
|
||||
|
||||
String get modeLabel {
|
||||
switch (mode) {
|
||||
case PathSelectionMode.directCurrent:
|
||||
return 'Current direct path';
|
||||
case PathSelectionMode.directHistorical:
|
||||
return 'Rotated direct path';
|
||||
case PathSelectionMode.flood:
|
||||
return 'Flood route';
|
||||
case PathSelectionMode.nearestRouter:
|
||||
final suffix = relayName?.trim().isNotEmpty == true
|
||||
? ' via $relayName'
|
||||
: relayKey6?.trim().isNotEmpty == true
|
||||
? ' via $relayKey6'
|
||||
: '';
|
||||
return 'Nearest router$suffix';
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'mode': mode.name,
|
||||
'router_fallback_attempted': routerFallbackAttempted,
|
||||
'relay_name': relayName,
|
||||
'relay_key6': relayKey6,
|
||||
'canonical_path': canonicalPath,
|
||||
'hop_count': hopCount,
|
||||
};
|
||||
}
|
||||
|
||||
factory MessageRouteMetadata.fromJson(Map<String, dynamic> json) {
|
||||
return MessageRouteMetadata(
|
||||
mode: PathSelectionMode.values.firstWhere(
|
||||
(value) => value.name == json['mode'],
|
||||
orElse: () => PathSelectionMode.directCurrent,
|
||||
),
|
||||
routerFallbackAttempted:
|
||||
json['router_fallback_attempted'] as bool? ?? false,
|
||||
relayName: json['relay_name'] as String?,
|
||||
relayKey6: json['relay_key6'] as String?,
|
||||
canonicalPath: json['canonical_path'] as String?,
|
||||
hopCount: json['hop_count'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
182
lib/models/path_history.dart
Normal file
182
lib/models/path_history.dart
Normal file
@@ -0,0 +1,182 @@
|
||||
class PathRecord {
|
||||
final List<int> pathBytes;
|
||||
final int hopCount;
|
||||
final int hashSize;
|
||||
final int successCount;
|
||||
final int failureCount;
|
||||
final int lastRoundTripTimeMs;
|
||||
final DateTime lastUsedAt;
|
||||
|
||||
const PathRecord({
|
||||
required this.pathBytes,
|
||||
required this.hopCount,
|
||||
required this.hashSize,
|
||||
required this.successCount,
|
||||
required this.failureCount,
|
||||
required this.lastRoundTripTimeMs,
|
||||
required this.lastUsedAt,
|
||||
});
|
||||
|
||||
String get signature =>
|
||||
pathBytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
||||
|
||||
double get successRate =>
|
||||
(successCount + 1) / (successCount + failureCount + 2);
|
||||
|
||||
PathRecord copyWith({
|
||||
List<int>? pathBytes,
|
||||
int? hopCount,
|
||||
int? hashSize,
|
||||
int? successCount,
|
||||
int? failureCount,
|
||||
int? lastRoundTripTimeMs,
|
||||
DateTime? lastUsedAt,
|
||||
}) {
|
||||
return PathRecord(
|
||||
pathBytes: pathBytes ?? this.pathBytes,
|
||||
hopCount: hopCount ?? this.hopCount,
|
||||
hashSize: hashSize ?? this.hashSize,
|
||||
successCount: successCount ?? this.successCount,
|
||||
failureCount: failureCount ?? this.failureCount,
|
||||
lastRoundTripTimeMs: lastRoundTripTimeMs ?? this.lastRoundTripTimeMs,
|
||||
lastUsedAt: lastUsedAt ?? this.lastUsedAt,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'path_bytes': pathBytes,
|
||||
'hop_count': hopCount,
|
||||
'hash_size': hashSize,
|
||||
'success_count': successCount,
|
||||
'failure_count': failureCount,
|
||||
'last_round_trip_time_ms': lastRoundTripTimeMs,
|
||||
'last_used_at': lastUsedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
factory PathRecord.fromJson(Map<String, dynamic> json) {
|
||||
return PathRecord(
|
||||
pathBytes: (json['path_bytes'] as List<dynamic>? ?? const <dynamic>[])
|
||||
.map((value) => value as int)
|
||||
.toList(),
|
||||
hopCount: json['hop_count'] as int? ?? 0,
|
||||
hashSize: json['hash_size'] as int? ?? 1,
|
||||
successCount: json['success_count'] as int? ?? 0,
|
||||
failureCount: json['failure_count'] as int? ?? 0,
|
||||
lastRoundTripTimeMs: json['last_round_trip_time_ms'] as int? ?? 0,
|
||||
lastUsedAt:
|
||||
DateTime.tryParse(json['last_used_at'] as String? ?? '') ??
|
||||
DateTime.fromMillisecondsSinceEpoch(0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FloodPathStats {
|
||||
final int successCount;
|
||||
final int failureCount;
|
||||
final int lastRoundTripTimeMs;
|
||||
final DateTime? lastUsedAt;
|
||||
|
||||
const FloodPathStats({
|
||||
required this.successCount,
|
||||
required this.failureCount,
|
||||
required this.lastRoundTripTimeMs,
|
||||
required this.lastUsedAt,
|
||||
});
|
||||
|
||||
const FloodPathStats.empty()
|
||||
: successCount = 0,
|
||||
failureCount = 0,
|
||||
lastRoundTripTimeMs = 0,
|
||||
lastUsedAt = null;
|
||||
|
||||
FloodPathStats copyWith({
|
||||
int? successCount,
|
||||
int? failureCount,
|
||||
int? lastRoundTripTimeMs,
|
||||
DateTime? lastUsedAt,
|
||||
}) {
|
||||
return FloodPathStats(
|
||||
successCount: successCount ?? this.successCount,
|
||||
failureCount: failureCount ?? this.failureCount,
|
||||
lastRoundTripTimeMs: lastRoundTripTimeMs ?? this.lastRoundTripTimeMs,
|
||||
lastUsedAt: lastUsedAt ?? this.lastUsedAt,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'success_count': successCount,
|
||||
'failure_count': failureCount,
|
||||
'last_round_trip_time_ms': lastRoundTripTimeMs,
|
||||
'last_used_at': lastUsedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
factory FloodPathStats.fromJson(Map<String, dynamic> json) {
|
||||
return FloodPathStats(
|
||||
successCount: json['success_count'] as int? ?? 0,
|
||||
failureCount: json['failure_count'] as int? ?? 0,
|
||||
lastRoundTripTimeMs: json['last_round_trip_time_ms'] as int? ?? 0,
|
||||
lastUsedAt: DateTime.tryParse(json['last_used_at'] as String? ?? ''),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContactPathHistory {
|
||||
final String contactPublicKeyHex;
|
||||
final List<PathRecord> directPaths;
|
||||
final FloodPathStats floodStats;
|
||||
final int rotationIndex;
|
||||
|
||||
const ContactPathHistory({
|
||||
required this.contactPublicKeyHex,
|
||||
required this.directPaths,
|
||||
required this.floodStats,
|
||||
required this.rotationIndex,
|
||||
});
|
||||
|
||||
const ContactPathHistory.empty(this.contactPublicKeyHex)
|
||||
: directPaths = const <PathRecord>[],
|
||||
floodStats = const FloodPathStats.empty(),
|
||||
rotationIndex = 0;
|
||||
|
||||
ContactPathHistory copyWith({
|
||||
List<PathRecord>? directPaths,
|
||||
FloodPathStats? floodStats,
|
||||
int? rotationIndex,
|
||||
}) {
|
||||
return ContactPathHistory(
|
||||
contactPublicKeyHex: contactPublicKeyHex,
|
||||
directPaths: directPaths ?? this.directPaths,
|
||||
floodStats: floodStats ?? this.floodStats,
|
||||
rotationIndex: rotationIndex ?? this.rotationIndex,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'direct_paths': directPaths.map((record) => record.toJson()).toList(),
|
||||
'flood_stats': floodStats.toJson(),
|
||||
'rotation_index': rotationIndex,
|
||||
};
|
||||
}
|
||||
|
||||
factory ContactPathHistory.fromJson(
|
||||
String contactPublicKeyHex,
|
||||
Map<String, dynamic> json,
|
||||
) {
|
||||
return ContactPathHistory(
|
||||
contactPublicKeyHex: contactPublicKeyHex,
|
||||
directPaths: (json['direct_paths'] as List<dynamic>? ?? const <dynamic>[])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(PathRecord.fromJson)
|
||||
.toList(),
|
||||
floodStats: json['flood_stats'] is Map<String, dynamic>
|
||||
? FloodPathStats.fromJson(json['flood_stats'] as Map<String, dynamic>)
|
||||
: const FloodPathStats.empty(),
|
||||
rotationIndex: json['rotation_index'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
65
lib/models/path_selection.dart
Normal file
65
lib/models/path_selection.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
enum PathSelectionMode { directCurrent, directHistorical, flood, nearestRouter }
|
||||
|
||||
class PathSelection {
|
||||
final PathSelectionMode mode;
|
||||
final Uint8List pathBytes;
|
||||
final int hopCount;
|
||||
final int hashSize;
|
||||
final String? relayName;
|
||||
final String? relayKey6;
|
||||
|
||||
const PathSelection({
|
||||
required this.mode,
|
||||
required this.pathBytes,
|
||||
required this.hopCount,
|
||||
required this.hashSize,
|
||||
this.relayName,
|
||||
this.relayKey6,
|
||||
});
|
||||
|
||||
PathSelection.flood()
|
||||
: mode = PathSelectionMode.flood,
|
||||
pathBytes = Uint8List(0),
|
||||
hopCount = -1,
|
||||
hashSize = 1,
|
||||
relayName = null,
|
||||
relayKey6 = null;
|
||||
|
||||
bool get usesFlood => mode == PathSelectionMode.flood;
|
||||
bool get hasDirectPath => !usesFlood && pathBytes.isNotEmpty && hopCount > 0;
|
||||
|
||||
String get canonicalPath {
|
||||
if (!hasDirectPath) return '';
|
||||
final hops = <String>[];
|
||||
for (var index = 0; index < pathBytes.length; index += hashSize) {
|
||||
hops.add(
|
||||
pathBytes
|
||||
.sublist(index, index + hashSize)
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.join()
|
||||
.toUpperCase(),
|
||||
);
|
||||
}
|
||||
return hops.join(',');
|
||||
}
|
||||
|
||||
PathSelection copyWith({
|
||||
PathSelectionMode? mode,
|
||||
Uint8List? pathBytes,
|
||||
int? hopCount,
|
||||
int? hashSize,
|
||||
String? relayName,
|
||||
String? relayKey6,
|
||||
}) {
|
||||
return PathSelection(
|
||||
mode: mode ?? this.mode,
|
||||
pathBytes: pathBytes ?? this.pathBytes,
|
||||
hopCount: hopCount ?? this.hopCount,
|
||||
hashSize: hashSize ?? this.hashSize,
|
||||
relayName: relayName ?? this.relayName,
|
||||
relayKey6: relayKey6 ?? this.relayKey6,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,15 @@ import 'image_provider.dart' as ip;
|
||||
import 'helpers/fragment_ack_wait_registry.dart';
|
||||
import 'helpers/session_metadata_restore.dart';
|
||||
import '../services/location_tracking_service.dart';
|
||||
import '../services/messaging_route_preferences.dart';
|
||||
import '../services/nearest_router_selector.dart';
|
||||
import '../services/packet_capture_storage_service.dart';
|
||||
import '../services/path_history_service.dart';
|
||||
import '../services/route_hash_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/ble_packet_log.dart';
|
||||
import '../models/path_selection.dart';
|
||||
import '../models/message_reception_details.dart';
|
||||
import '../utils/drawing_message_parser.dart';
|
||||
import '../utils/raw_route_probe.dart';
|
||||
@@ -25,6 +30,31 @@ import '../utils/media_swarm_protocol.dart';
|
||||
import '../utils/message_airtime_estimator.dart';
|
||||
import '../utils/fast_gps_packet.dart';
|
||||
|
||||
class _DirectMessageRouteSession {
|
||||
final PathSelection currentSelection;
|
||||
final ParsedContactRoute? originalRoute;
|
||||
final bool routerFallbackAttempted;
|
||||
|
||||
const _DirectMessageRouteSession({
|
||||
required this.currentSelection,
|
||||
required this.originalRoute,
|
||||
required this.routerFallbackAttempted,
|
||||
});
|
||||
|
||||
_DirectMessageRouteSession copyWith({
|
||||
PathSelection? currentSelection,
|
||||
ParsedContactRoute? originalRoute,
|
||||
bool? routerFallbackAttempted,
|
||||
}) {
|
||||
return _DirectMessageRouteSession(
|
||||
currentSelection: currentSelection ?? this.currentSelection,
|
||||
originalRoute: originalRoute ?? this.originalRoute,
|
||||
routerFallbackAttempted:
|
||||
routerFallbackAttempted ?? this.routerFallbackAttempted,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Main App Provider - coordinates all other providers
|
||||
class AppProvider with ChangeNotifier {
|
||||
static const int _maxDirectPayloadHops = 3;
|
||||
@@ -60,8 +90,24 @@ class AppProvider with ChangeNotifier {
|
||||
bool get isVoiceCompressorEnabled => _isVoiceCompressorEnabled;
|
||||
bool _isVoiceLimiterEnabled = true;
|
||||
bool get isVoiceLimiterEnabled => _isVoiceLimiterEnabled;
|
||||
double _messageFontScale = 1.0;
|
||||
double get messageFontScale => _messageFontScale;
|
||||
bool _autoAddDiscoveredContacts = false;
|
||||
bool get autoAddDiscoveredContacts => _autoAddDiscoveredContacts;
|
||||
bool _autoRouteRotationEnabled =
|
||||
MessagingRoutePreferences.defaultAutoRouteRotationEnabled;
|
||||
bool get autoRouteRotationEnabled => _autoRouteRotationEnabled;
|
||||
bool _clearPathOnMaxRetry =
|
||||
MessagingRoutePreferences.defaultClearPathOnMaxRetry;
|
||||
bool get clearPathOnMaxRetry => _clearPathOnMaxRetry;
|
||||
bool _nearestRelayFallbackEnabled =
|
||||
MessagingRoutePreferences.defaultNearestRelayFallbackEnabled;
|
||||
bool get nearestRelayFallbackEnabled => _nearestRelayFallbackEnabled;
|
||||
final PathHistoryService _pathHistoryService = PathHistoryService();
|
||||
final NearestRouterSelector _nearestRouterSelector =
|
||||
const NearestRouterSelector();
|
||||
final Map<String, _DirectMessageRouteSession> _directMessageRouteSessions =
|
||||
{};
|
||||
|
||||
static const Duration _packetRetryDelay = Duration(milliseconds: 1200);
|
||||
static const Duration _mediaSwarmResponseWindow = Duration(seconds: 10);
|
||||
@@ -100,7 +146,10 @@ class AppProvider with ChangeNotifier {
|
||||
_loadVoiceBandPassFilterEnabled();
|
||||
_loadVoiceCompressorEnabled();
|
||||
_loadVoiceLimiterEnabled();
|
||||
_loadMessageFontScale();
|
||||
_loadAutoAddDiscoveredContacts();
|
||||
_loadMessagingRouteSettings();
|
||||
unawaited(_pathHistoryService.initialize());
|
||||
_startPacketCapturePersistence();
|
||||
_syncDrawingsOnStartup(); // Sync drawings immediately after providers load
|
||||
_isInitialized = true;
|
||||
@@ -385,6 +434,30 @@ class AppProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMessageFontScale() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_messageFontScale = (prefs.getDouble('message_font_scale') ?? 1.0).clamp(
|
||||
0.85,
|
||||
1.4,
|
||||
);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error loading message font scale setting: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setMessageFontScale(double scale) async {
|
||||
try {
|
||||
_messageFontScale = scale.clamp(0.85, 1.4);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setDouble('message_font_scale', _messageFontScale);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error saving message font scale setting: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Load auto-add discovered contacts setting from shared preferences.
|
||||
Future<void> _loadAutoAddDiscoveredContacts() async {
|
||||
try {
|
||||
@@ -409,6 +482,50 @@ class AppProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMessagingRouteSettings() async {
|
||||
try {
|
||||
_autoRouteRotationEnabled =
|
||||
await MessagingRoutePreferences.getAutoRouteRotationEnabled();
|
||||
_clearPathOnMaxRetry =
|
||||
await MessagingRoutePreferences.getClearPathOnMaxRetry();
|
||||
_nearestRelayFallbackEnabled =
|
||||
await MessagingRoutePreferences.getNearestRelayFallbackEnabled();
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error loading messaging route settings: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleAutoRouteRotationEnabled(bool enabled) async {
|
||||
try {
|
||||
_autoRouteRotationEnabled = enabled;
|
||||
await MessagingRoutePreferences.setAutoRouteRotationEnabled(enabled);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error saving auto route rotation setting: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleClearPathOnMaxRetry(bool enabled) async {
|
||||
try {
|
||||
_clearPathOnMaxRetry = enabled;
|
||||
await MessagingRoutePreferences.setClearPathOnMaxRetry(enabled);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error saving clear path on max retry setting: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleNearestRelayFallbackEnabled(bool enabled) async {
|
||||
try {
|
||||
_nearestRelayFallbackEnabled = enabled;
|
||||
await MessagingRoutePreferences.setNearestRelayFallbackEnabled(enabled);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('Error saving nearest relay fallback setting: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize location tracking service
|
||||
Future<void> _initializeLocationTracking() async {
|
||||
try {
|
||||
@@ -488,6 +605,7 @@ class AppProvider with ChangeNotifier {
|
||||
contact,
|
||||
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||
);
|
||||
unawaited(_pathHistoryService.recordLearnedPath(contact));
|
||||
|
||||
// Broadcast to SSE clients if server is running
|
||||
connectionProvider.broadcastContactToSseClients(contact);
|
||||
@@ -500,6 +618,9 @@ class AppProvider with ChangeNotifier {
|
||||
contacts,
|
||||
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||
);
|
||||
for (final contact in contacts) {
|
||||
unawaited(_pathHistoryService.recordLearnedPath(contact));
|
||||
}
|
||||
debugPrint('Received ${contacts.length} contacts');
|
||||
|
||||
// Broadcast all contacts to SSE clients if server is running
|
||||
@@ -634,6 +755,19 @@ class AppProvider with ChangeNotifier {
|
||||
final receptionDetailsSnapshot = _buildReceptionDetailsSnapshot(
|
||||
enrichedMessage,
|
||||
);
|
||||
final receivedPathBytes = receptionDetailsSnapshot?.pathBytes;
|
||||
if (senderContact != null &&
|
||||
enrichedMessage.isChannelMessage &&
|
||||
(enrichedMessage.channelIdx ?? 0) == 0 &&
|
||||
receivedPathBytes != null &&
|
||||
receivedPathBytes.isNotEmpty) {
|
||||
unawaited(
|
||||
_learnPathFromPublicMessage(
|
||||
contact: senderContact,
|
||||
pathBytes: receivedPathBytes,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Check if message is a drawing broadcast
|
||||
if (DrawingMessageParser.isDrawingMessage(enrichedMessage.text)) {
|
||||
@@ -1117,6 +1251,14 @@ class AppProvider with ChangeNotifier {
|
||||
messagesProvider.handleMessageEcho(messageId, echoCount, snrRaw, rssiDbm);
|
||||
};
|
||||
|
||||
connectionProvider.prepareDirectMessageSendCallback =
|
||||
({required messageId, required contact, required retryAttempt}) async {
|
||||
return _prepareDirectMessageSend(
|
||||
messageId: messageId,
|
||||
contact: contact,
|
||||
);
|
||||
};
|
||||
|
||||
// Wire up MessagesProvider's sendMessageCallback for retry logic
|
||||
messagesProvider.sendMessageCallback =
|
||||
({
|
||||
@@ -1134,32 +1276,310 @@ class AppProvider with ChangeNotifier {
|
||||
retryAttempt: retryAttempt,
|
||||
);
|
||||
};
|
||||
|
||||
messagesProvider.onDirectPathFailedCallback =
|
||||
({required contact, required failureStreak}) async {
|
||||
debugPrint(
|
||||
'🧭 [AppProvider] Clearing unhealthy path for ${contact.advName} after $failureStreak failed send chain(s)',
|
||||
messagesProvider.onFinalRouterFallbackCallback =
|
||||
({required messageId, required contact, required message}) async {
|
||||
return _sendWithFinalNearestRouterFallback(
|
||||
messageId: messageId,
|
||||
contact: contact,
|
||||
message: message,
|
||||
);
|
||||
|
||||
contactsProvider.markPathUnhealthy(contact.publicKey);
|
||||
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await connectionProvider.resetPath(contact.publicKey);
|
||||
Future.delayed(const Duration(milliseconds: 150), () {
|
||||
if (connectionProvider.deviceInfo.isConnected) {
|
||||
connectionProvider.getContact(contact.publicKey);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'⚠️ [AppProvider] Failed to reset path for ${contact.advName}: $e',
|
||||
);
|
||||
}
|
||||
};
|
||||
messagesProvider.onFinalDirectMessageFailureCallback =
|
||||
({required messageId, required contact, required message}) async {
|
||||
await _handleDirectMessageFinalFailure(
|
||||
messageId: messageId,
|
||||
contact: contact,
|
||||
);
|
||||
};
|
||||
messagesProvider.onDirectMessageDeliveredCallback =
|
||||
({
|
||||
required messageId,
|
||||
required contact,
|
||||
required message,
|
||||
required roundTripTimeMs,
|
||||
}) {
|
||||
_handleDirectMessageDelivered(
|
||||
messageId: messageId,
|
||||
contact: contact,
|
||||
roundTripTimeMs: roundTripTimeMs,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Future<Contact> _prepareDirectMessageSend({
|
||||
required String messageId,
|
||||
required Contact contact,
|
||||
}) async {
|
||||
final latestContact =
|
||||
contactsProvider.findContactByKey(contact.publicKey) ?? contact;
|
||||
var session = _directMessageRouteSessions[messageId];
|
||||
if (session == null) {
|
||||
final selection = await _pathHistoryService.getSelectionForContact(
|
||||
latestContact,
|
||||
autoRouteRotationEnabled: _autoRouteRotationEnabled,
|
||||
);
|
||||
session = _DirectMessageRouteSession(
|
||||
currentSelection: selection,
|
||||
originalRoute: ContactRouteCodec.fromContact(latestContact),
|
||||
routerFallbackAttempted: false,
|
||||
);
|
||||
_directMessageRouteSessions[messageId] = session;
|
||||
}
|
||||
|
||||
await _applyPathSelection(
|
||||
latestContact,
|
||||
session.currentSelection,
|
||||
messageId: messageId,
|
||||
routerFallbackAttempted: session.routerFallbackAttempted,
|
||||
);
|
||||
return contactsProvider.findContactByKey(contact.publicKey) ??
|
||||
latestContact;
|
||||
}
|
||||
|
||||
Future<void> _applyPathSelection(
|
||||
Contact contact,
|
||||
PathSelection selection, {
|
||||
required String messageId,
|
||||
required bool routerFallbackAttempted,
|
||||
}) async {
|
||||
final previousRoute = ContactRouteCodec.fromContact(contact);
|
||||
|
||||
try {
|
||||
if (selection.usesFlood) {
|
||||
contactsProvider.resetContactRouteLocal(contact.publicKey);
|
||||
if (connectionProvider.deviceInfo.isConnected) {
|
||||
await connectionProvider.resetPath(contact.publicKey);
|
||||
}
|
||||
} else {
|
||||
final pathDescriptor =
|
||||
((selection.hashSize - 1) << 6) | (selection.hopCount & 0x3F);
|
||||
final signedDescriptor = ContactRouteCodec.toSignedDescriptor(
|
||||
pathDescriptor,
|
||||
);
|
||||
final paddedPathBytes = Uint8List(ContactRouteCodec.maxPathBytes)
|
||||
..setRange(0, selection.pathBytes.length, selection.pathBytes);
|
||||
|
||||
contactsProvider.setContactRouteLocal(
|
||||
contact.publicKey,
|
||||
signedEncodedPathLen: signedDescriptor,
|
||||
paddedPathBytes: paddedPathBytes,
|
||||
);
|
||||
if (connectionProvider.deviceInfo.isConnected) {
|
||||
await connectionProvider.setContactRoute(
|
||||
contact,
|
||||
signedEncodedPathLen: signedDescriptor,
|
||||
paddedPathBytes: paddedPathBytes,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
_restoreRouteLocal(contact.publicKey, previousRoute);
|
||||
rethrow;
|
||||
}
|
||||
|
||||
messagesProvider.updateMessageRouteSelection(
|
||||
messageId,
|
||||
selection,
|
||||
routerFallbackAttempted: routerFallbackAttempted,
|
||||
);
|
||||
}
|
||||
|
||||
void _restoreRouteLocal(Uint8List publicKey, ParsedContactRoute? route) {
|
||||
if (route == null) {
|
||||
contactsProvider.resetContactRouteLocal(publicKey);
|
||||
return;
|
||||
}
|
||||
|
||||
contactsProvider.setContactRouteLocal(
|
||||
publicKey,
|
||||
signedEncodedPathLen: route.signedEncodedPathLen,
|
||||
paddedPathBytes: route.paddedPathBytes,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _restoreRouteOnDevice(
|
||||
Contact contact,
|
||||
ParsedContactRoute? route,
|
||||
) async {
|
||||
_restoreRouteLocal(contact.publicKey, route);
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (route == null) {
|
||||
await connectionProvider.resetPath(contact.publicKey);
|
||||
return;
|
||||
}
|
||||
|
||||
await connectionProvider.setContactRoute(
|
||||
contact,
|
||||
signedEncodedPathLen: route.signedEncodedPathLen,
|
||||
paddedPathBytes: route.paddedPathBytes,
|
||||
);
|
||||
}
|
||||
|
||||
PathSelection _buildNearestRouterSelection(Contact repeater, int hashSize) {
|
||||
return PathSelection(
|
||||
mode: PathSelectionMode.nearestRouter,
|
||||
pathBytes: Uint8List.fromList(repeater.publicKey.sublist(0, hashSize)),
|
||||
hopCount: 1,
|
||||
hashSize: hashSize,
|
||||
relayName: repeater.advName,
|
||||
relayKey6: _key6(repeater.publicKey),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _sendWithFinalNearestRouterFallback({
|
||||
required String messageId,
|
||||
required Contact contact,
|
||||
required Message message,
|
||||
}) async {
|
||||
if (!_nearestRelayFallbackEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final latestContact =
|
||||
contactsProvider.findContactByKey(contact.publicKey) ?? contact;
|
||||
final session =
|
||||
_directMessageRouteSessions[messageId] ??
|
||||
_DirectMessageRouteSession(
|
||||
currentSelection: latestContact.routeHasPath
|
||||
? PathSelection(
|
||||
mode: PathSelectionMode.directCurrent,
|
||||
pathBytes: Uint8List.fromList(latestContact.routePathBytes),
|
||||
hopCount: latestContact.routeHopCount,
|
||||
hashSize: latestContact.routeHashSize,
|
||||
)
|
||||
: PathSelection.flood(),
|
||||
originalRoute: ContactRouteCodec.fromContact(latestContact),
|
||||
routerFallbackAttempted: false,
|
||||
);
|
||||
|
||||
await _pathHistoryService.recordPathResult(
|
||||
latestContact.publicKeyHex,
|
||||
session.currentSelection,
|
||||
success: false,
|
||||
);
|
||||
|
||||
final repeater = _nearestRouterSelector.select(
|
||||
senderPosition: locationTrackingService.currentPosition,
|
||||
repeaters: contactsProvider.repeaters,
|
||||
recipient: latestContact,
|
||||
);
|
||||
if (repeater == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final routeHashSize = await RouteHashPreferences.getHashSize();
|
||||
final fallbackSelection = _buildNearestRouterSelection(
|
||||
repeater,
|
||||
routeHashSize,
|
||||
);
|
||||
_directMessageRouteSessions[messageId] = session.copyWith(
|
||||
currentSelection: fallbackSelection,
|
||||
routerFallbackAttempted: true,
|
||||
);
|
||||
messagesProvider.updateMessageRouteSelection(
|
||||
messageId,
|
||||
fallbackSelection,
|
||||
routerFallbackAttempted: true,
|
||||
);
|
||||
|
||||
return connectionProvider.sendTextMessage(
|
||||
contactPublicKey: latestContact.publicKey,
|
||||
text: message.text,
|
||||
messageId: messageId,
|
||||
contact: latestContact,
|
||||
retryAttempt: message.retryAttempt + 1,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleDirectMessageDelivered({
|
||||
required String messageId,
|
||||
required Contact contact,
|
||||
required int roundTripTimeMs,
|
||||
}) {
|
||||
final session = _directMessageRouteSessions.remove(messageId);
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
unawaited(
|
||||
_pathHistoryService.recordPathResult(
|
||||
contact.publicKeyHex,
|
||||
session.currentSelection,
|
||||
success: true,
|
||||
roundTripTimeMs: roundTripTimeMs,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleDirectMessageFinalFailure({
|
||||
required String messageId,
|
||||
required Contact contact,
|
||||
}) async {
|
||||
final latestContact =
|
||||
contactsProvider.findContactByKey(contact.publicKey) ?? contact;
|
||||
final session = _directMessageRouteSessions.remove(messageId);
|
||||
if (session != null) {
|
||||
await _pathHistoryService.recordPathResult(
|
||||
latestContact.publicKeyHex,
|
||||
session.currentSelection,
|
||||
success: false,
|
||||
);
|
||||
if (session.routerFallbackAttempted) {
|
||||
await _restoreRouteOnDevice(latestContact, session.originalRoute);
|
||||
}
|
||||
}
|
||||
|
||||
if (_clearPathOnMaxRetry) {
|
||||
contactsProvider.resetContactRouteLocal(latestContact.publicKey);
|
||||
if (connectionProvider.deviceInfo.isConnected) {
|
||||
await connectionProvider.resetPath(latestContact.publicKey);
|
||||
Future.delayed(const Duration(milliseconds: 150), () {
|
||||
if (connectionProvider.deviceInfo.isConnected) {
|
||||
connectionProvider.getContact(latestContact.publicKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _key6(Uint8List publicKey) {
|
||||
final bytes = publicKey.sublist(0, math.min(6, publicKey.length));
|
||||
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
|
||||
Future<void> _learnPathFromPublicMessage({
|
||||
required Contact contact,
|
||||
required List<int> pathBytes,
|
||||
}) async {
|
||||
final preferred = await RouteHashPreferences.getHashSize();
|
||||
final inferredHashSize = _inferReceivedPathHashSize(
|
||||
pathBytes,
|
||||
preferredHashSize: preferred,
|
||||
);
|
||||
await _pathHistoryService.recordReceivedBytePath(
|
||||
contact.publicKeyHex,
|
||||
pathBytes,
|
||||
inferredHashSize,
|
||||
);
|
||||
}
|
||||
|
||||
int _inferReceivedPathHashSize(
|
||||
List<int> pathBytes, {
|
||||
required int preferredHashSize,
|
||||
}) {
|
||||
final preferred = preferredHashSize;
|
||||
final candidates = {preferred, 3, 2, 1}.toList();
|
||||
for (final candidate in candidates) {
|
||||
if (candidate >= 1 &&
|
||||
candidate <= 3 &&
|
||||
pathBytes.length % candidate == 0) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/// Initialize the app (load contacts, sync time, etc.)
|
||||
|
||||
@@ -175,6 +175,12 @@ class ConnectionProvider with ChangeNotifier {
|
||||
Function(String messageId, int expectedAckTag, int suggestedTimeoutMs)?
|
||||
onMessageSent;
|
||||
Function(int ackCode, int roundTripTimeMs)? onMessageDelivered;
|
||||
Future<Contact?> Function({
|
||||
required String messageId,
|
||||
required Contact contact,
|
||||
required int retryAttempt,
|
||||
})?
|
||||
prepareDirectMessageSendCallback;
|
||||
Function(String messageId, int echoCount, int snrRaw, int rssiDbm)?
|
||||
onMessageEchoDetected;
|
||||
Function(Uint8List publicKeyPrefix, Uint8List statusData)? onStatusResponse;
|
||||
@@ -1073,7 +1079,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
///
|
||||
/// [messageId] - optional message ID to track delivery status
|
||||
/// [contact] - optional contact object for path status logging
|
||||
/// [retryAttempt] - retry attempt number (0 = first send, 1-3 = retries)
|
||||
/// [retryAttempt] - retry attempt number (0 = first send, >0 = retries)
|
||||
Future<bool> sendTextMessage({
|
||||
required Uint8List contactPublicKey,
|
||||
required String text,
|
||||
@@ -1087,6 +1093,17 @@ class ConnectionProvider with ChangeNotifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
var effectiveContact = contact;
|
||||
if (messageId != null &&
|
||||
effectiveContact != null &&
|
||||
prepareDirectMessageSendCallback != null) {
|
||||
effectiveContact = await prepareDirectMessageSendCallback!(
|
||||
messageId: messageId,
|
||||
contact: effectiveContact,
|
||||
retryAttempt: retryAttempt,
|
||||
);
|
||||
}
|
||||
|
||||
// CRITICAL: Check firmware ACK limit (8 max in circular buffer)
|
||||
// Rate limit at 7 to stay under the limit
|
||||
if (_messageDeliveryTracker.shouldRateLimit) {
|
||||
@@ -1111,33 +1128,33 @@ class ConnectionProvider with ChangeNotifier {
|
||||
|
||||
try {
|
||||
// Log path status and retry info
|
||||
if (contact != null) {
|
||||
if (effectiveContact != null) {
|
||||
if (retryAttempt > 0) {
|
||||
debugPrint(
|
||||
'🔄 [ConnectionProvider] Sending message to ${contact.advName} (retry $retryAttempt/3)',
|
||||
'🔄 [ConnectionProvider] Sending message to ${effectiveContact.advName} (retry $retryAttempt)',
|
||||
);
|
||||
} else {
|
||||
debugPrint(
|
||||
'📤 [ConnectionProvider] Sending message to ${contact.advName}',
|
||||
'📤 [ConnectionProvider] Sending message to ${effectiveContact.advName}',
|
||||
);
|
||||
}
|
||||
debugPrint(' Type: ${contact.type.displayName}');
|
||||
debugPrint(' Path status: ${contact.routeSummary}');
|
||||
if (contact.routeHasPath) {
|
||||
debugPrint(' Type: ${effectiveContact.type.displayName}');
|
||||
debugPrint(' Path status: ${effectiveContact.routeSummary}');
|
||||
if (effectiveContact.routeHasPath) {
|
||||
debugPrint(
|
||||
' ✅ Using learned path (${contact.routeHopCount} hop(s), ${contact.routeHashSize}-byte hashes)',
|
||||
' ✅ Using learned path (${effectiveContact.routeHopCount} hop(s), ${effectiveContact.routeHashSize}-byte hashes)',
|
||||
);
|
||||
} else {
|
||||
debugPrint(' ⚠️ No path available - will use flood mode');
|
||||
}
|
||||
} else if (retryAttempt > 0) {
|
||||
debugPrint(
|
||||
'🔄 [ConnectionProvider] Sending message (retry $retryAttempt/3)',
|
||||
'🔄 [ConnectionProvider] Sending message (retry $retryAttempt)',
|
||||
);
|
||||
}
|
||||
|
||||
// Track pending operation for auto-recovery (if contact not found in radio)
|
||||
if (contact != null) {
|
||||
if (effectiveContact != null) {
|
||||
final operationId = contactPublicKey
|
||||
.sublist(0, 6)
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
@@ -1146,7 +1163,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
contactPublicKey: contactPublicKey,
|
||||
text: text,
|
||||
messageId: messageId,
|
||||
contact: contact,
|
||||
contact: effectiveContact,
|
||||
retryAttempt: retryAttempt,
|
||||
);
|
||||
debugPrint(
|
||||
@@ -1191,7 +1208,7 @@ class ConnectionProvider with ChangeNotifier {
|
||||
|
||||
// Clear pending operation after successful send (no error)
|
||||
// If ERR_CODE_NOT_FOUND occurs, the operation will be recovered automatically
|
||||
if (contact != null) {
|
||||
if (effectiveContact != null) {
|
||||
final operationId = contactPublicKey
|
||||
.sublist(0, 6)
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
|
||||
@@ -5,15 +5,13 @@ import '../../models/contact.dart';
|
||||
|
||||
/// Manages message retry state and logic
|
||||
///
|
||||
/// This helper class centralizes retry logic for direct messages, implementing
|
||||
/// a progressive timeout strategy (4s, 8s, 12s) for messages sent to contacts
|
||||
/// with learned routing paths.
|
||||
/// This helper class centralizes retry logic for direct messages.
|
||||
///
|
||||
/// IMPORTANT: Based on MeshCore firmware analysis:
|
||||
/// - Firmware calculates timeout based on path length and airtime
|
||||
/// - Direct mode: ~(path_len * airtime * 2) + margin
|
||||
/// - Flood mode: ~10-30 seconds for multi-hop
|
||||
/// - Our timeouts (4s, 8s, 12s) are conservative for direct paths
|
||||
/// - Our retry delays (1s, 2s, 4s, 8s) are app-level backoff timers
|
||||
/// - Firmware does NOT automatically retry - app must implement
|
||||
class MessageRetryManager {
|
||||
// Track retry state for each message ID
|
||||
@@ -21,10 +19,10 @@ class MessageRetryManager {
|
||||
final Map<String, DateTime> _lastRetryTimes = {};
|
||||
final Map<String, int> _pathFailureStreaks = {};
|
||||
|
||||
// Progressive timeout values in milliseconds
|
||||
// These are app-level timeouts, separate from firmware's suggested timeout
|
||||
// Firmware timeout is for ACK arrival, these are for retry attempts
|
||||
static const List<int> _timeouts = [4000, 8000, 12000];
|
||||
static const int maxRetryAttempts = 4;
|
||||
|
||||
// Retry backoff values in milliseconds.
|
||||
static const List<int> _retryDelays = [1000, 2000, 4000, 8000];
|
||||
static const int _defaultLoRaSf = 10;
|
||||
static const int _defaultLoRaCr = 5;
|
||||
static const int _defaultLoRaBwHz = 250000;
|
||||
@@ -32,13 +30,12 @@ class MessageRetryManager {
|
||||
static const int _defaultLoRaCrcEnabled = 1;
|
||||
static const int _defaultLoRaExplicitHeader = 1;
|
||||
|
||||
/// Get timeout for a specific retry attempt (0-2)
|
||||
/// Returns: 4000ms for attempt 0, 8000ms for attempt 1, 12000ms for attempt 2
|
||||
int getTimeoutForAttempt(int attempt) {
|
||||
if (attempt < 0 || attempt >= _timeouts.length) {
|
||||
return _timeouts.last; // Default to last timeout if out of range
|
||||
/// Get backoff delay for the next retry attempt.
|
||||
int getDelayForAttempt(int attempt) {
|
||||
if (attempt < 0 || attempt >= _retryDelays.length) {
|
||||
return _retryDelays.last;
|
||||
}
|
||||
return _timeouts[attempt];
|
||||
return _retryDelays[attempt];
|
||||
}
|
||||
|
||||
/// Calculate a conservative delivery-ACK timeout when firmware doesn't
|
||||
@@ -65,43 +62,8 @@ class MessageRetryManager {
|
||||
return ((airtimeMs * (hopCount + 1) * 2) + 1500).clamp(4000, 20000);
|
||||
}
|
||||
|
||||
/// Check if a message is eligible for retry
|
||||
///
|
||||
/// Returns true if:
|
||||
/// - The message has retryAttempt < 3
|
||||
/// - The contact has a learned path (contact.hasPath == true)
|
||||
/// - The message hasn't used flood fallback yet
|
||||
///
|
||||
/// Messages to contacts without paths should NOT retry (flood mode already broadcasts)
|
||||
bool canRetry(Message message, Contact contact) {
|
||||
// Never retry if already tried flood mode
|
||||
if (message.usedFloodFallback) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Never retry beyond 3 attempts
|
||||
if (message.retryAttempt >= 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only retry if contact has a learned path
|
||||
// If no path, the device uses flood mode automatically - retrying won't help
|
||||
return contact.routeHasPath;
|
||||
}
|
||||
|
||||
/// Check if should fall back to flood mode
|
||||
///
|
||||
/// Returns true if:
|
||||
/// - Message has exhausted all 3 retry attempts with direct mode
|
||||
/// - Contact HAS a learned path (so direct mode was used)
|
||||
/// - Hasn't already used flood fallback
|
||||
///
|
||||
/// IMPORTANT: Only contacts WITH paths need flood fallback.
|
||||
/// Contacts without paths already use flood mode automatically.
|
||||
bool shouldUseFloodFallback(Message message, Contact contact) {
|
||||
return message.retryAttempt >= 3 &&
|
||||
contact.routeHasPath &&
|
||||
!message.usedFloodFallback;
|
||||
return message.retryAttempt < maxRetryAttempts;
|
||||
}
|
||||
|
||||
/// Track a retry attempt for a message
|
||||
|
||||
@@ -5,6 +5,8 @@ import '../models/contact.dart';
|
||||
import '../models/message_contact_location.dart';
|
||||
import '../models/message_reception_details.dart';
|
||||
import '../models/message_transfer_details.dart';
|
||||
import '../models/message_route_metadata.dart';
|
||||
import '../models/path_selection.dart';
|
||||
import '../models/sar_marker.dart';
|
||||
import '../models/map_drawing.dart';
|
||||
import '../services/message_storage_service.dart';
|
||||
@@ -27,6 +29,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
final Map<String, MessageContactLocation> _messageContactLocations = {};
|
||||
final Map<String, MessageReceptionDetails> _messageReceptionDetails = {};
|
||||
final Map<String, MessageTransferDetails> _messageTransferDetails = {};
|
||||
final Map<String, MessageRouteMetadata> _messageRouteMetadata = {};
|
||||
|
||||
// Track pending sent messages by expected ACK/TAG
|
||||
final Map<int, Message> _pendingSentMessages = {};
|
||||
@@ -59,6 +62,8 @@ class MessagesProvider with ChangeNotifier {
|
||||
|
||||
// Navigation state for message highlighting/scrolling
|
||||
String? _targetMessageId;
|
||||
String? _targetDestinationType;
|
||||
String? _targetRecipientPublicKeyHex;
|
||||
|
||||
// Helper function to compare Uint8List for equality
|
||||
bool _listEquals(Uint8List a, Uint8List b) {
|
||||
@@ -81,6 +86,25 @@ class MessagesProvider with ChangeNotifier {
|
||||
|
||||
Future<void> Function({required Contact contact, required int failureStreak})?
|
||||
onDirectPathFailedCallback;
|
||||
Future<bool> Function({
|
||||
required String messageId,
|
||||
required Contact contact,
|
||||
required Message message,
|
||||
})?
|
||||
onFinalRouterFallbackCallback;
|
||||
Future<void> Function({
|
||||
required String messageId,
|
||||
required Contact contact,
|
||||
required Message message,
|
||||
})?
|
||||
onFinalDirectMessageFailureCallback;
|
||||
void Function({
|
||||
required String messageId,
|
||||
required Contact contact,
|
||||
required Message message,
|
||||
required int roundTripTimeMs,
|
||||
})?
|
||||
onDirectMessageDeliveredCallback;
|
||||
|
||||
String? Function(Uint8List? publicKey)? resolveContactNameCallback;
|
||||
String Function(int channelIdx)? resolveChannelNameCallback;
|
||||
@@ -126,6 +150,30 @@ class MessagesProvider with ChangeNotifier {
|
||||
MessageTransferDetails? getMessageTransferDetails(String messageId) =>
|
||||
_messageTransferDetails[messageId];
|
||||
|
||||
MessageRouteMetadata? getMessageRouteMetadata(String messageId) =>
|
||||
_messageRouteMetadata[messageId];
|
||||
|
||||
void updateMessageRouteSelection(
|
||||
String messageId,
|
||||
PathSelection selection, {
|
||||
required bool routerFallbackAttempted,
|
||||
}) {
|
||||
_messageRouteMetadata[messageId] = MessageRouteMetadata.fromSelection(
|
||||
selection,
|
||||
routerFallbackAttempted: routerFallbackAttempted,
|
||||
);
|
||||
|
||||
final index = _messages.indexWhere((message) => message.id == messageId);
|
||||
if (index != -1) {
|
||||
_messages[index] = _messages[index].copyWith(
|
||||
usedFloodFallback: selection.usesFlood,
|
||||
);
|
||||
}
|
||||
|
||||
_persistMessages();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set localizations for notifications
|
||||
void setLocalizations(AppLocalizations localizations) {
|
||||
_localizations = localizations;
|
||||
@@ -137,16 +185,83 @@ class MessagesProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String? get targetDestinationType => _targetDestinationType;
|
||||
String? get targetRecipientPublicKeyHex => _targetRecipientPublicKeyHex;
|
||||
|
||||
void navigateToDestination(String type, {String? recipientPublicKeyHex}) {
|
||||
_targetDestinationType = type;
|
||||
_targetRecipientPublicKeyHex = recipientPublicKeyHex;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear message navigation state
|
||||
void clearMessageNavigation() {
|
||||
_targetMessageId = null;
|
||||
}
|
||||
|
||||
void clearDestinationNavigation() {
|
||||
_targetDestinationType = null;
|
||||
_targetRecipientPublicKeyHex = null;
|
||||
}
|
||||
|
||||
/// Get count of unread messages (excluding sent messages and system messages)
|
||||
int get unreadCount => _messages
|
||||
.where((m) => !m.isRead && !m.isSentMessage && !m.isSystemMessage)
|
||||
.length;
|
||||
|
||||
int getMessageCountForDestination(Contact contact) => _messages
|
||||
.where((message) => _isMessageForDestination(message, contact))
|
||||
.length;
|
||||
|
||||
int getUnreadCountForDestination(Contact contact) => _messages
|
||||
.where(
|
||||
(message) =>
|
||||
_isMessageForDestination(message, contact) &&
|
||||
!message.isRead &&
|
||||
!message.isSentMessage &&
|
||||
!message.isSystemMessage,
|
||||
)
|
||||
.length;
|
||||
|
||||
bool _isMessageForDestination(Message message, Contact contact) {
|
||||
if (message.isSystemMessage) return false;
|
||||
|
||||
if (contact.isChannel) {
|
||||
final channelIdx = contact.publicKey.length > 1
|
||||
? contact.publicKey[1]
|
||||
: 0;
|
||||
return message.isChannelMessage &&
|
||||
(message.channelIdx ?? 0) == channelIdx;
|
||||
}
|
||||
|
||||
if (!message.isContactMessage || contact.publicKey.length < 6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final selectedPrefix = contact.publicKey.sublist(0, 6);
|
||||
|
||||
if (message.recipientPublicKey != null &&
|
||||
message.recipientPublicKey!.length >= 6 &&
|
||||
_matchesPrefix(message.recipientPublicKey!, selectedPrefix)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.senderPublicKeyPrefix != null &&
|
||||
message.senderPublicKeyPrefix!.length >= 6 &&
|
||||
_matchesPrefix(message.senderPublicKeyPrefix!, selectedPrefix)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _matchesPrefix(Uint8List key, Uint8List prefix) {
|
||||
for (var i = 0; i < 6; i++) {
|
||||
if (key[i] != prefix[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Initialize and load persisted messages
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
@@ -160,6 +275,8 @@ class MessagesProvider with ChangeNotifier {
|
||||
.loadMessageReceptionDetails();
|
||||
final storedTransferDetails = await _storageService
|
||||
.loadMessageTransferDetails();
|
||||
final storedRouteMetadata = await _storageService
|
||||
.loadMessageRouteMetadata();
|
||||
_messageContactLocations
|
||||
..clear()
|
||||
..addAll(storedContactLocations);
|
||||
@@ -169,6 +286,9 @@ class MessagesProvider with ChangeNotifier {
|
||||
_messageTransferDetails
|
||||
..clear()
|
||||
..addAll(storedTransferDetails);
|
||||
_messageRouteMetadata
|
||||
..clear()
|
||||
..addAll(storedRouteMetadata);
|
||||
|
||||
// Add stored messages with enhancement to ensure SAR detection
|
||||
for (final message in storedMessages) {
|
||||
@@ -688,6 +808,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
messageContactLocations: _messageContactLocations,
|
||||
messageReceptionDetails: _messageReceptionDetails,
|
||||
messageTransferDetails: _messageTransferDetails,
|
||||
messageRouteMetadata: _messageRouteMetadata,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MessagesProvider] Error persisting messages: $e');
|
||||
@@ -806,6 +927,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
_messageContactLocations.remove(messageId);
|
||||
_messageReceptionDetails.remove(messageId);
|
||||
_messageTransferDetails.remove(messageId);
|
||||
_messageRouteMetadata.remove(messageId);
|
||||
|
||||
debugPrint('🗑️ [MessagesProvider] Message $messageId deleted');
|
||||
|
||||
@@ -837,6 +959,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
_messageContactLocations.clear();
|
||||
_messageReceptionDetails.clear();
|
||||
_messageTransferDetails.clear();
|
||||
_messageRouteMetadata.clear();
|
||||
_persistMessages();
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -854,6 +977,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
_messageContactLocations.clear();
|
||||
_messageReceptionDetails.clear();
|
||||
_messageTransferDetails.clear();
|
||||
_messageRouteMetadata.clear();
|
||||
_persistMessages();
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -1549,6 +1673,12 @@ class MessagesProvider with ChangeNotifier {
|
||||
final deliveredContact = _messageContactMap[message.id];
|
||||
if (deliveredContact != null) {
|
||||
_retryManager.recordDeliverySuccess(deliveredContact);
|
||||
onDirectMessageDeliveredCallback?.call(
|
||||
messageId: message.id,
|
||||
contact: deliveredContact,
|
||||
message: updatedMessage,
|
||||
roundTripTimeMs: roundTripTimeMs,
|
||||
);
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
@@ -1592,6 +1722,12 @@ class MessagesProvider with ChangeNotifier {
|
||||
final deliveredContact = _messageContactMap[historicalMessageId];
|
||||
if (deliveredContact != null) {
|
||||
_retryManager.recordDeliverySuccess(deliveredContact);
|
||||
onDirectMessageDeliveredCallback?.call(
|
||||
messageId: historicalMessageId,
|
||||
contact: deliveredContact,
|
||||
message: _messages[historicalIndex],
|
||||
roundTripTimeMs: roundTripTimeMs,
|
||||
);
|
||||
}
|
||||
_persistMessages();
|
||||
notifyListeners();
|
||||
@@ -1677,29 +1813,29 @@ class MessagesProvider with ChangeNotifier {
|
||||
debugPrint(' Contact has path: ${contact?.routeHasPath ?? false}');
|
||||
debugPrint(' Used flood fallback: ${message.usedFloodFallback}');
|
||||
|
||||
// Decision tree for retry/flood/fail
|
||||
final routeMetadata = _messageRouteMetadata[messageId];
|
||||
final routerFallbackAttempted =
|
||||
routeMetadata?.routerFallbackAttempted ?? false;
|
||||
|
||||
// Decision tree for retry/final-router-fallback/fail
|
||||
if (contact != null && _retryManager.canRetry(message, contact)) {
|
||||
// RETRY: Contact has path and retry attempts < 3
|
||||
_scheduleRetry(messageId, message, contact);
|
||||
} else if (contact != null &&
|
||||
_retryManager.shouldUseFloodFallback(message, contact)) {
|
||||
// FLOOD FALLBACK: After 3 retries failed, try flood once
|
||||
_sendWithFloodMode(messageId, message, contact);
|
||||
} else if (contact != null && !routerFallbackAttempted) {
|
||||
unawaited(_sendWithFinalRouterFallback(messageId, message, contact));
|
||||
} else {
|
||||
// PERMANENTLY FAILED: No retry possible
|
||||
_markAsPermanentlyFailed(messageId, message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedule a retry with progressive timeout
|
||||
/// Schedule a retry with exponential backoff.
|
||||
void _scheduleRetry(String messageId, Message message, Contact contact) {
|
||||
final nextAttempt = message.retryAttempt + 1;
|
||||
final timeout = _retryManager.getTimeoutForAttempt(message.retryAttempt);
|
||||
final delayMs = _retryManager.getDelayForAttempt(message.retryAttempt);
|
||||
|
||||
debugPrint(
|
||||
'🔄 [MessagesProvider] Scheduling retry $nextAttempt/3 for message $messageId',
|
||||
'🔄 [MessagesProvider] Scheduling retry $nextAttempt/${MessageRetryManager.maxRetryAttempts} for message $messageId',
|
||||
);
|
||||
debugPrint(' Timeout: ${timeout}ms');
|
||||
debugPrint(' Delay: ${delayMs}ms');
|
||||
|
||||
// Update message with new retry attempt
|
||||
final index = _messages.indexWhere((m) => m.id == messageId);
|
||||
@@ -1720,10 +1856,10 @@ class MessagesProvider with ChangeNotifier {
|
||||
// Track retry
|
||||
_retryManager.trackRetry(messageId, nextAttempt);
|
||||
|
||||
notifyListeners(); // Update UI to show "Retrying (X/3)..."
|
||||
notifyListeners();
|
||||
|
||||
// Schedule actual retry after delay
|
||||
Timer(Duration(milliseconds: timeout), () async {
|
||||
Timer(Duration(milliseconds: delayMs), () async {
|
||||
debugPrint(
|
||||
'⏰ [MessagesProvider] Executing retry $nextAttempt for message $messageId',
|
||||
);
|
||||
@@ -1759,49 +1895,46 @@ class MessagesProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Send message with flood mode as last resort
|
||||
Future<void> _sendWithFloodMode(
|
||||
Future<void> _sendWithFinalRouterFallback(
|
||||
String messageId,
|
||||
Message message,
|
||||
Contact contact,
|
||||
) async {
|
||||
debugPrint(
|
||||
'🌊 [MessagesProvider] Trying flood mode for message $messageId',
|
||||
'🛟 [MessagesProvider] Trying final router fallback for $messageId',
|
||||
);
|
||||
|
||||
final index = _messages.indexWhere((m) => m.id == messageId);
|
||||
if (index != -1) {
|
||||
_messages[index] = message.copyWith(
|
||||
usedFloodFallback: true,
|
||||
deliveryStatus: MessageDeliveryStatus.sending,
|
||||
lastRetryAt: DateTime.now(),
|
||||
);
|
||||
|
||||
// Cancel old timeout timer
|
||||
_timeoutTimers[message.id]?.cancel();
|
||||
_timeoutTimers.remove(message.id);
|
||||
if (message.expectedAckTag != null) {
|
||||
_pendingSentMessages.remove(message.expectedAckTag);
|
||||
}
|
||||
_clearAckHistoryForMessage(messageId);
|
||||
|
||||
notifyListeners();
|
||||
|
||||
// Send with flood mode (no retry after this)
|
||||
if (sendMessageCallback != null) {
|
||||
final queued = await sendMessageCallback!(
|
||||
contactPublicKey: contact.publicKey,
|
||||
text: message.text,
|
||||
messageId: messageId,
|
||||
contact: contact,
|
||||
retryAttempt: 0, // Reset attempt for flood
|
||||
);
|
||||
if (!queued) {
|
||||
_markAsPermanentlyFailed(messageId, _messages[index]);
|
||||
}
|
||||
} else {
|
||||
if (onFinalRouterFallbackCallback == null) {
|
||||
debugPrint(
|
||||
'⚠️ [MessagesProvider] sendMessageCallback not set, cannot send flood',
|
||||
'⚠️ [MessagesProvider] onFinalRouterFallbackCallback not set',
|
||||
);
|
||||
_markAsPermanentlyFailed(messageId, _messages[index]);
|
||||
return;
|
||||
}
|
||||
|
||||
final queued = await onFinalRouterFallbackCallback!(
|
||||
messageId: messageId,
|
||||
contact: contact,
|
||||
message: _messages[index],
|
||||
);
|
||||
if (!queued) {
|
||||
_markAsPermanentlyFailed(messageId, _messages[index]);
|
||||
}
|
||||
|
||||
_persistMessages();
|
||||
@@ -1830,19 +1963,15 @@ class MessagesProvider with ChangeNotifier {
|
||||
_retryManager.clearRetry(messageId);
|
||||
|
||||
final failedContact = _messageContactMap[messageId];
|
||||
if (failedContact != null && failedContact.routeHasPath) {
|
||||
final failureStreak = _retryManager.recordPathFailure(failedContact);
|
||||
debugPrint(
|
||||
' Path failure streak for ${failedContact.advName}: $failureStreak',
|
||||
if (failedContact != null &&
|
||||
onFinalDirectMessageFailureCallback != null) {
|
||||
unawaited(
|
||||
onFinalDirectMessageFailureCallback!(
|
||||
messageId: messageId,
|
||||
contact: failedContact,
|
||||
message: _messages[index],
|
||||
),
|
||||
);
|
||||
if (failureStreak >= 2 && onDirectPathFailedCallback != null) {
|
||||
unawaited(
|
||||
onDirectPathFailedCallback!(
|
||||
contact: failedContact,
|
||||
failureStreak: failureStreak,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_persistMessages();
|
||||
@@ -1870,6 +1999,7 @@ class MessagesProvider with ChangeNotifier {
|
||||
}
|
||||
_clearAckHistoryForMessage(messageId);
|
||||
_retryManager.clearRetry(messageId);
|
||||
_messageRouteMetadata.remove(messageId);
|
||||
|
||||
_messages[index] = Message(
|
||||
id: message.id,
|
||||
|
||||
@@ -12,8 +12,13 @@ import '../widgets/contacts/add_channel_dialog.dart';
|
||||
|
||||
class ContactsTab extends StatefulWidget {
|
||||
final VoidCallback? onNavigateToMap;
|
||||
final VoidCallback? onNavigateToMessages;
|
||||
|
||||
const ContactsTab({super.key, this.onNavigateToMap});
|
||||
const ContactsTab({
|
||||
super.key,
|
||||
this.onNavigateToMap,
|
||||
this.onNavigateToMessages,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ContactsTab> createState() => _ContactsTabState();
|
||||
@@ -211,11 +216,12 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
final channels = _sortContactsByDistance(contactsProvider.channels);
|
||||
final pendingAdverts = contactsProvider.pendingAdverts;
|
||||
|
||||
// Check if there are any displayable contacts (excluding channels)
|
||||
// Check if there are any displayable contacts
|
||||
final hasDisplayableContacts =
|
||||
chatContacts.isNotEmpty ||
|
||||
repeaters.isNotEmpty ||
|
||||
rooms.isNotEmpty ||
|
||||
channels.isNotEmpty ||
|
||||
pendingAdverts.isNotEmpty;
|
||||
|
||||
if (!hasDisplayableContacts) {
|
||||
@@ -284,6 +290,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
calculateDistance: _calculateDistanceInMeters,
|
||||
formatDistance: _formatDistance,
|
||||
onNavigateToMap: widget.onNavigateToMap,
|
||||
onNavigateToMessages: widget.onNavigateToMessages,
|
||||
),
|
||||
),
|
||||
const Divider(height: 32),
|
||||
@@ -303,6 +310,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
calculateDistance: _calculateDistanceInMeters,
|
||||
formatDistance: _formatDistance,
|
||||
onNavigateToMap: widget.onNavigateToMap,
|
||||
onNavigateToMessages: widget.onNavigateToMessages,
|
||||
),
|
||||
),
|
||||
const Divider(height: 32),
|
||||
@@ -322,6 +330,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
calculateDistance: _calculateDistanceInMeters,
|
||||
formatDistance: _formatDistance,
|
||||
onNavigateToMap: widget.onNavigateToMap,
|
||||
onNavigateToMessages: widget.onNavigateToMessages,
|
||||
),
|
||||
),
|
||||
const Divider(height: 32),
|
||||
@@ -341,6 +350,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
||||
calculateDistance: _calculateDistanceInMeters,
|
||||
formatDistance: _formatDistance,
|
||||
onNavigateToMap: widget.onNavigateToMap,
|
||||
onNavigateToMessages: widget.onNavigateToMessages,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -23,6 +23,7 @@ import '../l10n/app_localizations.dart';
|
||||
import '../widgets/permission_request_dialog.dart';
|
||||
import '../widgets/connection_dialog.dart';
|
||||
import '../utils/battery_display_helper.dart';
|
||||
import '../services/developer_mode_service.dart';
|
||||
|
||||
enum _HomeTab { messages, contacts, sensors, map }
|
||||
|
||||
@@ -55,6 +56,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
int _currentIndex = 0;
|
||||
bool _isMapFullscreen = false;
|
||||
bool _showRxTxIndicators = true;
|
||||
bool _isDeveloperModeEnabled = false;
|
||||
bool _isMapEnabled = true;
|
||||
bool _isContactsEnabled = true;
|
||||
bool _isSensorsEnabled = false;
|
||||
@@ -90,6 +92,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
// Initialize synchronously so first build always has a valid controller.
|
||||
_initTabController();
|
||||
_loadRxTxPreference();
|
||||
_loadDeveloperModePreference();
|
||||
|
||||
// Show permission dialog after the first frame if needed
|
||||
if (widget.shouldShowPermissionDialog) {
|
||||
@@ -228,6 +231,14 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadDeveloperModePreference() async {
|
||||
final isEnabled = await DeveloperModeService.isEnabled();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isDeveloperModeEnabled = isEnabled;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
@@ -598,56 +609,67 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.radar),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Spectrum Scan'),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final navigator = Navigator.of(context);
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (!mounted) return;
|
||||
navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const SpectrumScanScreen(),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.settings),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.settings),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
// Capture context-dependent objects before async gap
|
||||
final navigator = Navigator.of(context);
|
||||
Future.delayed(Duration.zero, () async {
|
||||
if (!mounted) return;
|
||||
await navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SettingsScreen(
|
||||
onThemeChanged: widget.onThemeChanged,
|
||||
onLocaleChanged: widget.onLocaleChanged,
|
||||
currentTheme: widget.currentTheme,
|
||||
currentLocale: widget.currentLocale,
|
||||
itemBuilder: (context) {
|
||||
final items = <PopupMenuEntry<void>>[];
|
||||
|
||||
if (_isDeveloperModeEnabled) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.radar),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Spectrum Scan'),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final navigator = Navigator.of(context);
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (!mounted) return;
|
||||
navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const SpectrumScanScreen(),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.settings),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.settings),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final navigator = Navigator.of(context);
|
||||
Future.delayed(Duration.zero, () async {
|
||||
if (!mounted) return;
|
||||
await navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SettingsScreen(
|
||||
onThemeChanged: widget.onThemeChanged,
|
||||
onLocaleChanged: widget.onLocaleChanged,
|
||||
currentTheme: widget.currentTheme,
|
||||
currentLocale: widget.currentLocale,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// Reload preference when returning from settings
|
||||
_loadRxTxPreference();
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
_loadRxTxPreference();
|
||||
_loadDeveloperModePreference();
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return items;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -666,6 +688,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
onNavigateToMap: _isMapEnabled
|
||||
? () => _navigateToTab(_HomeTab.map)
|
||||
: null,
|
||||
onNavigateToMessages: () => _navigateToTab(_HomeTab.messages),
|
||||
);
|
||||
case _HomeTab.sensors:
|
||||
return const SensorsTab();
|
||||
|
||||
@@ -128,11 +128,20 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
void _checkForNavigationRequest() {
|
||||
final messagesProvider = context.read<MessagesProvider>();
|
||||
final targetMessageId = messagesProvider.targetMessageId;
|
||||
final targetDestinationType = messagesProvider.targetDestinationType;
|
||||
|
||||
if (targetMessageId != null) {
|
||||
_scrollToMessage(targetMessageId);
|
||||
messagesProvider.clearMessageNavigation();
|
||||
}
|
||||
|
||||
if (targetDestinationType != null) {
|
||||
_applyPendingDestination(
|
||||
type: targetDestinationType,
|
||||
recipientPublicKeyHex: messagesProvider.targetRecipientPublicKeyHex,
|
||||
);
|
||||
messagesProvider.clearDestinationNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
void _scrollToMessage(String messageId) {
|
||||
@@ -258,6 +267,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
/// Show recipient selector bottom sheet
|
||||
void _showRecipientSelector() {
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
final messagesProvider = context.read<MessagesProvider>();
|
||||
|
||||
// Filter contacts by type
|
||||
final contacts = contactsProvider.contacts
|
||||
@@ -278,6 +288,13 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
contacts: contacts,
|
||||
rooms: rooms,
|
||||
channels: channels,
|
||||
unreadCount: messagesProvider.unreadCount,
|
||||
unreadCountsByPublicKey: {
|
||||
for (final contact in [...contacts, ...rooms, ...channels])
|
||||
contact.publicKeyHex: messagesProvider.getUnreadCountForDestination(
|
||||
contact,
|
||||
),
|
||||
},
|
||||
currentDestinationType: _destinationType,
|
||||
currentRecipientPublicKey: _selectedRecipient?.publicKeyHex,
|
||||
onSelect: _onRecipientSelected,
|
||||
@@ -304,9 +321,73 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
if (!mounted) return;
|
||||
}
|
||||
|
||||
Future<void> _applyPendingDestination({
|
||||
required String type,
|
||||
String? recipientPublicKeyHex,
|
||||
}) async {
|
||||
Contact? recipient;
|
||||
if (recipientPublicKeyHex != null) {
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
recipient = contactsProvider.contacts.where((contact) {
|
||||
return contact.publicKeyHex == recipientPublicKeyHex;
|
||||
}).firstOrNull;
|
||||
}
|
||||
|
||||
await _onRecipientSelected(type, recipient);
|
||||
if (!mounted) return;
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
|
||||
Future<void> _replyToMessage(Message message) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
|
||||
String destinationType;
|
||||
Contact? recipient;
|
||||
|
||||
if (message.isChannelMessage) {
|
||||
final channelIdx = message.channelIdx ?? 0;
|
||||
destinationType = MessageDestinationPreferences.destinationTypeChannel;
|
||||
|
||||
if (channelIdx != 0) {
|
||||
recipient = contactsProvider.channels.where((contact) {
|
||||
return contact.publicKey.length > 1 &&
|
||||
contact.publicKey[1] == channelIdx;
|
||||
}).firstOrNull;
|
||||
|
||||
if (recipient == null) {
|
||||
ToastLogger.error(context, l10n.cannotReplyContactNotFound);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final senderPrefix = message.senderPublicKeyPrefix;
|
||||
if (senderPrefix == null || senderPrefix.length < 6) {
|
||||
ToastLogger.error(context, l10n.cannotReplySenderMissing);
|
||||
return;
|
||||
}
|
||||
|
||||
recipient = contactsProvider.findContactByPrefix(senderPrefix);
|
||||
if (recipient == null) {
|
||||
ToastLogger.error(context, l10n.cannotReplyContactNotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
destinationType = recipient.isRoom
|
||||
? MessageDestinationPreferences.destinationTypeRoom
|
||||
: MessageDestinationPreferences.destinationTypeContact;
|
||||
}
|
||||
|
||||
await _onRecipientSelected(destinationType, recipient);
|
||||
if (!mounted) return;
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
|
||||
/// Get icon for current destination type
|
||||
IconData _getDestinationIcon() {
|
||||
if (_destinationType ==
|
||||
if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
|
||||
return Icons.all_inbox;
|
||||
} else if (_destinationType ==
|
||||
MessageDestinationPreferences.destinationTypeChannel) {
|
||||
return Icons.public;
|
||||
} else if (_destinationType ==
|
||||
@@ -318,6 +399,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
}
|
||||
|
||||
String _getDestinationLabel() {
|
||||
if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
|
||||
return AppLocalizations.of(context)!.showAll;
|
||||
}
|
||||
if (_destinationType ==
|
||||
MessageDestinationPreferences.destinationTypeChannel &&
|
||||
_selectedRecipient != null) {
|
||||
@@ -343,6 +427,12 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
|
||||
if (!mounted) return;
|
||||
ToastLogger.error(context, 'Select a channel, contact, or room first');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check destination type and send accordingly
|
||||
if (_destinationType ==
|
||||
@@ -1107,6 +1197,15 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.search),
|
||||
title: const Text('Search messages'),
|
||||
onTap: () async {
|
||||
await _runAfterSheetDismissal(sheetContext, () async {
|
||||
_showFilteredMessageSearch();
|
||||
});
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add_location_alt),
|
||||
title: Text(AppLocalizations.of(context)!.sendSarMarker),
|
||||
@@ -1174,6 +1273,116 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showFilteredMessageSearch() {
|
||||
final messagesProvider = context.read<MessagesProvider>();
|
||||
final scopedMessages = _getFilteredMessages(messagesProvider);
|
||||
final searchController = TextEditingController();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (sheetContext) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
final query = searchController.text.trim().toLowerCase();
|
||||
final matches = query.isEmpty
|
||||
? scopedMessages
|
||||
: scopedMessages.where((message) {
|
||||
return message.text.toLowerCase().contains(query) ||
|
||||
(message.senderName?.toLowerCase().contains(query) ??
|
||||
false);
|
||||
}).toList();
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(sheetContext).viewInsets.bottom,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(sheetContext).size.height * 0.72,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search in current filter',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: searchController.text.isEmpty
|
||||
? null
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
setModalState(() {});
|
||||
},
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
onChanged: (_) => setModalState(() {}),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: matches.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
query.isEmpty
|
||||
? 'No messages in this filter'
|
||||
: 'No matches in this filter',
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
itemCount: matches.length,
|
||||
separatorBuilder: (_, _) =>
|
||||
const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final message = matches[index];
|
||||
final title =
|
||||
message.senderName?.trim().isNotEmpty ==
|
||||
true
|
||||
? message.senderName!
|
||||
: message.isSentMessage
|
||||
? 'You'
|
||||
: _getDestinationLabel();
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
message.text,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_scrollToMessage(message.id);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
).whenComplete(searchController.dispose);
|
||||
}
|
||||
|
||||
Widget _buildDestinationAvatar(BuildContext context) {
|
||||
final recipient = _selectedRecipient;
|
||||
if (recipient != null) {
|
||||
@@ -1439,17 +1648,18 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
List<Message> filteredMessages;
|
||||
|
||||
// If channel destination is selected, filter by selected channel.
|
||||
if (_destinationType ==
|
||||
if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
|
||||
filteredMessages = allMessages;
|
||||
} else if (_destinationType ==
|
||||
MessageDestinationPreferences.destinationTypeChannel) {
|
||||
final selectedChannelIdx = _selectedRecipient?.publicKey[1] ?? 0;
|
||||
if (selectedChannelIdx == 0) {
|
||||
// Public channel view keeps showing all messages (current app behavior).
|
||||
filteredMessages = allMessages;
|
||||
} else {
|
||||
filteredMessages = allMessages
|
||||
.where((message) => message.channelIdx == selectedChannelIdx)
|
||||
.toList();
|
||||
}
|
||||
filteredMessages = allMessages
|
||||
.where(
|
||||
(message) =>
|
||||
message.isChannelMessage &&
|
||||
(message.channelIdx ?? 0) == selectedChannelIdx,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
// If a contact or room is selected, filter by recipient/sender prefixes.
|
||||
else if ((_destinationType ==
|
||||
@@ -1458,6 +1668,10 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
MessageDestinationPreferences.destinationTypeRoom) &&
|
||||
_selectedRecipient != null) {
|
||||
filteredMessages = allMessages.where((message) {
|
||||
if (!message.isContactMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Include messages sent TO this recipient
|
||||
if (message.recipientPublicKey != null &&
|
||||
message.recipientPublicKey!.length >= 6 &&
|
||||
@@ -1538,6 +1752,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
onRefresh: _handleRefresh,
|
||||
onNavigateToMap: widget.onNavigateToMap,
|
||||
onMessageTap: _handleMessageTap,
|
||||
onReplyToMessage: _replyToMessage,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:meshcore_client/meshcore_client.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../services/route_hash_preferences.dart';
|
||||
import '../utils/log_rx_route_decoder.dart';
|
||||
|
||||
class PacketLogScreen extends StatefulWidget {
|
||||
@@ -460,26 +461,6 @@ class _PacketLogCard extends StatelessWidget {
|
||||
final isRx = log.direction == PacketDirection.rx;
|
||||
final directionColor = isRx ? Colors.green : Colors.blue;
|
||||
final rxInfo = log.logRxDataInfo;
|
||||
final contacts = context.watch<ContactsProvider>().contacts;
|
||||
final connectionProvider = context.watch<ConnectionProvider>();
|
||||
final decodedRoute = LogRxRouteDecoder.decode(log.rawData);
|
||||
final ownPublicKey = connectionProvider.deviceInfo.publicKey;
|
||||
final ownName =
|
||||
connectionProvider.deviceInfo.selfName ??
|
||||
connectionProvider.deviceInfo.displayName;
|
||||
final resolvedPath = decodedRoute?.pathHashes
|
||||
.map(
|
||||
(hash) => LogRxRouteDecoder.resolveHash(
|
||||
hash,
|
||||
contacts: contacts,
|
||||
ownPublicKey: ownPublicKey,
|
||||
ownName: ownName,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final originalSender = resolvedPath != null && resolvedPath.isNotEmpty
|
||||
? resolvedPath.first
|
||||
: null;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
@@ -604,13 +585,9 @@ class _PacketLogCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
if (isRx && decodedRoute != null) ...[
|
||||
if (isRx) ...[
|
||||
const SizedBox(height: 12),
|
||||
_RouteSection(
|
||||
route: decodedRoute,
|
||||
path: resolvedPath ?? const [],
|
||||
originalSender: originalSender,
|
||||
),
|
||||
_DecodedRouteSection(log: log),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
@@ -755,6 +732,53 @@ class _PacketLogCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _DecodedRouteSection extends StatelessWidget {
|
||||
final BlePacketLog log;
|
||||
|
||||
const _DecodedRouteSection({required this.log});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final contacts = context.watch<ContactsProvider>().contacts;
|
||||
final connectionProvider = context.watch<ConnectionProvider>();
|
||||
final ownPublicKey = connectionProvider.deviceInfo.publicKey;
|
||||
final ownName =
|
||||
connectionProvider.deviceInfo.selfName ??
|
||||
connectionProvider.deviceInfo.displayName;
|
||||
|
||||
return FutureBuilder<int>(
|
||||
future: RouteHashPreferences.getHashSize(),
|
||||
builder: (context, snapshot) {
|
||||
final decodedRoute = LogRxRouteDecoder.decode(
|
||||
log.rawData,
|
||||
preferredHashSize: snapshot.data,
|
||||
);
|
||||
if (decodedRoute == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final resolvedPath = decodedRoute.hopHashes
|
||||
.map(
|
||||
(hashHex) => LogRxRouteDecoder.resolveHash(
|
||||
hashHex,
|
||||
contacts: contacts,
|
||||
ownPublicKey: ownPublicKey,
|
||||
ownName: ownName,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final originalSender = resolvedPath.isEmpty ? null : resolvedPath.first;
|
||||
|
||||
return _RouteSection(
|
||||
route: decodedRoute,
|
||||
path: resolvedPath,
|
||||
originalSender: originalSender,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RouteSection extends StatelessWidget {
|
||||
final DecodedLogRxRoute route;
|
||||
final List<ResolvedNodeHash> path;
|
||||
@@ -802,7 +826,13 @@ class _RouteSection extends StatelessWidget {
|
||||
_FactCard(
|
||||
icon: Icons.hub,
|
||||
label: 'Hops',
|
||||
value: '${route.pathHashes.length}',
|
||||
value: '${route.hopCount}',
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.tag,
|
||||
label: 'Hash size',
|
||||
value:
|
||||
'${route.hashSize} byte${route.hashSize == 1 ? '' : 's'}',
|
||||
),
|
||||
if (originalSender != null)
|
||||
_FactCard(
|
||||
|
||||
@@ -371,6 +371,7 @@ class _SensorCard extends StatelessWidget {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final telemetry = contact?.telemetry;
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final metrics = contact == null || telemetry == null
|
||||
? const <_MetricCardData>[]
|
||||
: _buildMetricCards(l10n, telemetry, contact!);
|
||||
@@ -378,16 +379,23 @@ class _SensorCard extends StatelessWidget {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colorScheme.surfaceContainerLow,
|
||||
colorScheme.surfaceContainerHighest.withValues(alpha: 0.9),
|
||||
],
|
||||
),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.14),
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.35),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 18,
|
||||
offset: const Offset(0, 8),
|
||||
color: Colors.black.withValues(alpha: 0.045),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -21,6 +21,7 @@ import '../services/voice_bitrate_preferences.dart';
|
||||
import '../services/image_preferences.dart';
|
||||
import '../services/route_hash_preferences.dart';
|
||||
import '../services/image_codec_service.dart';
|
||||
import '../services/developer_mode_service.dart';
|
||||
import '../utils/sample_data_generator.dart';
|
||||
import '../utils/image_message_parser.dart';
|
||||
import '../utils/voice_message_parser.dart';
|
||||
@@ -70,6 +71,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
bool _fastLocationUpdatesEnabled = false;
|
||||
double _fastLocationMovementThresholdMeters = 10.0;
|
||||
int _fastLocationActiveCadenceSeconds = 10;
|
||||
bool _isDeveloperModeEnabled = false;
|
||||
int _versionTapCount = 0;
|
||||
final ImagePicker _imagePicker = ImagePicker();
|
||||
final LocationTrackingService _locationService = LocationTrackingService();
|
||||
|
||||
@@ -85,6 +88,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
_loadRouteHashSizePreference();
|
||||
_loadImagePreferences();
|
||||
_loadFastLocationSettings();
|
||||
_loadDeveloperMode();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -114,6 +118,48 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadDeveloperMode() async {
|
||||
final isEnabled = await DeveloperModeService.isEnabled();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isDeveloperModeEnabled = isEnabled;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _handleVersionTap() async {
|
||||
if (_isDeveloperModeEnabled) {
|
||||
await DeveloperModeService.setEnabled(false);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isDeveloperModeEnabled = false;
|
||||
_versionTapCount = 0;
|
||||
});
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Developer mode disabled')));
|
||||
return;
|
||||
}
|
||||
|
||||
final nextTapCount = _versionTapCount + 1;
|
||||
if (nextTapCount >= 3) {
|
||||
await DeveloperModeService.setEnabled(true);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isDeveloperModeEnabled = true;
|
||||
_versionTapCount = 0;
|
||||
});
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Developer mode enabled')));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_versionTapCount = nextTapCount;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _saveRxTxPreference(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('show_rx_tx_indicators', value);
|
||||
@@ -929,6 +975,45 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: _showRouteHashSizeDialog,
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.swap_horiz),
|
||||
title: const Text('Auto route rotation'),
|
||||
subtitle: const Text(
|
||||
'Rotate between best known direct paths and flood mode for room/contact sends',
|
||||
),
|
||||
value: appProvider.autoRouteRotationEnabled,
|
||||
onChanged: (value) async {
|
||||
await appProvider.toggleAutoRouteRotationEnabled(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.route),
|
||||
title: const Text('Nearest repeater fallback'),
|
||||
subtitle: const Text(
|
||||
'After normal retries fail, try one final resend through the nearest repeater',
|
||||
),
|
||||
value: appProvider.nearestRelayFallbackEnabled,
|
||||
onChanged: (value) async {
|
||||
await appProvider.toggleNearestRelayFallbackEnabled(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => SwitchListTile(
|
||||
secondary: const Icon(Icons.route),
|
||||
title: const Text('Clear path on max retry'),
|
||||
subtitle: const Text(
|
||||
'Clear the route only after all retries and final router fallback fail',
|
||||
),
|
||||
value: appProvider.clearPathOnMaxRetry,
|
||||
onChanged: (value) async {
|
||||
await appProvider.toggleClearPathOnMaxRetry(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_sweep, color: Colors.red),
|
||||
title: const Text(
|
||||
@@ -938,6 +1023,28 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
subtitle: const Text('Delete all stored message history'),
|
||||
onTap: _clearMessages,
|
||||
),
|
||||
Consumer<AppProvider>(
|
||||
builder: (context, appProvider, child) => ListTile(
|
||||
leading: const Icon(Icons.format_size),
|
||||
title: const Text('Message font size'),
|
||||
subtitle: Text(
|
||||
'${(appProvider.messageFontScale * 100).round()}% of default',
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 150,
|
||||
child: Slider(
|
||||
value: appProvider.messageFontScale,
|
||||
min: 0.85,
|
||||
max: 1.4,
|
||||
divisions: 11,
|
||||
label: '${(appProvider.messageFontScale * 100).round()}%',
|
||||
onChanged: (value) {
|
||||
appProvider.setMessageFontScale(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
|
||||
_buildSectionHeader('Voice'),
|
||||
@@ -1197,6 +1304,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
? '${_packageInfo!.version} (${_packageInfo!.buildNumber})'
|
||||
: 'Loading...',
|
||||
),
|
||||
onTap: _handleVersionTap,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.badge),
|
||||
|
||||
15
lib/services/developer_mode_service.dart
Normal file
15
lib/services/developer_mode_service.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class DeveloperModeService {
|
||||
static const String _developerModeKey = 'developer_mode_enabled';
|
||||
|
||||
static Future<bool> isEnabled() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_developerModeKey) ?? false;
|
||||
}
|
||||
|
||||
static Future<void> setEnabled(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_developerModeKey, enabled);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,10 @@ class LocalePreferences {
|
||||
Locale('fr'), // French
|
||||
Locale('it'), // Italian
|
||||
Locale('el'), // Greek
|
||||
Locale('pt'), // Portuguese
|
||||
Locale('tr'), // Turkish
|
||||
Locale('pl'), // Polish
|
||||
Locale('uk'), // Ukrainian
|
||||
Locale('ru'), // Russian
|
||||
Locale('zh'), // Chinese
|
||||
];
|
||||
@@ -66,6 +70,14 @@ class LocalePreferences {
|
||||
return 'Italiano';
|
||||
case 'el':
|
||||
return 'Greek';
|
||||
case 'pt':
|
||||
return 'Portuguese';
|
||||
case 'tr':
|
||||
return 'Turkish';
|
||||
case 'pl':
|
||||
return 'Polish';
|
||||
case 'uk':
|
||||
return 'Ukrainian';
|
||||
case 'ru':
|
||||
return 'Русский';
|
||||
case 'zh':
|
||||
@@ -94,6 +106,14 @@ class LocalePreferences {
|
||||
return 'Italiano';
|
||||
case 'el':
|
||||
return 'Ελληνικά';
|
||||
case 'pt':
|
||||
return 'Português';
|
||||
case 'tr':
|
||||
return 'Türkçe';
|
||||
case 'pl':
|
||||
return 'Polski';
|
||||
case 'uk':
|
||||
return 'Українська';
|
||||
case 'ru':
|
||||
return 'Русский';
|
||||
case 'zh':
|
||||
|
||||
@@ -7,6 +7,7 @@ class MessageDestinationPreferences {
|
||||
static const String _recipientPublicKeyKey = 'message_recipient_public_key';
|
||||
|
||||
/// Destination types
|
||||
static const String destinationTypeAll = 'all';
|
||||
static const String destinationTypeChannel = 'channel';
|
||||
static const String destinationTypeContact = 'contact';
|
||||
static const String destinationTypeRoom = 'room';
|
||||
@@ -24,14 +25,11 @@ class MessageDestinationPreferences {
|
||||
|
||||
final publicKey = prefs.getString(_recipientPublicKeyKey);
|
||||
|
||||
return {
|
||||
'type': type,
|
||||
'publicKey': ?publicKey,
|
||||
};
|
||||
return {'type': type, 'publicKey': ?publicKey};
|
||||
}
|
||||
|
||||
/// Save the selected destination
|
||||
/// [type] - one of: destinationTypeChannel, destinationTypeContact, destinationTypeRoom
|
||||
/// [type] - one of: destinationTypeAll, destinationTypeChannel, destinationTypeContact, destinationTypeRoom
|
||||
/// [recipientPublicKey] - hex string of recipient's public key (required for contact/room)
|
||||
static Future<void> setDestination(
|
||||
String type, {
|
||||
@@ -58,6 +56,8 @@ class MessageDestinationPreferences {
|
||||
/// Get display name for destination type
|
||||
static String getDestinationTypeName(String type) {
|
||||
switch (type) {
|
||||
case destinationTypeAll:
|
||||
return 'All';
|
||||
case destinationTypeChannel:
|
||||
return 'Channel';
|
||||
case destinationTypeContact:
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../models/message.dart';
|
||||
import '../models/message_contact_location.dart';
|
||||
import '../models/message_reception_details.dart';
|
||||
import '../models/message_transfer_details.dart';
|
||||
import '../models/message_route_metadata.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
/// Service for persisting messages to local storage
|
||||
@@ -16,6 +17,8 @@ class MessageStorageService {
|
||||
'stored_message_reception_details';
|
||||
static const String _messageTransferDetailsKey =
|
||||
'stored_message_transfer_details';
|
||||
static const String _messageRouteMetadataKey =
|
||||
'stored_message_route_metadata';
|
||||
static const int _maxStoredMessages = 1000; // Store up to 1000 messages
|
||||
|
||||
/// Save messages to persistent storage
|
||||
@@ -24,6 +27,7 @@ class MessageStorageService {
|
||||
Map<String, MessageContactLocation> messageContactLocations = const {},
|
||||
Map<String, MessageReceptionDetails> messageReceptionDetails = const {},
|
||||
Map<String, MessageTransferDetails> messageTransferDetails = const {},
|
||||
Map<String, MessageRouteMetadata> messageRouteMetadata = const {},
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -44,6 +48,7 @@ class MessageStorageService {
|
||||
final locationJson = <String, dynamic>{};
|
||||
final receptionJson = <String, dynamic>{};
|
||||
final transferJson = <String, dynamic>{};
|
||||
final routeMetadataJson = <String, dynamic>{};
|
||||
for (final entry in messageContactLocations.entries) {
|
||||
if (retainedMessageIds.contains(entry.key)) {
|
||||
locationJson[entry.key] = entry.value.toJson();
|
||||
@@ -59,6 +64,11 @@ class MessageStorageService {
|
||||
transferJson[entry.key] = entry.value.toJson();
|
||||
}
|
||||
}
|
||||
for (final entry in messageRouteMetadata.entries) {
|
||||
if (retainedMessageIds.contains(entry.key)) {
|
||||
routeMetadataJson[entry.key] = entry.value.toJson();
|
||||
}
|
||||
}
|
||||
await prefs.setString(
|
||||
_messageContactLocationsKey,
|
||||
jsonEncode(locationJson),
|
||||
@@ -71,6 +81,10 @@ class MessageStorageService {
|
||||
_messageTransferDetailsKey,
|
||||
jsonEncode(transferJson),
|
||||
);
|
||||
await prefs.setString(
|
||||
_messageRouteMetadataKey,
|
||||
jsonEncode(routeMetadataJson),
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
'✅ [MessageStorage] Saved ${limitedList.length} messages to storage',
|
||||
@@ -170,6 +184,32 @@ class MessageStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, MessageRouteMetadata>> loadMessageRouteMetadata() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messageRouteMetadataKey);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return const {};
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(jsonString);
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
return const {};
|
||||
}
|
||||
|
||||
final result = <String, MessageRouteMetadata>{};
|
||||
for (final entry in decoded.entries) {
|
||||
final value = entry.value;
|
||||
if (value is! Map<String, dynamic>) continue;
|
||||
result[entry.key] = MessageRouteMetadata.fromJson(value);
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MessageStorage] Error loading route metadata: $e');
|
||||
return const {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Load messages from persistent storage
|
||||
Future<List<Message>> loadMessages() async {
|
||||
try {
|
||||
@@ -206,6 +246,7 @@ class MessageStorageService {
|
||||
await prefs.remove(_messageContactLocationsKey);
|
||||
await prefs.remove(_messageReceptionDetailsKey);
|
||||
await prefs.remove(_messageTransferDetailsKey);
|
||||
await prefs.remove(_messageRouteMetadataKey);
|
||||
debugPrint('✅ [MessageStorage] Cleared all stored messages');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MessageStorage] Error clearing messages: $e');
|
||||
|
||||
46
lib/services/messaging_route_preferences.dart
Normal file
46
lib/services/messaging_route_preferences.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class MessagingRoutePreferences {
|
||||
static const bool defaultAutoRouteRotationEnabled = false;
|
||||
static const bool defaultClearPathOnMaxRetry = false;
|
||||
static const bool defaultNearestRelayFallbackEnabled = true;
|
||||
|
||||
static const String _autoRouteRotationKey =
|
||||
'messaging_auto_route_rotation_enabled';
|
||||
static const String _clearPathOnMaxRetryKey =
|
||||
'messaging_clear_path_on_max_retry';
|
||||
static const String _nearestRelayFallbackKey =
|
||||
'messaging_nearest_relay_fallback_enabled';
|
||||
|
||||
static Future<bool> getAutoRouteRotationEnabled() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_autoRouteRotationKey) ??
|
||||
defaultAutoRouteRotationEnabled;
|
||||
}
|
||||
|
||||
static Future<void> setAutoRouteRotationEnabled(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_autoRouteRotationKey, enabled);
|
||||
}
|
||||
|
||||
static Future<bool> getClearPathOnMaxRetry() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_clearPathOnMaxRetryKey) ?? defaultClearPathOnMaxRetry;
|
||||
}
|
||||
|
||||
static Future<void> setClearPathOnMaxRetry(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_clearPathOnMaxRetryKey, enabled);
|
||||
}
|
||||
|
||||
static Future<bool> getNearestRelayFallbackEnabled() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_nearestRelayFallbackKey) ??
|
||||
defaultNearestRelayFallbackEnabled;
|
||||
}
|
||||
|
||||
static Future<void> setNearestRelayFallbackEnabled(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_nearestRelayFallbackKey, enabled);
|
||||
}
|
||||
}
|
||||
69
lib/services/nearest_router_selector.dart
Normal file
69
lib/services/nearest_router_selector.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
import '../models/contact.dart';
|
||||
|
||||
class NearestRouterSelector {
|
||||
const NearestRouterSelector();
|
||||
|
||||
Contact? select({
|
||||
required Position? senderPosition,
|
||||
required List<Contact> repeaters,
|
||||
required Contact recipient,
|
||||
}) {
|
||||
if (senderPosition == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final eligible = repeaters.where((contact) {
|
||||
if (contact.publicKeyHex == recipient.publicKeyHex) {
|
||||
return false;
|
||||
}
|
||||
if (!contact.isRecentlySeen) {
|
||||
return false;
|
||||
}
|
||||
return contact.displayLocation != null;
|
||||
}).toList();
|
||||
if (eligible.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
eligible.sort((a, b) {
|
||||
final locationA = a.displayLocation!;
|
||||
final locationB = b.displayLocation!;
|
||||
final distanceA = Geolocator.distanceBetween(
|
||||
senderPosition.latitude,
|
||||
senderPosition.longitude,
|
||||
locationA.latitude,
|
||||
locationA.longitude,
|
||||
);
|
||||
final distanceB = Geolocator.distanceBetween(
|
||||
senderPosition.latitude,
|
||||
senderPosition.longitude,
|
||||
locationB.latitude,
|
||||
locationB.longitude,
|
||||
);
|
||||
final distanceCompare = distanceA.compareTo(distanceB);
|
||||
if (distanceCompare != 0) {
|
||||
return distanceCompare;
|
||||
}
|
||||
|
||||
final advertCompare = b.lastAdvert.compareTo(a.lastAdvert);
|
||||
if (advertCompare != 0) {
|
||||
return advertCompare;
|
||||
}
|
||||
|
||||
final hopCompare = a.routeHopCount.compareTo(b.routeHopCount);
|
||||
if (hopCompare != 0) {
|
||||
return hopCompare;
|
||||
}
|
||||
|
||||
final nameCompare = a.advName.compareTo(b.advName);
|
||||
if (nameCompare != 0) {
|
||||
return nameCompare;
|
||||
}
|
||||
return a.publicKeyHex.compareTo(b.publicKeyHex);
|
||||
});
|
||||
|
||||
return eligible.first;
|
||||
}
|
||||
}
|
||||
273
lib/services/path_history_service.dart
Normal file
273
lib/services/path_history_service.dart
Normal file
@@ -0,0 +1,273 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/contact.dart';
|
||||
import '../models/path_history.dart';
|
||||
import '../models/path_selection.dart';
|
||||
|
||||
class PathHistoryService {
|
||||
static const String _storageKey = 'contact_path_history_v1';
|
||||
static const int _maxDirectPaths = 20;
|
||||
static const int _topRotationCount = 3;
|
||||
|
||||
final Map<String, ContactPathHistory> _cache = {};
|
||||
bool _isLoaded = false;
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (_isLoaded) return;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_storageKey);
|
||||
if (raw == null || raw.isEmpty) {
|
||||
_isLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
for (final entry in decoded.entries) {
|
||||
final value = entry.value;
|
||||
if (value is Map<String, dynamic>) {
|
||||
_cache[entry.key] = ContactPathHistory.fromJson(entry.key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint('⚠️ [PathHistoryService] Failed to load history: $error');
|
||||
}
|
||||
_isLoaded = true;
|
||||
}
|
||||
|
||||
Future<void> recordLearnedPath(Contact contact) async {
|
||||
await initialize();
|
||||
if (!contact.routeHasPath || contact.routeHopCount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final history = _historyFor(contact.publicKeyHex);
|
||||
final signature = _signature(contact.routePathBytes);
|
||||
final existing = _findDirectPath(history.directPaths, signature);
|
||||
final updated = PathRecord(
|
||||
pathBytes: contact.routePathBytes.toList(),
|
||||
hopCount: contact.routeHopCount,
|
||||
hashSize: contact.routeHashSize,
|
||||
successCount: existing?.successCount ?? 0,
|
||||
failureCount: existing?.failureCount ?? 0,
|
||||
lastRoundTripTimeMs: existing?.lastRoundTripTimeMs ?? 0,
|
||||
lastUsedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
await _saveHistory(
|
||||
contact.publicKeyHex,
|
||||
history.copyWith(
|
||||
directPaths: _upsertDirectPath(history.directPaths, updated),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> recordReceivedBytePath(
|
||||
String contactPublicKeyHex,
|
||||
List<int> pathBytes,
|
||||
int hashSize,
|
||||
) async {
|
||||
await initialize();
|
||||
if (pathBytes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (hashSize < 1 || hashSize > 3) {
|
||||
return;
|
||||
}
|
||||
if (pathBytes.length % hashSize != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final history = _historyFor(contactPublicKeyHex);
|
||||
final signature = pathBytes
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final existing = _findDirectPath(history.directPaths, signature);
|
||||
final updated = PathRecord(
|
||||
pathBytes: List<int>.from(pathBytes),
|
||||
hopCount: pathBytes.length ~/ hashSize,
|
||||
hashSize: hashSize,
|
||||
successCount: existing?.successCount ?? 0,
|
||||
failureCount: existing?.failureCount ?? 0,
|
||||
lastRoundTripTimeMs: existing?.lastRoundTripTimeMs ?? 0,
|
||||
lastUsedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
await _saveHistory(
|
||||
contactPublicKeyHex,
|
||||
history.copyWith(
|
||||
directPaths: _upsertDirectPath(history.directPaths, updated),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<PathSelection> getSelectionForContact(
|
||||
Contact contact, {
|
||||
required bool autoRouteRotationEnabled,
|
||||
}) async {
|
||||
await initialize();
|
||||
await recordLearnedPath(contact);
|
||||
|
||||
if (contact.routeHasPath && contact.routeHopCount > 0) {
|
||||
return PathSelection(
|
||||
mode: PathSelectionMode.directCurrent,
|
||||
pathBytes: Uint8List.fromList(contact.routePathBytes),
|
||||
hopCount: contact.routeHopCount,
|
||||
hashSize: contact.routeHashSize,
|
||||
);
|
||||
}
|
||||
|
||||
if (!autoRouteRotationEnabled) {
|
||||
return PathSelection.flood();
|
||||
}
|
||||
|
||||
final history = _historyFor(contact.publicKeyHex);
|
||||
final ranked = List<PathRecord>.from(history.directPaths)
|
||||
..sort(_comparePathRecords);
|
||||
final topPaths = ranked.take(_topRotationCount).toList();
|
||||
if (topPaths.isEmpty) {
|
||||
final nextFloodHistory = history.copyWith(
|
||||
rotationIndex: history.rotationIndex + 1,
|
||||
);
|
||||
await _saveHistory(contact.publicKeyHex, nextFloodHistory);
|
||||
return PathSelection.flood();
|
||||
}
|
||||
|
||||
final selections =
|
||||
topPaths
|
||||
.map(
|
||||
(record) => PathSelection(
|
||||
mode: PathSelectionMode.directHistorical,
|
||||
pathBytes: Uint8List.fromList(record.pathBytes),
|
||||
hopCount: record.hopCount,
|
||||
hashSize: record.hashSize,
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
..add(PathSelection.flood());
|
||||
|
||||
final index = history.rotationIndex % selections.length;
|
||||
final updatedHistory = history.copyWith(
|
||||
rotationIndex: history.rotationIndex + 1,
|
||||
);
|
||||
await _saveHistory(contact.publicKeyHex, updatedHistory);
|
||||
return selections[index];
|
||||
}
|
||||
|
||||
Future<void> recordPathResult(
|
||||
String contactPublicKeyHex,
|
||||
PathSelection selection, {
|
||||
required bool success,
|
||||
int? roundTripTimeMs,
|
||||
}) async {
|
||||
await initialize();
|
||||
final history = _historyFor(contactPublicKeyHex);
|
||||
if (selection.usesFlood) {
|
||||
final current = history.floodStats;
|
||||
await _saveHistory(
|
||||
contactPublicKeyHex,
|
||||
history.copyWith(
|
||||
floodStats: current.copyWith(
|
||||
successCount: current.successCount + (success ? 1 : 0),
|
||||
failureCount: current.failureCount + (success ? 0 : 1),
|
||||
lastRoundTripTimeMs: success
|
||||
? (roundTripTimeMs ?? current.lastRoundTripTimeMs)
|
||||
: current.lastRoundTripTimeMs,
|
||||
lastUsedAt: DateTime.now(),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final signature = _signature(selection.pathBytes);
|
||||
final existing = _findDirectPath(history.directPaths, signature);
|
||||
final updated = PathRecord(
|
||||
pathBytes: selection.pathBytes.toList(),
|
||||
hopCount: selection.hopCount,
|
||||
hashSize: selection.hashSize,
|
||||
successCount: (existing?.successCount ?? 0) + (success ? 1 : 0),
|
||||
failureCount: (existing?.failureCount ?? 0) + (success ? 0 : 1),
|
||||
lastRoundTripTimeMs: success
|
||||
? (roundTripTimeMs ?? existing?.lastRoundTripTimeMs ?? 0)
|
||||
: (existing?.lastRoundTripTimeMs ?? 0),
|
||||
lastUsedAt: DateTime.now(),
|
||||
);
|
||||
await _saveHistory(
|
||||
contactPublicKeyHex,
|
||||
history.copyWith(
|
||||
directPaths: _upsertDirectPath(history.directPaths, updated),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ContactPathHistory historyFor(String contactPublicKeyHex) {
|
||||
return _cache[contactPublicKeyHex] ??
|
||||
ContactPathHistory.empty(contactPublicKeyHex);
|
||||
}
|
||||
|
||||
ContactPathHistory _historyFor(String contactPublicKeyHex) {
|
||||
return _cache.putIfAbsent(
|
||||
contactPublicKeyHex,
|
||||
() => ContactPathHistory.empty(contactPublicKeyHex),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveHistory(
|
||||
String contactPublicKeyHex,
|
||||
ContactPathHistory history,
|
||||
) async {
|
||||
_cache[contactPublicKeyHex] = history;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final payload = <String, dynamic>{};
|
||||
for (final entry in _cache.entries) {
|
||||
payload[entry.key] = entry.value.toJson();
|
||||
}
|
||||
await prefs.setString(_storageKey, jsonEncode(payload));
|
||||
}
|
||||
|
||||
List<PathRecord> _upsertDirectPath(
|
||||
List<PathRecord> existing,
|
||||
PathRecord updatedRecord,
|
||||
) {
|
||||
final updated = List<PathRecord>.from(existing)
|
||||
..removeWhere((record) => record.signature == updatedRecord.signature)
|
||||
..insert(0, updatedRecord);
|
||||
if (updated.length > _maxDirectPaths) {
|
||||
return updated.take(_maxDirectPaths).toList();
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
int _comparePathRecords(PathRecord a, PathRecord b) {
|
||||
final successRateCompare = b.successRate.compareTo(a.successRate);
|
||||
if (successRateCompare != 0) return successRateCompare;
|
||||
|
||||
final successCountCompare = b.successCount.compareTo(a.successCount);
|
||||
if (successCountCompare != 0) return successCountCompare;
|
||||
|
||||
final aRtt = a.lastRoundTripTimeMs == 0 ? 1 << 30 : a.lastRoundTripTimeMs;
|
||||
final bRtt = b.lastRoundTripTimeMs == 0 ? 1 << 30 : b.lastRoundTripTimeMs;
|
||||
final rttCompare = aRtt.compareTo(bRtt);
|
||||
if (rttCompare != 0) return rttCompare;
|
||||
|
||||
return b.lastUsedAt.compareTo(a.lastUsedAt);
|
||||
}
|
||||
|
||||
PathRecord? _findDirectPath(List<PathRecord> records, String signature) {
|
||||
for (final record in records) {
|
||||
if (record.signature == signature) {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _signature(Uint8List bytes) =>
|
||||
bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
@@ -4,38 +4,49 @@ import '../models/contact.dart';
|
||||
|
||||
class DecodedLogRxRoute {
|
||||
final int payloadType;
|
||||
final List<int> pathHashes;
|
||||
final List<int> pathBytes;
|
||||
final int hashSize;
|
||||
|
||||
const DecodedLogRxRoute({
|
||||
required this.payloadType,
|
||||
required this.pathHashes,
|
||||
required this.pathBytes,
|
||||
required this.hashSize,
|
||||
});
|
||||
|
||||
int? get originalSenderHash => pathHashes.isEmpty ? null : pathHashes.first;
|
||||
List<String> get hopHashes =>
|
||||
LogRxRouteDecoder.splitHopHashes(pathBytes, hashSize: hashSize);
|
||||
|
||||
int get hopCount => hopHashes.length;
|
||||
|
||||
String? get originalSenderHashHex =>
|
||||
hopHashes.isEmpty ? null : hopHashes.first;
|
||||
}
|
||||
|
||||
class ResolvedNodeHash {
|
||||
final int hash;
|
||||
final String hashHex;
|
||||
final String label;
|
||||
final bool isOwnNode;
|
||||
final bool isUniqueMatch;
|
||||
final int matchCount;
|
||||
|
||||
const ResolvedNodeHash({
|
||||
required this.hash,
|
||||
required this.hashHex,
|
||||
required this.label,
|
||||
required this.isOwnNode,
|
||||
required this.isUniqueMatch,
|
||||
required this.matchCount,
|
||||
});
|
||||
|
||||
String get hexLabel => '0x${hash.toRadixString(16).padLeft(2, '0')}';
|
||||
String get hexLabel => '0x${hashHex.toUpperCase()}';
|
||||
}
|
||||
|
||||
class LogRxRouteDecoder {
|
||||
const LogRxRouteDecoder._();
|
||||
|
||||
static DecodedLogRxRoute? decode(Uint8List rawData) {
|
||||
static DecodedLogRxRoute? decode(
|
||||
Uint8List rawData, {
|
||||
int? preferredHashSize,
|
||||
}) {
|
||||
if (rawData.length < 5 || rawData[0] != 0x88) return null;
|
||||
|
||||
final rawPacketData = rawData.sublist(3);
|
||||
@@ -54,28 +65,79 @@ class LogRxRouteDecoder {
|
||||
if (rawPacketData.length <= index) return null;
|
||||
final pathLen = rawPacketData[index++];
|
||||
if (rawPacketData.length < index + pathLen) return null;
|
||||
final pathBytes = rawPacketData.sublist(index, index + pathLen);
|
||||
final hashSize = inferHashSize(
|
||||
pathBytes,
|
||||
preferredHashSize: preferredHashSize,
|
||||
);
|
||||
|
||||
return DecodedLogRxRoute(
|
||||
payloadType: payloadType,
|
||||
pathHashes: rawPacketData.sublist(index, index + pathLen),
|
||||
pathBytes: pathBytes,
|
||||
hashSize: hashSize,
|
||||
);
|
||||
}
|
||||
|
||||
static int inferHashSize(List<int> pathBytes, {int? preferredHashSize}) {
|
||||
if (pathBytes.isEmpty) return 1;
|
||||
|
||||
final normalizedPreferred =
|
||||
preferredHashSize != null &&
|
||||
preferredHashSize >= 1 &&
|
||||
preferredHashSize <= 3
|
||||
? preferredHashSize
|
||||
: null;
|
||||
if (normalizedPreferred != null &&
|
||||
pathBytes.length % normalizedPreferred == 0) {
|
||||
return normalizedPreferred;
|
||||
}
|
||||
|
||||
for (final candidate in const [3, 2, 1]) {
|
||||
if (pathBytes.length % candidate == 0) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static List<String> splitHopHashes(
|
||||
List<int> pathBytes, {
|
||||
required int hashSize,
|
||||
}) {
|
||||
if (pathBytes.isEmpty) return const [];
|
||||
if (hashSize < 1 || hashSize > 3 || pathBytes.length % hashSize != 0) {
|
||||
return pathBytes
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.toList();
|
||||
}
|
||||
|
||||
final hops = <String>[];
|
||||
for (var index = 0; index < pathBytes.length; index += hashSize) {
|
||||
hops.add(
|
||||
pathBytes
|
||||
.sublist(index, index + hashSize)
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.join(),
|
||||
);
|
||||
}
|
||||
return hops;
|
||||
}
|
||||
|
||||
static ResolvedNodeHash resolveHash(
|
||||
int hash, {
|
||||
String hashHex, {
|
||||
required Iterable<Contact> contacts,
|
||||
Uint8List? ownPublicKey,
|
||||
String? ownName,
|
||||
}) {
|
||||
final ownHash = ownPublicKey != null && ownPublicKey.isNotEmpty
|
||||
? ownPublicKey.first
|
||||
: null;
|
||||
if (ownHash == hash) {
|
||||
final normalizedHashHex = hashHex.toLowerCase();
|
||||
final ownKeyHex = _bytesToHex(ownPublicKey);
|
||||
if (ownKeyHex != null && ownKeyHex.startsWith(normalizedHashHex)) {
|
||||
final ownLabel = (ownName != null && ownName.trim().isNotEmpty)
|
||||
? '$ownName (you)'
|
||||
: 'You';
|
||||
return ResolvedNodeHash(
|
||||
hash: hash,
|
||||
hashHex: normalizedHashHex,
|
||||
label: ownLabel,
|
||||
isOwnNode: true,
|
||||
isUniqueMatch: true,
|
||||
@@ -84,12 +146,12 @@ class LogRxRouteDecoder {
|
||||
}
|
||||
|
||||
final matches = contacts.where((contact) {
|
||||
return contact.publicKey.isNotEmpty && contact.publicKey.first == hash;
|
||||
return contact.publicKeyHex.toLowerCase().startsWith(normalizedHashHex);
|
||||
}).toList();
|
||||
|
||||
if (matches.isEmpty) {
|
||||
return ResolvedNodeHash(
|
||||
hash: hash,
|
||||
hashHex: normalizedHashHex,
|
||||
label: 'Unknown',
|
||||
isOwnNode: false,
|
||||
isUniqueMatch: false,
|
||||
@@ -99,7 +161,7 @@ class LogRxRouteDecoder {
|
||||
|
||||
if (matches.length == 1) {
|
||||
return ResolvedNodeHash(
|
||||
hash: hash,
|
||||
hashHex: normalizedHashHex,
|
||||
label: matches.first.displayName,
|
||||
isOwnNode: false,
|
||||
isUniqueMatch: true,
|
||||
@@ -119,11 +181,19 @@ class LogRxRouteDecoder {
|
||||
? '$candidateNames +$extraCount'
|
||||
: candidateNames;
|
||||
return ResolvedNodeHash(
|
||||
hash: hash,
|
||||
hashHex: normalizedHashHex,
|
||||
label: label,
|
||||
isOwnNode: false,
|
||||
isUniqueMatch: false,
|
||||
matchCount: matches.length,
|
||||
);
|
||||
}
|
||||
|
||||
static String? _bytesToHex(Uint8List? bytes) {
|
||||
if (bytes == null || bytes.isEmpty) return null;
|
||||
return bytes
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.join()
|
||||
.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/message.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../providers/messages_provider.dart';
|
||||
|
||||
/// Extension for Message to provide localized delivery status
|
||||
extension MessageLocalization on Message {
|
||||
/// Get localized delivery status text
|
||||
String getLocalizedDeliveryStatus(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final routeMetadata = context
|
||||
.read<MessagesProvider>()
|
||||
.getMessageRouteMetadata(id);
|
||||
|
||||
// For channel messages, show echo count instead of delivery status
|
||||
if (isChannelMessage && deliveryStatus == MessageDeliveryStatus.sent) {
|
||||
@@ -26,29 +31,51 @@ extension MessageLocalization on Message {
|
||||
case MessageDeliveryStatus.sending:
|
||||
if (isContactMessage) {
|
||||
if (retryAttempt > 0) {
|
||||
return '${l10n.pending} • ${l10n.retryAttempt} $retryAttempt/3';
|
||||
final routeSuffix = routeMetadata != null
|
||||
? ' • ${routeMetadata.modeLabel}'
|
||||
: '';
|
||||
return '${l10n.pending} • ${l10n.retryAttempt} $retryAttempt/4$routeSuffix';
|
||||
}
|
||||
return l10n.pending;
|
||||
return routeMetadata == null
|
||||
? l10n.pending
|
||||
: '${l10n.pending} • ${routeMetadata.modeLabel}';
|
||||
}
|
||||
return l10n.sending;
|
||||
case MessageDeliveryStatus.sent:
|
||||
return l10n.sent;
|
||||
return routeMetadata == null
|
||||
? l10n.sent
|
||||
: '${l10n.sent} • ${routeMetadata.modeLabel}';
|
||||
case MessageDeliveryStatus.delivered:
|
||||
if (retryAttempt > 0 && roundTripTimeMs != null) {
|
||||
return '${l10n.deliveredWithTime(roundTripTimeMs!)} • ${l10n.retryAttempt} $retryAttempt/3';
|
||||
final routeSuffix = routeMetadata != null
|
||||
? ' • ${routeMetadata.modeLabel}'
|
||||
: '';
|
||||
return '${l10n.deliveredWithTime(roundTripTimeMs!)} • ${l10n.retryAttempt} $retryAttempt/4$routeSuffix';
|
||||
}
|
||||
if (retryAttempt > 0) {
|
||||
return '${l10n.delivered} • ${l10n.retryAttempt} $retryAttempt/3';
|
||||
final routeSuffix = routeMetadata != null
|
||||
? ' • ${routeMetadata.modeLabel}'
|
||||
: '';
|
||||
return '${l10n.delivered} • ${l10n.retryAttempt} $retryAttempt/4$routeSuffix';
|
||||
}
|
||||
if (roundTripTimeMs != null) {
|
||||
return l10n.deliveredWithTime(roundTripTimeMs!);
|
||||
return routeMetadata == null
|
||||
? l10n.deliveredWithTime(roundTripTimeMs!)
|
||||
: '${l10n.deliveredWithTime(roundTripTimeMs!)} • ${routeMetadata.modeLabel}';
|
||||
}
|
||||
return l10n.delivered;
|
||||
return routeMetadata == null
|
||||
? l10n.delivered
|
||||
: '${l10n.delivered} • ${routeMetadata.modeLabel}';
|
||||
case MessageDeliveryStatus.failed:
|
||||
if (retryAttempt > 0) {
|
||||
return '${l10n.failed} • ${l10n.retryAttempt} $retryAttempt/3';
|
||||
final routeSuffix = routeMetadata != null
|
||||
? ' • ${routeMetadata.modeLabel}'
|
||||
: '';
|
||||
return '${l10n.failed} • ${l10n.retryAttempt} $retryAttempt/4$routeSuffix';
|
||||
}
|
||||
return l10n.failed;
|
||||
return routeMetadata == null
|
||||
? l10n.failed
|
||||
: '${l10n.failed} • ${routeMetadata.modeLabel}';
|
||||
case MessageDeliveryStatus.received:
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../models/contact.dart';
|
||||
import '../../providers/app_provider.dart';
|
||||
import '../../services/route_hash_preferences.dart';
|
||||
|
||||
class ContactRouteDialogResult {
|
||||
@@ -57,6 +59,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
|
||||
int _selectedHashSize = RouteHashPreferences.defaultHashSize;
|
||||
ParsedContactRoute? _parsedRoute;
|
||||
String? _errorText;
|
||||
bool _showRoutingInfo = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -133,6 +136,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appProvider = context.watch<AppProvider>();
|
||||
final routeCandidates =
|
||||
widget.availableContacts
|
||||
.where((contact) => contact.isRepeater || contact.isRoom)
|
||||
@@ -184,6 +188,19 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
_AutomationRoutingInfo(
|
||||
isExpanded: _showRoutingInfo,
|
||||
onToggle: () {
|
||||
setState(() {
|
||||
_showRoutingInfo = !_showRoutingInfo;
|
||||
});
|
||||
},
|
||||
autoRouteRotationEnabled: appProvider.autoRouteRotationEnabled,
|
||||
nearestRelayFallbackEnabled:
|
||||
appProvider.nearestRelayFallbackEnabled,
|
||||
clearPathOnMaxRetry: appProvider.clearPathOnMaxRetry,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Pick hops from contacts',
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
@@ -203,10 +220,6 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(candidate.displayName),
|
||||
subtitle: Text(
|
||||
'1B ${_tokenFor(candidate, 1)} • 2B ${_tokenFor(candidate, 2)} • 3B ${_tokenFor(candidate, 3)}',
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
),
|
||||
trailing: TextButton(
|
||||
onPressed: () => _appendHop(candidate),
|
||||
child: Text(
|
||||
@@ -248,3 +261,134 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AutomationRoutingInfo extends StatelessWidget {
|
||||
final bool isExpanded;
|
||||
final VoidCallback onToggle;
|
||||
final bool autoRouteRotationEnabled;
|
||||
final bool nearestRelayFallbackEnabled;
|
||||
final bool clearPathOnMaxRetry;
|
||||
|
||||
const _AutomationRoutingInfo({
|
||||
required this.isExpanded,
|
||||
required this.onToggle,
|
||||
required this.autoRouteRotationEnabled,
|
||||
required this.nearestRelayFallbackEnabled,
|
||||
required this.clearPathOnMaxRetry,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.45),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: onToggle,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 18,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Automatic direct-send routing',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isExpanded ? Icons.expand_less : Icons.expand_more,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isExpanded) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Room/contact sends keep one selected path for the whole send chain, retry up to 5 total attempts with 1s, 2s, 4s, and 8s backoff, then try one final nearest repeater if everything else fails.',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Public and channel broadcasts are not affected by this automation.',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_InfoChip(
|
||||
label: autoRouteRotationEnabled
|
||||
? 'Auto route rotation on'
|
||||
: 'Auto route rotation off',
|
||||
icon: Icons.swap_horiz,
|
||||
),
|
||||
_InfoChip(
|
||||
label: nearestRelayFallbackEnabled
|
||||
? 'Nearest repeater fallback on'
|
||||
: 'Nearest repeater fallback off',
|
||||
icon: Icons.router,
|
||||
),
|
||||
_InfoChip(
|
||||
label: clearPathOnMaxRetry
|
||||
? 'Clear path on max retry on'
|
||||
: 'Clear path on max retry off',
|
||||
icon: Icons.route,
|
||||
),
|
||||
],
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Shows retry, rotation, and final repeater fallback behavior.',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoChip extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
|
||||
const _InfoChip({required this.label, required this.icon});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 14),
|
||||
const SizedBox(width: 6),
|
||||
Text(label, style: Theme.of(context).textTheme.labelMedium),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import '../../models/room_login_state.dart';
|
||||
import '../../providers/connection_provider.dart';
|
||||
import '../../providers/contacts_provider.dart';
|
||||
import '../../providers/map_provider.dart';
|
||||
import '../../providers/messages_provider.dart';
|
||||
import '../../services/message_destination_preferences.dart';
|
||||
import 'contact_route_dialog.dart';
|
||||
import 'room_login_sheet.dart';
|
||||
import '../common/contact_avatar.dart';
|
||||
@@ -21,6 +23,7 @@ class ContactTile extends StatelessWidget {
|
||||
final double Function(double, double, double, double)? calculateDistance;
|
||||
final String Function(double)? formatDistance;
|
||||
final VoidCallback? onNavigateToMap;
|
||||
final VoidCallback? onNavigateToMessages;
|
||||
|
||||
const ContactTile({
|
||||
super.key,
|
||||
@@ -29,6 +32,7 @@ class ContactTile extends StatelessWidget {
|
||||
this.calculateDistance,
|
||||
this.formatDistance,
|
||||
this.onNavigateToMap,
|
||||
this.onNavigateToMessages,
|
||||
});
|
||||
|
||||
/// Get localized time since last seen
|
||||
@@ -68,42 +72,7 @@ class ContactTile extends StatelessWidget {
|
||||
final roomLoginState = contact.type == ContactType.room
|
||||
? connectionProvider.getRoomLoginState(contact.publicKeyPrefix)
|
||||
: null;
|
||||
final trailing = contact.isChannel && !contact.isPublicChannel
|
||||
? PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (value) {
|
||||
if (value == 'delete') {
|
||||
_showDeleteChannelDialog(context, contact);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete, color: Colors.red, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.deleteChannel,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: null;
|
||||
void handleTap() {
|
||||
if (contact.type == ContactType.chat) {
|
||||
_showSetRouteDialog(context, contact);
|
||||
} else if (contact.type == ContactType.repeater) {
|
||||
_jumpToMapForRepeater(context, contact);
|
||||
} else if (contact.type == ContactType.room && !contact.isPublicChannel) {
|
||||
_showRoomLoginDialog(context, contact);
|
||||
} else {
|
||||
_showContactDetails(context, contact);
|
||||
}
|
||||
}
|
||||
void handleTap() => _handlePrimaryTap(context, contact);
|
||||
|
||||
final onLongPress = isPingInProgress
|
||||
? null
|
||||
@@ -280,10 +249,6 @@ class ContactTile extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
if (trailing != null) ...[
|
||||
const SizedBox(width: 2),
|
||||
trailing,
|
||||
],
|
||||
],
|
||||
),
|
||||
subtitleWidget,
|
||||
@@ -298,6 +263,97 @@ class ContactTile extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _handlePrimaryTap(BuildContext context, Contact contact) {
|
||||
if (contact.type == ContactType.repeater) {
|
||||
_showContactDetails(context, contact);
|
||||
return;
|
||||
}
|
||||
|
||||
_showContactActionSheet(context, contact);
|
||||
}
|
||||
|
||||
void _showContactActionSheet(BuildContext context, Contact contact) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final canMessage =
|
||||
contact.type == ContactType.chat ||
|
||||
contact.type == ContactType.room ||
|
||||
contact.type == ContactType.channel;
|
||||
final canSetPath =
|
||||
contact.type == ContactType.chat || contact.type == ContactType.room;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.message_outlined),
|
||||
title: Text(l10n.messages),
|
||||
enabled: canMessage,
|
||||
onTap: !canMessage
|
||||
? null
|
||||
: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
await _openMessagesForContact(context, contact);
|
||||
},
|
||||
),
|
||||
if (canSetPath)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.alt_route),
|
||||
title: const Text('Set path'),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_showSetRouteDialog(context, contact);
|
||||
},
|
||||
),
|
||||
if (!contact.isPublicChannel)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete, color: Colors.red),
|
||||
title: Text(
|
||||
contact.isChannel ? l10n.deleteChannel : l10n.deleteContact,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
if (contact.isChannel) {
|
||||
_showDeleteChannelDialog(context, contact);
|
||||
} else {
|
||||
_showDeleteConfirmation(context, contact);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openMessagesForContact(
|
||||
BuildContext context,
|
||||
Contact contact,
|
||||
) async {
|
||||
final messagesProvider = context.read<MessagesProvider>();
|
||||
final destinationType = contact.type == ContactType.channel
|
||||
? MessageDestinationPreferences.destinationTypeChannel
|
||||
: contact.type == ContactType.room
|
||||
? MessageDestinationPreferences.destinationTypeRoom
|
||||
: MessageDestinationPreferences.destinationTypeContact;
|
||||
|
||||
await MessageDestinationPreferences.setDestination(
|
||||
destinationType,
|
||||
recipientPublicKey: contact.publicKeyHex,
|
||||
);
|
||||
messagesProvider.navigateToDestination(
|
||||
destinationType,
|
||||
recipientPublicKeyHex: contact.publicKeyHex,
|
||||
);
|
||||
onNavigateToMessages?.call();
|
||||
}
|
||||
|
||||
void _showRoomLoginDialog(BuildContext context, Contact contact) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@@ -307,24 +363,11 @@ class ContactTile extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _jumpToMapForRepeater(BuildContext context, Contact contact) {
|
||||
final location = contact.displayLocation;
|
||||
if (location != null) {
|
||||
final mapProvider = context.read<MapProvider>();
|
||||
|
||||
// Navigate to map location
|
||||
mapProvider.navigateToLocation(
|
||||
location: LatLng(location.latitude, location.longitude),
|
||||
zoom: 15.0,
|
||||
animate: true,
|
||||
);
|
||||
|
||||
// Switch to map tab using callback
|
||||
onNavigateToMap?.call();
|
||||
}
|
||||
}
|
||||
|
||||
void _showDeleteConfirmation(BuildContext context, Contact contact) {
|
||||
void _showDeleteConfirmation(
|
||||
BuildContext context,
|
||||
Contact contact, {
|
||||
bool closeDetailsSheetOnDelete = false,
|
||||
}) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
@@ -342,7 +385,9 @@ class ContactTile extends StatelessWidget {
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context); // Close confirmation dialog
|
||||
Navigator.pop(context); // Close contact details sheet
|
||||
if (closeDetailsSheetOnDelete) {
|
||||
Navigator.pop(context); // Close contact details sheet
|
||||
}
|
||||
await _deleteContact(context, contact);
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
@@ -836,11 +881,27 @@ class ContactTile extends StatelessWidget {
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () =>
|
||||
_showDeleteConfirmation(context, contact),
|
||||
onPressed: () {
|
||||
if (contact.isChannel) {
|
||||
_showDeleteChannelDialog(
|
||||
context,
|
||||
contact,
|
||||
closeDetailsSheetOnDelete: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_showDeleteConfirmation(
|
||||
context,
|
||||
contact,
|
||||
closeDetailsSheetOnDelete: true,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
label: Text(
|
||||
AppLocalizations.of(context)!.deleteContact,
|
||||
contact.isChannel
|
||||
? AppLocalizations.of(context)!.deleteChannel
|
||||
: AppLocalizations.of(context)!.deleteContact,
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
@@ -1190,7 +1251,15 @@ class ContactTile extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// Show delete channel confirmation dialog
|
||||
void _showDeleteChannelDialog(BuildContext context, Contact contact) {
|
||||
void _showDeleteChannelDialog(
|
||||
BuildContext context,
|
||||
Contact contact, {
|
||||
bool closeDetailsSheetOnDelete = false,
|
||||
}) {
|
||||
if (contact.isPublicChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
showDialog(
|
||||
@@ -1206,6 +1275,9 @@ class ContactTile extends StatelessWidget {
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(dialogContext).pop();
|
||||
if (closeDetailsSheetOnDelete) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
try {
|
||||
// Extract channel index from pseudo public key
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../../providers/connection_provider.dart';
|
||||
import '../../providers/drawing_provider.dart';
|
||||
import '../../providers/voice_provider.dart';
|
||||
import '../../providers/image_provider.dart' as ip;
|
||||
import '../../providers/app_provider.dart';
|
||||
import '../drawing_minimap_preview.dart';
|
||||
import '../../models/ble_packet_log.dart';
|
||||
import '../../services/sar_template_service.dart';
|
||||
@@ -44,6 +45,7 @@ import 'system_message_bubble.dart';
|
||||
class MessageBubble extends StatefulWidget {
|
||||
final Message message;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onReply;
|
||||
final bool isHighlighted;
|
||||
final VoidCallback? onNavigateToMap;
|
||||
|
||||
@@ -54,6 +56,7 @@ class MessageBubble extends StatefulWidget {
|
||||
super.key,
|
||||
required this.message,
|
||||
this.onTap,
|
||||
this.onReply,
|
||||
this.isHighlighted = false,
|
||||
this.onNavigateToMap,
|
||||
this.isCompact = false,
|
||||
@@ -209,6 +212,15 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!isOwnMessage && widget.onReply != null)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.reply),
|
||||
title: Text(AppLocalizations.of(context)!.reply),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
widget.onReply?.call();
|
||||
},
|
||||
),
|
||||
// Copy text option
|
||||
ListTile(
|
||||
leading: const Icon(Icons.copy),
|
||||
@@ -469,6 +481,9 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
final retryCause = _retryCauseLabel(widget.message);
|
||||
final retryResult = _retryResultLabel(widget.message);
|
||||
final retryMode = _retryModeLabel(widget.message);
|
||||
final routeMetadata = context
|
||||
.read<MessagesProvider>()
|
||||
.getMessageRouteMetadata(widget.message.id);
|
||||
|
||||
final rawLines = <String>[
|
||||
'Message ID: ${widget.message.id}',
|
||||
@@ -790,7 +805,7 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
_detailRow(
|
||||
context,
|
||||
label: l10n.retryAttempt,
|
||||
value: '${widget.message.retryAttempt}/3',
|
||||
value: '${widget.message.retryAttempt}/4',
|
||||
),
|
||||
if (widget.message.lastRetryAt != null)
|
||||
_detailRow(
|
||||
@@ -809,6 +824,20 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
label: l10n.floodFallback,
|
||||
value: l10n.yes,
|
||||
),
|
||||
if (routeMetadata?.relayName case final relayName?)
|
||||
_detailRow(
|
||||
context,
|
||||
label: 'Relay',
|
||||
value: relayName,
|
||||
),
|
||||
if (routeMetadata?.canonicalPath
|
||||
case final routePath?)
|
||||
_detailRow(
|
||||
context,
|
||||
label: 'Selected path',
|
||||
value: routePath,
|
||||
onCopy: () => copyField(routePath),
|
||||
),
|
||||
if (retryResult != null)
|
||||
_detailRow(
|
||||
context,
|
||||
@@ -885,7 +914,9 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
_detailRow(
|
||||
context,
|
||||
label: l10n.envelope,
|
||||
value: envelope != null ? 'VE3 compact' : l10n.unknown,
|
||||
value: envelope != null
|
||||
? 'VE3 compact'
|
||||
: l10n.unknown,
|
||||
),
|
||||
if (voiceSession != null)
|
||||
_detailRow(
|
||||
@@ -1545,8 +1576,15 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
return null;
|
||||
}
|
||||
|
||||
final routeMetadata = context
|
||||
.read<MessagesProvider>()
|
||||
.getMessageRouteMetadata(message.id);
|
||||
if (routeMetadata != null) {
|
||||
return routeMetadata.modeLabel;
|
||||
}
|
||||
|
||||
if (message.usedFloodFallback) {
|
||||
return 'Flood fallback';
|
||||
return 'Flood route';
|
||||
}
|
||||
|
||||
if (message.retryAttempt > 0 || message.expectedAckTag != null) {
|
||||
@@ -1561,9 +1599,17 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
return null;
|
||||
}
|
||||
|
||||
final routeMetadata = context
|
||||
.read<MessagesProvider>()
|
||||
.getMessageRouteMetadata(message.id);
|
||||
final routeLabel = routeMetadata?.modeLabel.toLowerCase();
|
||||
|
||||
if (message.deliveryStatus == MessageDeliveryStatus.delivered) {
|
||||
if (routeLabel != null) {
|
||||
return 'Delivered via $routeLabel';
|
||||
}
|
||||
if (message.usedFloodFallback) {
|
||||
return 'Delivered after flood fallback';
|
||||
return 'Delivered after flood route';
|
||||
}
|
||||
if (message.retryAttempt > 0) {
|
||||
return 'Delivered after retry';
|
||||
@@ -1574,8 +1620,11 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
}
|
||||
|
||||
if (message.deliveryStatus == MessageDeliveryStatus.sending) {
|
||||
if (routeLabel != null) {
|
||||
return '$routeLabel in progress';
|
||||
}
|
||||
if (message.usedFloodFallback) {
|
||||
return 'Flood fallback in progress';
|
||||
return 'Flood route in progress';
|
||||
}
|
||||
if (message.retryAttempt > 0) {
|
||||
return 'Retry in progress';
|
||||
@@ -1586,8 +1635,11 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
}
|
||||
|
||||
if (message.deliveryStatus == MessageDeliveryStatus.failed) {
|
||||
if (routeLabel != null) {
|
||||
return 'Failed via $routeLabel';
|
||||
}
|
||||
if (message.usedFloodFallback) {
|
||||
return 'Failed after flood fallback';
|
||||
return 'Failed after flood route';
|
||||
}
|
||||
if (message.retryAttempt > 0) {
|
||||
return 'Failed after retry attempts';
|
||||
@@ -1598,6 +1650,36 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget _buildDirectMessageStatusIndicator(
|
||||
BuildContext context,
|
||||
Message message,
|
||||
) {
|
||||
switch (message.deliveryStatus) {
|
||||
case MessageDeliveryStatus.delivered:
|
||||
return Icon(
|
||||
Icons.done_all,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
case MessageDeliveryStatus.sent:
|
||||
return Icon(
|
||||
Icons.done,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
);
|
||||
case MessageDeliveryStatus.sending:
|
||||
return Icon(Icons.schedule, size: 15, color: Colors.orange);
|
||||
case MessageDeliveryStatus.failed:
|
||||
return const Icon(Icons.error_outline, size: 15, color: Colors.red);
|
||||
case MessageDeliveryStatus.received:
|
||||
return Icon(
|
||||
Icons.done_all,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Display system messages with minimal styling
|
||||
@@ -1615,6 +1697,7 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
}
|
||||
final isSarMarker = message.isSarMarker;
|
||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||
final messageFontScale = context.watch<AppProvider>().messageFontScale;
|
||||
|
||||
// Determine if this is own message
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
@@ -1728,6 +1811,9 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
: null;
|
||||
|
||||
final shouldFloatBubble = widget.isCompact;
|
||||
final baseBodyStyle = Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(fontSize: 14 * messageFontScale);
|
||||
final bubble = ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: shouldFloatBubble
|
||||
@@ -2146,10 +2232,7 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
);
|
||||
|
||||
if (drawing == null) {
|
||||
return Text(
|
||||
message.text,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
);
|
||||
return Text(message.text, style: baseBodyStyle);
|
||||
}
|
||||
|
||||
final String drawingTypeLabel;
|
||||
@@ -2235,10 +2318,7 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
)
|
||||
// Regular message content
|
||||
else if (!message.isDrawing || widget.isCompact)
|
||||
Text(
|
||||
message.text,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Text(message.text, style: baseBodyStyle),
|
||||
|
||||
if (!widget.isCompact &&
|
||||
!isSarMarker &&
|
||||
@@ -2458,33 +2538,42 @@ class _MessageBubbleState extends State<MessageBubble> {
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Icon(
|
||||
getDeliveryStatusIcon(message.deliveryStatus),
|
||||
size: 12,
|
||||
color: getDeliveryStatusColor(
|
||||
message.deliveryStatus,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
message.getLocalizedDeliveryStatus(context),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall
|
||||
?.copyWith(
|
||||
color: getDeliveryStatusColor(
|
||||
message.deliveryStatus,
|
||||
),
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
if (message.isContactMessage)
|
||||
_buildDirectMessageStatusIndicator(
|
||||
context,
|
||||
message,
|
||||
)
|
||||
else ...[
|
||||
Icon(
|
||||
getDeliveryStatusIcon(message.deliveryStatus),
|
||||
size: 12,
|
||||
color: getDeliveryStatusColor(
|
||||
message.deliveryStatus,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
message.getLocalizedDeliveryStatus(
|
||||
context,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall
|
||||
?.copyWith(
|
||||
color: getDeliveryStatusColor(
|
||||
message.deliveryStatus,
|
||||
),
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
// Show retry button for failed messages
|
||||
if (message.deliveryStatus ==
|
||||
MessageDeliveryStatus.failed) ...[
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../models/message.dart';
|
||||
import '../../models/path_selection.dart';
|
||||
import '../../models/message_reception_details.dart';
|
||||
import '../../providers/messages_provider.dart';
|
||||
|
||||
IconData getDeliveryStatusIcon(MessageDeliveryStatus status) {
|
||||
switch (status) {
|
||||
@@ -185,6 +188,9 @@ Widget buildSentDirectSignalStatus(
|
||||
required int roundTripTimeMs,
|
||||
required Duration txEstimate,
|
||||
}) {
|
||||
final routeMetadata = context
|
||||
.read<MessagesProvider>()
|
||||
.getMessageRouteMetadata(message.id);
|
||||
final estimatedTransmitMs = sanitizeEstimatedTransmitMs(
|
||||
estimatedTransmitMs: txEstimate > Duration.zero
|
||||
? txEstimate.inMilliseconds
|
||||
@@ -230,7 +236,7 @@ Widget buildSentDirectSignalStatus(
|
||||
_techChip(
|
||||
context,
|
||||
icon: Icons.refresh,
|
||||
label: 'retry ${message.retryAttempt}/3',
|
||||
label: 'retry ${message.retryAttempt}/4',
|
||||
color: Colors.redAccent,
|
||||
),
|
||||
if (message.suggestedTimeoutMs != null)
|
||||
@@ -244,7 +250,7 @@ Widget buildSentDirectSignalStatus(
|
||||
_techChip(
|
||||
context,
|
||||
icon: Icons.waves,
|
||||
label: 'flood fallback',
|
||||
label: 'flood route',
|
||||
color: Colors.teal,
|
||||
)
|
||||
else if (message.expectedAckTag != null)
|
||||
@@ -254,6 +260,17 @@ Widget buildSentDirectSignalStatus(
|
||||
label: 'direct ACK',
|
||||
color: Colors.indigo,
|
||||
),
|
||||
if (routeMetadata != null)
|
||||
_techChip(
|
||||
context,
|
||||
icon: routeMetadata.mode == PathSelectionMode.nearestRouter
|
||||
? Icons.router
|
||||
: Icons.alt_route,
|
||||
label: routeMetadata.modeLabel,
|
||||
color: routeMetadata.mode == PathSelectionMode.nearestRouter
|
||||
? Colors.deepPurple
|
||||
: Colors.indigo,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import '../../models/message.dart';
|
||||
import '../../providers/connection_provider.dart';
|
||||
import '../../providers/contacts_provider.dart';
|
||||
import '../../services/mesh_map_nodes_service.dart';
|
||||
import '../../services/route_hash_preferences.dart';
|
||||
import '../../utils/log_rx_route_decoder.dart';
|
||||
|
||||
class MessageTraceSheet extends StatefulWidget {
|
||||
final Message message;
|
||||
@@ -32,6 +34,7 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
||||
Future<_TraceResult> _loadTrace() async {
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
final preferredHashSize = await RouteHashPreferences.getHashSize();
|
||||
final packetPath = _extractPathFromPacketLogs(
|
||||
logs: connectionProvider.bleService.packetLogs,
|
||||
message: widget.message,
|
||||
@@ -46,10 +49,14 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
||||
var trace = _buildTraceResult(
|
||||
nodes: localNodes,
|
||||
packetPath: packetPath,
|
||||
preferredHashSize: preferredHashSize,
|
||||
senderPrefix: senderPrefix,
|
||||
recipientPrefix: recipientPrefix,
|
||||
);
|
||||
if (_isCompleteTrace(trace, expectedRelayCount: math.max(0, widget.message.pathLen))) {
|
||||
if (_isCompleteTrace(
|
||||
trace,
|
||||
expectedRelayCount: math.max(0, widget.message.pathLen),
|
||||
)) {
|
||||
return trace;
|
||||
}
|
||||
|
||||
@@ -59,6 +66,7 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
||||
trace = _buildTraceResult(
|
||||
nodes: _mergeNodes(localNodes, remoteNodes),
|
||||
packetPath: packetPath,
|
||||
preferredHashSize: preferredHashSize,
|
||||
senderPrefix: senderPrefix,
|
||||
recipientPrefix: recipientPrefix,
|
||||
);
|
||||
@@ -336,9 +344,7 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
||||
|
||||
if (trace.mode == TraceMode.packetPath) {
|
||||
final entries = trace.matchedPathNodes.asMap().entries.map((entry) {
|
||||
final hashHex = trace.pathHashes[entry.key]
|
||||
.toRadixString(16)
|
||||
.padLeft(2, '0');
|
||||
final hashHex = trace.pathHashes[entry.key].toUpperCase();
|
||||
return _RouteDisplayEntry(
|
||||
node: entry.value,
|
||||
label: entry.value?.name ?? 'Unknown',
|
||||
@@ -428,6 +434,7 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
||||
_TraceResult _buildTraceResult({
|
||||
required List<MeshMapNode> nodes,
|
||||
required List<int>? packetPath,
|
||||
required int preferredHashSize,
|
||||
required String? senderPrefix,
|
||||
required String? recipientPrefix,
|
||||
}) {
|
||||
@@ -435,9 +442,17 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
||||
final recipientNode = _bestNodeForPrefix(nodes, recipientPrefix);
|
||||
|
||||
if (packetPath != null && packetPath.isNotEmpty) {
|
||||
final hashSize = LogRxRouteDecoder.inferHashSize(
|
||||
packetPath,
|
||||
preferredHashSize: preferredHashSize,
|
||||
);
|
||||
final hopHashes = LogRxRouteDecoder.splitHopHashes(
|
||||
packetPath,
|
||||
hashSize: hashSize,
|
||||
);
|
||||
final matched = _matchNodesFromPathHashes(
|
||||
nodes: nodes,
|
||||
pathHashes: packetPath,
|
||||
pathHashes: hopHashes,
|
||||
senderPrefix: senderPrefix,
|
||||
recipientPrefix: recipientPrefix,
|
||||
);
|
||||
@@ -445,7 +460,7 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
||||
mode: TraceMode.packetPath,
|
||||
sender: senderNode,
|
||||
recipient: recipientNode,
|
||||
pathHashes: packetPath,
|
||||
pathHashes: hopHashes,
|
||||
matchedPathNodes: matched,
|
||||
);
|
||||
}
|
||||
@@ -481,7 +496,9 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
||||
trace.matchedPathNodes.every((node) => node != null);
|
||||
}
|
||||
|
||||
final concreteCount = trace.matchedPathNodes.whereType<MeshMapNode>().length;
|
||||
final concreteCount = trace.matchedPathNodes
|
||||
.whereType<MeshMapNode>()
|
||||
.length;
|
||||
return concreteCount >= expectedRelayCount + 2;
|
||||
}
|
||||
|
||||
@@ -523,13 +540,13 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
||||
|
||||
List<MeshMapNode?> _matchNodesFromPathHashes({
|
||||
required List<MeshMapNode> nodes,
|
||||
required List<int> pathHashes,
|
||||
required List<String> pathHashes,
|
||||
required String? senderPrefix,
|
||||
required String? recipientPrefix,
|
||||
}) {
|
||||
final result = <MeshMapNode?>[];
|
||||
for (var i = 0; i < pathHashes.length; i++) {
|
||||
final hashHex = pathHashes[i].toRadixString(16).padLeft(2, '0');
|
||||
final hashHex = pathHashes[i].toLowerCase();
|
||||
final candidates = nodes
|
||||
.where((n) => n.publicKey.startsWith(hashHex))
|
||||
.toList();
|
||||
@@ -618,7 +635,7 @@ class _TraceResult {
|
||||
final TraceMode mode;
|
||||
final MeshMapNode? sender;
|
||||
final MeshMapNode? recipient;
|
||||
final List<int> pathHashes;
|
||||
final List<String> pathHashes;
|
||||
final List<MeshMapNode?> matchedPathNodes;
|
||||
|
||||
const _TraceResult({
|
||||
@@ -645,7 +662,10 @@ class _RouteDisplayEntry {
|
||||
return _RouteDisplayEntry(
|
||||
node: node,
|
||||
label: node.name,
|
||||
keyLabel: node.publicKey.substring(0, math.min(12, node.publicKey.length)),
|
||||
keyLabel: node.publicKey.substring(
|
||||
0,
|
||||
math.min(12, node.publicKey.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../../providers/app_provider.dart';
|
||||
|
||||
class MessagesComposer extends StatelessWidget {
|
||||
final TextEditingController textController;
|
||||
@@ -253,6 +255,10 @@ class _MessageInput extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final messageFontScale = context.watch<AppProvider>().messageFontScale;
|
||||
const baseFontSize = 15.0;
|
||||
final resolvedFontSize = baseFontSize * messageFontScale;
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
constraints: const BoxConstraints(minHeight: 46, maxHeight: 132),
|
||||
@@ -286,12 +292,12 @@ class _MessageInput extends StatelessWidget {
|
||||
maxLines: 4,
|
||||
keyboardType: TextInputType.multiline,
|
||||
inputFormatters: [messageByteLimiter],
|
||||
style: const TextStyle(fontSize: 15),
|
||||
style: TextStyle(fontSize: resolvedFontSize),
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
hintText: AppLocalizations.of(context)!.typeYourMessage,
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 15,
|
||||
fontSize: resolvedFontSize,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant.withValues(alpha: 0.9),
|
||||
|
||||
@@ -14,6 +14,7 @@ class MessagesContent extends StatelessWidget {
|
||||
final Future<void> Function() onRefresh;
|
||||
final VoidCallback? onNavigateToMap;
|
||||
final ValueChanged<Message>? onMessageTap;
|
||||
final ValueChanged<Message>? onReplyToMessage;
|
||||
|
||||
const MessagesContent({
|
||||
super.key,
|
||||
@@ -24,6 +25,7 @@ class MessagesContent extends StatelessWidget {
|
||||
required this.onRefresh,
|
||||
this.onNavigateToMap,
|
||||
this.onMessageTap,
|
||||
this.onReplyToMessage,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -86,6 +88,9 @@ class MessagesContent extends StatelessWidget {
|
||||
onTap: onMessageTap == null
|
||||
? null
|
||||
: () => onMessageTap!(message),
|
||||
onReply: onReplyToMessage == null
|
||||
? null
|
||||
: () => onReplyToMessage!(message),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -8,6 +8,8 @@ class RecipientSelectorSheet extends StatefulWidget {
|
||||
final List<Contact> contacts;
|
||||
final List<Contact> rooms;
|
||||
final List<Contact> channels;
|
||||
final int unreadCount;
|
||||
final Map<String, int> unreadCountsByPublicKey;
|
||||
final String? currentDestinationType;
|
||||
final String? currentRecipientPublicKey;
|
||||
final Function(String type, Contact? recipient) onSelect;
|
||||
@@ -17,6 +19,8 @@ class RecipientSelectorSheet extends StatefulWidget {
|
||||
required this.contacts,
|
||||
required this.rooms,
|
||||
required this.channels,
|
||||
required this.unreadCount,
|
||||
required this.unreadCountsByPublicKey,
|
||||
this.currentDestinationType,
|
||||
this.currentRecipientPublicKey,
|
||||
required this.onSelect,
|
||||
@@ -47,7 +51,7 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
|
||||
bool _isSelected(String type, Contact? contact) {
|
||||
if (widget.currentDestinationType != type) return false;
|
||||
if (contact == null) return false;
|
||||
if (contact == null) return widget.currentRecipientPublicKey == null;
|
||||
return contact.publicKeyHex == widget.currentRecipientPublicKey;
|
||||
}
|
||||
|
||||
@@ -138,6 +142,19 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
_buildOptionTile(
|
||||
context: context,
|
||||
icon: Icons.all_inbox,
|
||||
title: l10n.showAll,
|
||||
subtitle: 'All messages',
|
||||
unreadCount: widget.unreadCount,
|
||||
isSelected: _isSelected('all', null),
|
||||
onTap: () {
|
||||
widget.onSelect('all', null);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
// Channels section
|
||||
if (widget.channels.isNotEmpty) ...[
|
||||
Padding(
|
||||
@@ -174,6 +191,10 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
subtitle: channel.isPublicChannel
|
||||
? l10n.broadcastToAllNearby
|
||||
: '${l10n.channel} ${channel.publicKey[1]}', // Show slot number
|
||||
unreadCount:
|
||||
widget.unreadCountsByPublicKey[channel
|
||||
.publicKeyHex] ??
|
||||
0,
|
||||
isSelected: _isSelected('channel', channel),
|
||||
onTap: () {
|
||||
widget.onSelect('channel', channel);
|
||||
@@ -219,6 +240,10 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
contact: contact,
|
||||
title: contact.displayName,
|
||||
subtitle: contact.publicKeyShort,
|
||||
unreadCount:
|
||||
widget.unreadCountsByPublicKey[contact
|
||||
.publicKeyHex] ??
|
||||
0,
|
||||
isSelected: _isSelected('contact', contact),
|
||||
onTap: () {
|
||||
widget.onSelect('contact', contact);
|
||||
@@ -264,6 +289,9 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
contact: room,
|
||||
title: room.displayName,
|
||||
subtitle: room.publicKeyShort,
|
||||
unreadCount:
|
||||
widget.unreadCountsByPublicKey[room.publicKeyHex] ??
|
||||
0,
|
||||
isSelected: _isSelected('room', room),
|
||||
onTap: () {
|
||||
widget.onSelect('room', room);
|
||||
@@ -314,6 +342,7 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
required Contact contact,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required int unreadCount,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
@@ -339,13 +368,95 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
||||
).textTheme.bodySmall?.color?.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
trailing: isSelected
|
||||
? Icon(
|
||||
Icons.check_circle,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
trailing: _buildTrailing(
|
||||
context,
|
||||
unreadCount: unreadCount,
|
||||
isSelected: isSelected,
|
||||
),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOptionTile({
|
||||
required BuildContext context,
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required int unreadCount,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
child: Icon(icon, color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
style: const TextStyle(fontSize: 12, fontFamily: 'monospace').copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.color?.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
trailing: _buildTrailing(
|
||||
context,
|
||||
unreadCount: unreadCount,
|
||||
isSelected: isSelected,
|
||||
),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildTrailing(
|
||||
BuildContext context, {
|
||||
required int unreadCount,
|
||||
required bool isSelected,
|
||||
}) {
|
||||
if (unreadCount <= 0 && !isSelected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (unreadCount > 0)
|
||||
Container(
|
||||
key: Key('unread-badge-$unreadCount'),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
unreadCount > 99 ? '99+' : '$unreadCount',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (unreadCount > 0 && isSelected) const SizedBox(width: 8),
|
||||
if (isSelected)
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 2026.0308.1+21
|
||||
version: 2026.0308.3+23
|
||||
|
||||
environment:
|
||||
sdk: ^3.9.2
|
||||
|
||||
@@ -4,7 +4,10 @@ import 'package:fake_async/fake_async.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/models/contact.dart';
|
||||
import 'package:meshcore_sar_app/models/message.dart';
|
||||
import 'package:meshcore_sar_app/models/path_selection.dart';
|
||||
import 'package:meshcore_sar_app/providers/helpers/message_retry_manager.dart';
|
||||
import 'package:meshcore_sar_app/providers/messages_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
Contact _buildContact() {
|
||||
return Contact(
|
||||
@@ -39,6 +42,10 @@ Message _buildDirectMessage(String id) {
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
group('MessagesProvider retransmission', () {
|
||||
test('direct messages become sent before delivery ACK arrives', () {
|
||||
final provider = MessagesProvider();
|
||||
@@ -171,7 +178,11 @@ void main() {
|
||||
);
|
||||
expect(retryCalls, 0);
|
||||
|
||||
async.elapse(const Duration(seconds: 4));
|
||||
async.elapse(const Duration(milliseconds: 998));
|
||||
async.flushMicrotasks();
|
||||
expect(retryCalls, 0);
|
||||
|
||||
async.elapse(const Duration(milliseconds: 1));
|
||||
async.flushMicrotasks();
|
||||
|
||||
expect(retryCalls, 1);
|
||||
@@ -237,68 +248,89 @@ void main() {
|
||||
expect(provider.messages.single.retryAttempt, 0);
|
||||
});
|
||||
|
||||
test('repeated max-retry failures request path reset', () async {
|
||||
test(
|
||||
'final router fallback runs after all normal retries are exhausted',
|
||||
() async {
|
||||
final provider = MessagesProvider();
|
||||
final fallbackCalls = <String>[];
|
||||
provider.onFinalRouterFallbackCallback =
|
||||
({required messageId, required contact, required message}) async {
|
||||
fallbackCalls.add(messageId);
|
||||
return true;
|
||||
};
|
||||
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage(
|
||||
'm5',
|
||||
).copyWith(retryAttempt: MessageRetryManager.maxRetryAttempts),
|
||||
contact: _buildContact(),
|
||||
);
|
||||
|
||||
provider.markMessageFailed('m5');
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(fallbackCalls, ['m5']);
|
||||
expect(
|
||||
provider.messages.single.deliveryStatus,
|
||||
MessageDeliveryStatus.sending,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('final router fallback is not retried twice', () async {
|
||||
final provider = MessagesProvider();
|
||||
final contact = _buildContact();
|
||||
final resetRequests = <(String, int)>[];
|
||||
provider.onDirectPathFailedCallback =
|
||||
({required contact, required failureStreak}) async {
|
||||
resetRequests.add((contact.advName, failureStreak));
|
||||
};
|
||||
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage(
|
||||
'm5',
|
||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
||||
contact: contact,
|
||||
);
|
||||
provider.markMessageFailed('m5');
|
||||
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage(
|
||||
'm6',
|
||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
||||
contact: contact,
|
||||
).copyWith(retryAttempt: MessageRetryManager.maxRetryAttempts),
|
||||
contact: _buildContact(),
|
||||
);
|
||||
provider.updateMessageRouteSelection(
|
||||
'm6',
|
||||
PathSelection.flood(),
|
||||
routerFallbackAttempted: true,
|
||||
);
|
||||
|
||||
provider.markMessageFailed('m6');
|
||||
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(resetRequests, [('Teammate', 2)]);
|
||||
expect(
|
||||
provider.messages.single.deliveryStatus,
|
||||
MessageDeliveryStatus.failed,
|
||||
);
|
||||
});
|
||||
|
||||
test('successful delivery clears path failure streak', () async {
|
||||
final provider = MessagesProvider();
|
||||
final contact = _buildContact();
|
||||
final resetRequests = <int>[];
|
||||
provider.onDirectPathFailedCallback =
|
||||
({required contact, required failureStreak}) async {
|
||||
resetRequests.add(failureStreak);
|
||||
};
|
||||
test(
|
||||
'final permanent failure callback runs after router fallback failure',
|
||||
() async {
|
||||
final provider = MessagesProvider();
|
||||
final failedMessageIds = <String>[];
|
||||
provider.onFinalDirectMessageFailureCallback =
|
||||
({required messageId, required contact, required message}) async {
|
||||
failedMessageIds.add(messageId);
|
||||
};
|
||||
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage(
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage(
|
||||
'm7',
|
||||
).copyWith(retryAttempt: MessageRetryManager.maxRetryAttempts),
|
||||
contact: _buildContact(),
|
||||
);
|
||||
provider.updateMessageRouteSelection(
|
||||
'm7',
|
||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
||||
contact: contact,
|
||||
);
|
||||
provider.markMessageFailed('m7');
|
||||
PathSelection.flood(),
|
||||
routerFallbackAttempted: true,
|
||||
);
|
||||
|
||||
provider.addSentMessage(_buildDirectMessage('m8'), contact: contact);
|
||||
provider.markMessageSent('m8', 123, 10);
|
||||
provider.markMessageDelivered(123, 150);
|
||||
provider.markMessageFailed('m7');
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
provider.addSentMessage(
|
||||
_buildDirectMessage(
|
||||
'm9',
|
||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
||||
contact: contact,
|
||||
);
|
||||
provider.markMessageFailed('m9');
|
||||
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(resetRequests, isEmpty);
|
||||
});
|
||||
expect(failedMessageIds, ['m7']);
|
||||
expect(
|
||||
provider.messages.single.deliveryStatus,
|
||||
MessageDeliveryStatus.failed,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
40
test/services/messaging_route_preferences_test.dart
Normal file
40
test/services/messaging_route_preferences_test.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:meshcore_sar_app/services/messaging_route_preferences.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
test('route preference defaults are disabled', () async {
|
||||
expect(
|
||||
await MessagingRoutePreferences.getAutoRouteRotationEnabled(),
|
||||
isFalse,
|
||||
);
|
||||
expect(await MessagingRoutePreferences.getClearPathOnMaxRetry(), isFalse);
|
||||
expect(
|
||||
await MessagingRoutePreferences.getNearestRelayFallbackEnabled(),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('route preferences persist changes', () async {
|
||||
await MessagingRoutePreferences.setAutoRouteRotationEnabled(true);
|
||||
await MessagingRoutePreferences.setClearPathOnMaxRetry(true);
|
||||
await MessagingRoutePreferences.setNearestRelayFallbackEnabled(false);
|
||||
|
||||
expect(
|
||||
await MessagingRoutePreferences.getAutoRouteRotationEnabled(),
|
||||
isTrue,
|
||||
);
|
||||
expect(await MessagingRoutePreferences.getClearPathOnMaxRetry(), isTrue);
|
||||
expect(
|
||||
await MessagingRoutePreferences.getNearestRelayFallbackEnabled(),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
}
|
||||
115
test/services/nearest_router_selector_test.dart
Normal file
115
test/services/nearest_router_selector_test.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
import 'package:meshcore_sar_app/models/contact.dart';
|
||||
import 'package:meshcore_sar_app/services/nearest_router_selector.dart';
|
||||
|
||||
Contact _buildRepeater({
|
||||
required int seed,
|
||||
required String name,
|
||||
required double latitude,
|
||||
required double longitude,
|
||||
required int lastAdvert,
|
||||
int outPathLen = -1,
|
||||
}) {
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(List<int>.generate(32, (i) => i + seed)),
|
||||
type: ContactType.repeater,
|
||||
flags: 0,
|
||||
outPathLen: outPathLen,
|
||||
outPath: Uint8List(0),
|
||||
advName: name,
|
||||
lastAdvert: lastAdvert,
|
||||
advLat: (latitude * 1e6).round(),
|
||||
advLon: (longitude * 1e6).round(),
|
||||
lastMod: lastAdvert,
|
||||
);
|
||||
}
|
||||
|
||||
Position _position(double latitude, double longitude) {
|
||||
return Position(
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
timestamp: DateTime.now(),
|
||||
accuracy: 1,
|
||||
altitude: 0,
|
||||
altitudeAccuracy: 1,
|
||||
heading: 0,
|
||||
headingAccuracy: 1,
|
||||
speed: 0,
|
||||
speedAccuracy: 0,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('selector chooses nearest eligible repeater', () {
|
||||
final selector = NearestRouterSelector();
|
||||
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
final recipient = _buildRepeater(
|
||||
seed: 90,
|
||||
name: 'Recipient',
|
||||
latitude: 46.05,
|
||||
longitude: 14.50,
|
||||
lastAdvert: now,
|
||||
).copyWith(type: ContactType.chat);
|
||||
|
||||
final selected = selector.select(
|
||||
senderPosition: _position(46.0569, 14.5058),
|
||||
repeaters: [
|
||||
_buildRepeater(
|
||||
seed: 1,
|
||||
name: 'Far',
|
||||
latitude: 46.10,
|
||||
longitude: 14.60,
|
||||
lastAdvert: now,
|
||||
outPathLen: 1,
|
||||
),
|
||||
_buildRepeater(
|
||||
seed: 2,
|
||||
name: 'Near',
|
||||
latitude: 46.0570,
|
||||
longitude: 14.5060,
|
||||
lastAdvert: now - 5,
|
||||
outPathLen: 1,
|
||||
),
|
||||
],
|
||||
recipient: recipient,
|
||||
);
|
||||
|
||||
expect(selected?.advName, 'Near');
|
||||
});
|
||||
|
||||
test('selector skips stale repeaters', () {
|
||||
final selector = NearestRouterSelector();
|
||||
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
final staleAdvert = now - (11 * 60);
|
||||
final recipient = _buildRepeater(
|
||||
seed: 91,
|
||||
name: 'Recipient',
|
||||
latitude: 46.05,
|
||||
longitude: 14.50,
|
||||
lastAdvert: now,
|
||||
).copyWith(type: ContactType.chat);
|
||||
|
||||
final selected = selector.select(
|
||||
senderPosition: _position(46.0569, 14.5058),
|
||||
repeaters: [
|
||||
_buildRepeater(
|
||||
seed: 3,
|
||||
name: 'Stale',
|
||||
latitude: 46.0570,
|
||||
longitude: 14.5060,
|
||||
lastAdvert: staleAdvert,
|
||||
outPathLen: 1,
|
||||
),
|
||||
],
|
||||
recipient: recipient,
|
||||
);
|
||||
|
||||
expect(selected, isNull);
|
||||
});
|
||||
}
|
||||
175
test/services/path_history_service_test.dart
Normal file
175
test/services/path_history_service_test.dart
Normal file
@@ -0,0 +1,175 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:meshcore_sar_app/models/contact.dart';
|
||||
import 'package:meshcore_sar_app/models/path_selection.dart';
|
||||
import 'package:meshcore_sar_app/services/path_history_service.dart';
|
||||
|
||||
Contact _buildContact({
|
||||
required int seed,
|
||||
required List<int> pathBytes,
|
||||
required int hopCount,
|
||||
required int hashSize,
|
||||
}) {
|
||||
final encoded = ((hashSize - 1) << 6) | (hopCount & 0x3F);
|
||||
final outPath = Uint8List(ContactRouteCodec.maxPathBytes)
|
||||
..setRange(0, pathBytes.length, pathBytes);
|
||||
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(List<int>.generate(32, (i) => i + seed)),
|
||||
type: ContactType.chat,
|
||||
flags: 0,
|
||||
outPathLen: ContactRouteCodec.toSignedDescriptor(encoded),
|
||||
outPath: outPath,
|
||||
advName: 'Contact $seed',
|
||||
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
advLat: 0,
|
||||
advLon: 0,
|
||||
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
);
|
||||
}
|
||||
|
||||
Contact _buildContactWithoutRoute({required int seed}) {
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(List<int>.generate(32, (i) => i + seed)),
|
||||
type: ContactType.chat,
|
||||
flags: 0,
|
||||
outPathLen: -1,
|
||||
outPath: Uint8List(0),
|
||||
advName: 'Contact $seed',
|
||||
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
advLat: 0,
|
||||
advLon: 0,
|
||||
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
});
|
||||
|
||||
test('auto rotation ranks best paths before flood', () async {
|
||||
final service = PathHistoryService();
|
||||
final contact = _buildContactWithoutRoute(seed: 0);
|
||||
final best = PathSelection(
|
||||
mode: PathSelectionMode.directHistorical,
|
||||
pathBytes: Uint8List.fromList([0xAA, 0xBB]),
|
||||
hopCount: 2,
|
||||
hashSize: 1,
|
||||
);
|
||||
final second = PathSelection(
|
||||
mode: PathSelectionMode.directHistorical,
|
||||
pathBytes: Uint8List.fromList([0xCC, 0xDD]),
|
||||
hopCount: 2,
|
||||
hashSize: 1,
|
||||
);
|
||||
|
||||
await service.initialize();
|
||||
await service.recordLearnedPath(contact);
|
||||
await service.recordPathResult(
|
||||
contact.publicKeyHex,
|
||||
best,
|
||||
success: true,
|
||||
roundTripTimeMs: 120,
|
||||
);
|
||||
await service.recordPathResult(
|
||||
contact.publicKeyHex,
|
||||
best,
|
||||
success: true,
|
||||
roundTripTimeMs: 110,
|
||||
);
|
||||
await service.recordPathResult(
|
||||
contact.publicKeyHex,
|
||||
second,
|
||||
success: true,
|
||||
roundTripTimeMs: 200,
|
||||
);
|
||||
await service.recordPathResult(
|
||||
contact.publicKeyHex,
|
||||
second,
|
||||
success: false,
|
||||
);
|
||||
|
||||
final first = await service.getSelectionForContact(
|
||||
contact,
|
||||
autoRouteRotationEnabled: true,
|
||||
);
|
||||
final third = await service.getSelectionForContact(
|
||||
contact,
|
||||
autoRouteRotationEnabled: true,
|
||||
);
|
||||
final secondPick = await service.getSelectionForContact(
|
||||
contact,
|
||||
autoRouteRotationEnabled: true,
|
||||
);
|
||||
|
||||
expect(first.mode, PathSelectionMode.directHistorical);
|
||||
expect(first.canonicalPath, 'AA,BB');
|
||||
expect(third.mode, PathSelectionMode.directHistorical);
|
||||
expect(third.canonicalPath, 'CC,DD');
|
||||
expect(secondPick.mode, PathSelectionMode.flood);
|
||||
});
|
||||
|
||||
test(
|
||||
'current learned route is reused first even with rotation enabled',
|
||||
() async {
|
||||
final service = PathHistoryService();
|
||||
final contact = _buildContact(
|
||||
seed: 9,
|
||||
pathBytes: [0xAA, 0xBB, 0xCC],
|
||||
hopCount: 1,
|
||||
hashSize: 3,
|
||||
);
|
||||
|
||||
await service.initialize();
|
||||
await service.recordPathResult(
|
||||
contact.publicKeyHex,
|
||||
PathSelection(
|
||||
mode: PathSelectionMode.directHistorical,
|
||||
pathBytes: Uint8List.fromList([0x11, 0x22, 0x33]),
|
||||
hopCount: 1,
|
||||
hashSize: 3,
|
||||
),
|
||||
success: true,
|
||||
roundTripTimeMs: 90,
|
||||
);
|
||||
|
||||
final selection = await service.getSelectionForContact(
|
||||
contact,
|
||||
autoRouteRotationEnabled: true,
|
||||
);
|
||||
|
||||
expect(selection.mode, PathSelectionMode.directCurrent);
|
||||
expect(selection.canonicalPath, 'AABBCC');
|
||||
},
|
||||
);
|
||||
|
||||
test('no history falls back to flood', () async {
|
||||
final service = PathHistoryService();
|
||||
final contact = _buildContactWithoutRoute(seed: 0);
|
||||
|
||||
final selection = await service.getSelectionForContact(
|
||||
contact,
|
||||
autoRouteRotationEnabled: true,
|
||||
);
|
||||
|
||||
expect(selection.mode, PathSelectionMode.flood);
|
||||
});
|
||||
|
||||
test('received public byte path is added to history', () async {
|
||||
final service = PathHistoryService();
|
||||
await service.initialize();
|
||||
await service.recordReceivedBytePath('abc123', [0x01, 0x02, 0x03], 3);
|
||||
|
||||
final history = service.historyFor('abc123');
|
||||
expect(history.directPaths, hasLength(1));
|
||||
expect(history.directPaths.single.pathBytes, [0x01, 0x02, 0x03]);
|
||||
expect(history.directPaths.single.hashSize, 3);
|
||||
expect(history.directPaths.single.hopCount, 1);
|
||||
});
|
||||
}
|
||||
@@ -24,15 +24,39 @@ void main() {
|
||||
|
||||
expect(decoded, isNotNull);
|
||||
expect(decoded!.payloadType, 0x01);
|
||||
expect(decoded.pathHashes, [0xc2, 0xba, 0x5f, 0xde]);
|
||||
expect(decoded.originalSenderHash, 0xc2);
|
||||
expect(decoded.pathBytes, [0xc2, 0xba, 0x5f, 0xde]);
|
||||
expect(decoded.hashSize, 2);
|
||||
expect(decoded.hopHashes, ['c2ba', '5fde']);
|
||||
expect(decoded.originalSenderHashHex, 'c2ba');
|
||||
});
|
||||
|
||||
test('uses preferred hash size when packet length is ambiguous', () {
|
||||
final packet = Uint8List.fromList([
|
||||
0x88,
|
||||
0x37,
|
||||
0xae,
|
||||
0x09,
|
||||
0x06,
|
||||
0xaa,
|
||||
0xbb,
|
||||
0xcc,
|
||||
0xdd,
|
||||
0xee,
|
||||
0xff,
|
||||
]);
|
||||
|
||||
final decoded = LogRxRouteDecoder.decode(packet, preferredHashSize: 1);
|
||||
|
||||
expect(decoded, isNotNull);
|
||||
expect(decoded!.hashSize, 1);
|
||||
expect(decoded.hopHashes, ['aa', 'bb', 'cc', 'dd', 'ee', 'ff']);
|
||||
});
|
||||
});
|
||||
|
||||
group('LogRxRouteDecoder.resolveHash', () {
|
||||
test('prefers own node when hash matches device key', () {
|
||||
final resolved = LogRxRouteDecoder.resolveHash(
|
||||
0xc2,
|
||||
'c201',
|
||||
contacts: const [],
|
||||
ownPublicKey: Uint8List.fromList([0xc2, 0x01, 0x02]),
|
||||
ownName: 'Base',
|
||||
@@ -44,8 +68,10 @@ void main() {
|
||||
|
||||
test('resolves unique contact by first public key byte', () {
|
||||
final resolved = LogRxRouteDecoder.resolveHash(
|
||||
0xc2,
|
||||
contacts: [_contact(name: 'Alpha', keyPrefix: 0xc2)],
|
||||
'c211',
|
||||
contacts: [
|
||||
_contact(name: 'Alpha', keyPrefix: [0xc2, 0x11]),
|
||||
],
|
||||
);
|
||||
|
||||
expect(resolved.isUniqueMatch, isTrue);
|
||||
@@ -54,10 +80,10 @@ void main() {
|
||||
|
||||
test('marks ambiguous matches without pretending certainty', () {
|
||||
final resolved = LogRxRouteDecoder.resolveHash(
|
||||
0xc2,
|
||||
'c2',
|
||||
contacts: [
|
||||
_contact(name: 'Alpha', keyPrefix: 0xc2),
|
||||
_contact(name: 'Bravo', keyPrefix: 0xc2),
|
||||
_contact(name: 'Alpha', keyPrefix: [0xc2, 0x11]),
|
||||
_contact(name: 'Bravo', keyPrefix: [0xc2, 0x22]),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -67,10 +93,10 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
Contact _contact({required String name, required int keyPrefix}) {
|
||||
Contact _contact({required String name, required List<int> keyPrefix}) {
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList([
|
||||
keyPrefix,
|
||||
...keyPrefix,
|
||||
0x11,
|
||||
0x22,
|
||||
0x33,
|
||||
|
||||
74
test/widgets/recipient_selector_sheet_test.dart
Normal file
74
test/widgets/recipient_selector_sheet_test.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_sar_app/l10n/app_localizations.dart';
|
||||
import 'package:meshcore_sar_app/models/contact.dart';
|
||||
import 'package:meshcore_sar_app/widgets/messages/recipient_selector_sheet.dart';
|
||||
|
||||
void main() {
|
||||
Contact buildContact({
|
||||
required String name,
|
||||
required ContactType type,
|
||||
int secondByte = 0,
|
||||
}) {
|
||||
final publicKey = Uint8List(32);
|
||||
publicKey[1] = secondByte;
|
||||
|
||||
return Contact(
|
||||
publicKey: publicKey,
|
||||
type: type,
|
||||
flags: 0,
|
||||
outPathLen: 0,
|
||||
outPath: Uint8List(0),
|
||||
advName: name,
|
||||
lastAdvert: 0,
|
||||
advLat: 0,
|
||||
advLon: 0,
|
||||
lastMod: 0,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> pumpSheet(WidgetTester tester) async {
|
||||
final channel = buildContact(
|
||||
name: 'Ops',
|
||||
type: ContactType.channel,
|
||||
secondByte: 3,
|
||||
);
|
||||
final contact = buildContact(name: 'John Smith', type: ContactType.chat);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: RecipientSelectorSheet(
|
||||
contacts: [contact],
|
||||
rooms: const [],
|
||||
channels: [channel],
|
||||
unreadCount: 11,
|
||||
unreadCountsByPublicKey: {
|
||||
channel.publicKeyHex: 7,
|
||||
contact.publicKeyHex: 3,
|
||||
},
|
||||
currentDestinationType: 'all',
|
||||
onSelect: (_, __) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('renders unread badges for all and destination filters', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpSheet(tester);
|
||||
|
||||
expect(find.byKey(const Key('unread-badge-11')), findsOneWidget);
|
||||
expect(find.byKey(const Key('unread-badge-7')), findsOneWidget);
|
||||
expect(find.byKey(const Key('unread-badge-3')), findsOneWidget);
|
||||
expect(find.text('11'), findsOneWidget);
|
||||
expect(find.text('7'), findsOneWidget);
|
||||
expect(find.text('3'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user