mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-16 02:40:29 +00:00
Compare commits
31 Commits
v2026.0308
...
v2026.0310
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9deb563c05 | ||
|
|
15ac3e2798 | ||
|
|
97addf7ba6 | ||
|
|
025bd737e7 | ||
|
|
07b675dec6 | ||
|
|
d675ce18dd | ||
|
|
a8e31bb2e8 | ||
|
|
3cd96ba961 | ||
|
|
a98fc6314e | ||
|
|
ae1ea71f58 | ||
|
|
4dff79cd7b | ||
|
|
cbcb329c70 | ||
|
|
7abf96abf2 | ||
|
|
13bf74178c | ||
|
|
66a00c9569 | ||
|
|
b60fa008eb | ||
|
|
d113f7a881 | ||
|
|
2ab8681ac4 | ||
|
|
de5fb1e164 | ||
|
|
dae7e67a8a | ||
|
|
d51924021f | ||
|
|
0f91292fd7 | ||
|
|
b471ab0990 | ||
|
|
771cddb80d | ||
|
|
bfd15065f0 | ||
|
|
f72db76538 | ||
|
|
a96f96ca4c | ||
|
|
9f62055eef | ||
|
|
6b2f18130c | ||
|
|
922b40b755 | ||
|
|
17d7c43745 |
2
.github/workflows/build-artifacts.yml
vendored
2
.github/workflows/build-artifacts.yml
vendored
@@ -280,7 +280,7 @@ jobs:
|
|||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|
||||||
- name: Build web release
|
- name: Build web release
|
||||||
run: flutter build web --release --base-href /meshcore_sar_app/
|
run: flutter build web --release --base-href /meshcore-sar/
|
||||||
|
|
||||||
- name: Upload pages artifact
|
- name: Upload pages artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
MeshCore SAR helps teams coordinate in low-connectivity or no-connectivity environments with messaging, voice, images, maps, and live location context in one app.
|
MeshCore SAR helps teams coordinate in low-connectivity or no-connectivity environments with messaging, voice, images, maps, and live location context in one app.
|
||||||
It uses the MeshCore protocol over LoRa for long-range, infrastructure-free communication.
|
It uses the MeshCore protocol over LoRa for long-range, infrastructure-free communication.
|
||||||
`iOS TestFlight:` https://testflight.apple.com/join/HhzerdHp
|
`iOS TestFlight:` https://testflight.apple.com/join/nCZYMPPz
|
||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
|
|
||||||
|
|||||||
@@ -489,7 +489,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 105;
|
CURRENT_PROJECT_VERSION = 107;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -511,7 +511,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 105;
|
CURRENT_PROJECT_VERSION = 107;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
@@ -530,7 +530,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 105;
|
CURRENT_PROJECT_VERSION = 107;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
@@ -547,7 +547,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 105;
|
CURRENT_PROJECT_VERSION = 107;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
@@ -679,7 +679,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 105;
|
CURRENT_PROJECT_VERSION = 107;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -702,7 +702,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 105;
|
CURRENT_PROJECT_VERSION = 107;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Flutter
|
import Flutter
|
||||||
|
import UserNotifications
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
@@ -8,6 +9,7 @@ import Flutter
|
|||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>105</string>
|
<string>107</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
1396
lib/l10n/app_de.arb
1396
lib/l10n/app_de.arb
File diff suppressed because it is too large
Load Diff
2617
lib/l10n/app_el.arb
2617
lib/l10n/app_el.arb
File diff suppressed because it is too large
Load Diff
1726
lib/l10n/app_en.arb
1726
lib/l10n/app_en.arb
File diff suppressed because it is too large
Load Diff
1385
lib/l10n/app_es.arb
1385
lib/l10n/app_es.arb
File diff suppressed because it is too large
Load Diff
1390
lib/l10n/app_fr.arb
1390
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
1390
lib/l10n/app_it.arb
1390
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
|
@override
|
||||||
String get disconnect => 'Trennen';
|
String get disconnect => 'Trennen';
|
||||||
|
|
||||||
@override
|
|
||||||
String get scanningForDevices => 'Suche nach Geräten...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDevicesFound => 'Keine Geräte gefunden';
|
String get noDevicesFound => 'Keine Geräte gefunden';
|
||||||
|
|
||||||
@@ -63,27 +60,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get failedToGetGpsLocation =>
|
String get failedToGetGpsLocation =>
|
||||||
'GPS-Position konnte nicht abgerufen werden';
|
'GPS-Position konnte nicht abgerufen werden';
|
||||||
|
|
||||||
@override
|
|
||||||
String advertisedAtLocation(String latitude, String longitude) {
|
|
||||||
return 'Position gesendet bei $latitude, $longitude';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String failedToAdvertise(String error) {
|
String failedToAdvertise(String error) {
|
||||||
return 'Senden fehlgeschlagen: $error';
|
return 'Senden fehlgeschlagen: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String reconnecting(int attempt, int max) {
|
|
||||||
return 'Wiederverbindung... ($attempt/$max)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get cancelReconnection => 'Wiederverbindung abbrechen';
|
String get cancelReconnection => 'Wiederverbindung abbrechen';
|
||||||
|
|
||||||
@override
|
|
||||||
String get mapManagement => 'Kartenverwaltung';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get general => 'Allgemein';
|
String get general => 'Allgemein';
|
||||||
|
|
||||||
@@ -130,13 +114,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get displayPacketActivity =>
|
String get displayPacketActivity =>
|
||||||
'Paketaktivitätsindikatoren in der oberen Leiste anzeigen';
|
'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
|
@override
|
||||||
String get disableMap => 'Karte deaktivieren';
|
String get disableMap => 'Karte deaktivieren';
|
||||||
|
|
||||||
@@ -150,74 +127,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chooseLanguage => 'Sprache auswählen';
|
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
|
@override
|
||||||
String get save => 'Speichern';
|
String get save => 'Speichern';
|
||||||
|
|
||||||
@@ -253,12 +162,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get moreInfo => 'Mehr Info';
|
String get moreInfo => 'Mehr Info';
|
||||||
|
|
||||||
@override
|
|
||||||
String get learnMoreAbout => 'Erfahren Sie mehr über MeshCore SAR';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get developer => 'Entwickler';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get packageName => 'Paketname';
|
String get packageName => 'Paketname';
|
||||||
|
|
||||||
@@ -335,9 +238,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get refresh => 'Aktualisieren';
|
String get refresh => 'Aktualisieren';
|
||||||
|
|
||||||
@override
|
|
||||||
String get sendDirectMessage => 'Senden';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get resetPath => 'Pfad zurücksetzen (Umleitung)';
|
String get resetPath => 'Pfad zurücksetzen (Umleitung)';
|
||||||
|
|
||||||
@@ -457,21 +357,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
return '$count Zeichnung$plural teilen';
|
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
|
@override
|
||||||
String get showReceivedDrawings => 'Empfangene Zeichnungen anzeigen';
|
String get showReceivedDrawings => 'Empfangene Zeichnungen anzeigen';
|
||||||
|
|
||||||
@@ -493,9 +378,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get clearAll => 'Alle löschen';
|
String get clearAll => 'Alle löschen';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noLocalDrawings => 'Keine lokalen Zeichnungen zum Teilen';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get publicChannel => 'Öffentlicher Kanal';
|
String get publicChannel => 'Öffentlicher Kanal';
|
||||||
|
|
||||||
@@ -505,37 +387,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get storedPermanently => 'Dauerhaft im Raum gespeichert';
|
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
|
@override
|
||||||
String get notConnectedToDevice => 'Nicht mit Gerät verbunden';
|
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
|
@override
|
||||||
String get typeYourMessage => 'Geben Sie Ihre Nachricht ein...';
|
String get typeYourMessage => 'Geben Sie Ihre Nachricht ein...';
|
||||||
|
|
||||||
@@ -565,9 +419,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get location => 'Standort';
|
String get location => 'Standort';
|
||||||
|
|
||||||
@override
|
|
||||||
String get myLocation => 'Mein Standort';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fromMap => 'Von Karte';
|
String get fromMap => 'Von Karte';
|
||||||
|
|
||||||
@@ -630,9 +481,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get flood => 'Flut';
|
String get flood => 'Flut';
|
||||||
|
|
||||||
@override
|
|
||||||
String get admin => 'Admin';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get loggedIn => 'Angemeldet';
|
String get loggedIn => 'Angemeldet';
|
||||||
|
|
||||||
@@ -642,29 +490,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get distance => 'Entfernung';
|
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
|
@override
|
||||||
String directPingTimeout(String name) {
|
String directPingTimeout(String name) {
|
||||||
return 'Direkter Ping-Timeout - wiederhole $name mit Flutung...';
|
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
|
@override
|
||||||
String pingFailed(String name) {
|
String pingFailed(String name) {
|
||||||
return 'Ping fehlgeschlagen an $name - keine Antwort erhalten';
|
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.';
|
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
|
@override
|
||||||
String failedToRemoveContact(String error) {
|
String failedToRemoveContact(String error) {
|
||||||
return 'Fehler beim Entfernen des Kontakts: $error';
|
return 'Fehler beim Entfernen des Kontakts: $error';
|
||||||
@@ -729,11 +549,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get telemetry => 'Telemetrie';
|
String get telemetry => 'Telemetrie';
|
||||||
|
|
||||||
@override
|
|
||||||
String requestingTelemetry(String name) {
|
|
||||||
return 'Fordere Telemetrie von $name an...';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get voltage => 'Spannung';
|
String get voltage => 'Spannung';
|
||||||
|
|
||||||
@@ -805,9 +620,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get showAll => 'Alle anzeigen';
|
String get showAll => 'Alle anzeigen';
|
||||||
|
|
||||||
@override
|
|
||||||
String get nearbyContacts => 'Kontakte in der Nähe';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationUnavailable => 'Standort nicht verfügbar';
|
String get locationUnavailable => 'Standort nicht verfügbar';
|
||||||
|
|
||||||
@@ -826,7 +638,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String latLonFormat(String latitude, String longitude) {
|
String latLonFormat(String latitude, String longitude) {
|
||||||
return 'Lat: $latitude Lon: $longitude';
|
return 'Breite: $latitude Lange: $longitude';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -848,175 +660,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels => 'Kanäle';
|
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
|
@override
|
||||||
String get selectMapLayer => 'Kartenebene auswählen';
|
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
|
@override
|
||||||
String get openStreetMap => 'OpenStreetMap';
|
String get openStreetMap => 'OpenStreetMap';
|
||||||
|
|
||||||
@@ -1035,12 +681,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get googleTerrain => 'Google Gelände';
|
String get googleTerrain => 'Google Gelände';
|
||||||
|
|
||||||
@override
|
|
||||||
String get downloadVisibleArea => 'Sichtbaren Bereich herunterladen';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get initializingMap => 'Initialisiere Karte...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dragToPosition => 'Zur Position ziehen';
|
String get dragToPosition => 'Zur Position ziehen';
|
||||||
|
|
||||||
@@ -1056,53 +696,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarAlert => 'SAR-ALARM';
|
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
|
@override
|
||||||
String get textCopiedToClipboard => 'Text in Zwischenablage kopiert';
|
String get textCopiedToClipboard => 'Text in Zwischenablage kopiert';
|
||||||
|
|
||||||
@@ -1114,9 +707,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get cannotReplyContactNotFound =>
|
String get cannotReplyContactNotFound =>
|
||||||
'Antwort nicht möglich: Kontakt nicht gefunden';
|
'Antwort nicht möglich: Kontakt nicht gefunden';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messageDeleted => 'Nachricht gelöscht';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get copyText => 'Text kopieren';
|
String get copyText => 'Text kopieren';
|
||||||
|
|
||||||
@@ -1153,12 +743,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarLocationShare => 'SAR Standort';
|
String get sarLocationShare => 'SAR Standort';
|
||||||
|
|
||||||
@override
|
|
||||||
String get locationShared => 'Standort geteilt';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get refreshedContacts => 'Kontakte aktualisiert';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get justNow => 'Gerade eben';
|
String get justNow => 'Gerade eben';
|
||||||
|
|
||||||
@@ -1200,7 +784,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get failed => 'Fehlgeschlagen';
|
String get failed => 'Fehlgeschlagen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get broadcast => 'Broadcast';
|
String get broadcast => 'Rundspruch';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String deliveredToContacts(int delivered, int total) {
|
String deliveredToContacts(int delivered, int total) {
|
||||||
@@ -1260,130 +844,28 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String maxPowerDbm(int power) {
|
String maxPowerDbm(int power) {
|
||||||
return 'Max: $power dBm';
|
return 'Max.: $power dBm';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get you => 'Du';
|
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
|
@override
|
||||||
String exportFailed(String error) {
|
String exportFailed(String error) {
|
||||||
return 'Export failed: $error';
|
return 'Export fehlgeschlagen: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String importFailed(String error) {
|
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
|
@override
|
||||||
String get unknown => 'Unbekannt';
|
String get unknown => 'Unbekannt';
|
||||||
|
|
||||||
@override
|
|
||||||
String get bounds => 'Grenzen';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onlineLayers => 'Online-Ebenen';
|
String get onlineLayers => 'Online-Ebenen';
|
||||||
|
|
||||||
@override
|
|
||||||
String get offlineLayers => 'Offline-Ebenen';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationTrail => 'Standortverlauf';
|
String get locationTrail => 'Standortverlauf';
|
||||||
|
|
||||||
@@ -1423,47 +905,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => 'Verlaufssteuerung';
|
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
|
@override
|
||||||
String get contactTrails => 'Kontaktverläufe';
|
String get contactTrails => 'Kontaktverläufe';
|
||||||
|
|
||||||
@@ -1526,10 +967,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get telemetryAndLocationSharing => 'Telemetrie & Standortfreigabe';
|
String get telemetryAndLocationSharing => 'Telemetrie & Standortfreigabe';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get lat => 'Lat';
|
String get lat => 'Breite';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get lon => 'Lon';
|
String get lon => 'Lange';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get useCurrentLocation => 'Aktuellen Standort verwenden';
|
String get useCurrentLocation => 'Aktuellen Standort verwenden';
|
||||||
@@ -1541,7 +982,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get chatNode => 'Chat-Knoten';
|
String get chatNode => 'Chat-Knoten';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get repeater => 'Repeater';
|
String get repeater => 'Relais';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get roomChannel => 'Raum/Kanal';
|
String get roomChannel => 'Raum/Kanal';
|
||||||
@@ -1585,7 +1026,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get templateName => 'Vorlagenname';
|
String get templateName => 'Vorlagenname';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateNameHint => 'e.g. Found Person';
|
String get templateNameHint => 'z. B. Gefundene Person';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateEmoji => 'Emoji';
|
String get templateEmoji => 'Emoji';
|
||||||
@@ -1597,16 +1038,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get nameRequired => 'Name ist erforderlich';
|
String get nameRequired => 'Name ist erforderlich';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateDescription => 'Description (Optional)';
|
String get templateDescription => 'Beschreibung (optional)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateDescriptionHint => 'Add additional context...';
|
String get templateDescriptionHint => 'Zusatzlichen Kontext hinzufugen...';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateColor => 'Color';
|
String get templateColor => 'Farbe';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get previewFormat => 'Preview (SAR Message Format)';
|
String get previewFormat => 'Vorschau (SAR-Nachrichtenformat)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get importFromClipboard => 'Importieren';
|
String get importFromClipboard => 'Importieren';
|
||||||
@@ -1616,26 +1057,26 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String deleteTemplateConfirmation(String name) {
|
String deleteTemplateConfirmation(String name) {
|
||||||
return 'Delete template \'$name\'?';
|
return 'Vorlage \'$name\' loschen?';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateAdded => 'Template added';
|
String get templateAdded => 'Vorlage hinzugefugt';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateUpdated => 'Template updated';
|
String get templateUpdated => 'Vorlage aktualisiert';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateDeleted => 'Template deleted';
|
String get templateDeleted => 'Vorlage geloscht';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String templatesImported(int count) {
|
String templatesImported(int count) {
|
||||||
String _temp0 = intl.Intl.pluralLogic(
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
count,
|
count,
|
||||||
locale: localeName,
|
locale: localeName,
|
||||||
other: 'Imported $count templates',
|
other: '$count Vorlagen importiert',
|
||||||
one: 'Imported 1 template',
|
one: '1 Vorlage importiert',
|
||||||
zero: 'No templates imported',
|
zero: 'Keine Vorlagen importiert',
|
||||||
);
|
);
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
@@ -1645,8 +1086,8 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String _temp0 = intl.Intl.pluralLogic(
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
count,
|
count,
|
||||||
locale: localeName,
|
locale: localeName,
|
||||||
other: 'Exported $count templates to clipboard',
|
other: '$count Vorlagen in die Zwischenablage exportiert',
|
||||||
one: 'Exported 1 template to clipboard',
|
one: '1 Vorlage in die Zwischenablage exportiert',
|
||||||
);
|
);
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
@@ -1665,10 +1106,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get resetComplete => 'Vorlagen auf Standard zurückgesetzt';
|
String get resetComplete => 'Vorlagen auf Standard zurückgesetzt';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noTemplates => 'No templates available';
|
String get noTemplates => 'Keine Vorlagen verfugbar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get tapAddToCreate => 'Tap + to create your first template';
|
String get tapAddToCreate =>
|
||||||
|
'Tippen Sie auf +, um Ihre erste Vorlage zu erstellen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get ok => 'OK';
|
String get ok => 'OK';
|
||||||
@@ -1736,20 +1178,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get noRoomsFound => 'Keine Räume gefunden';
|
String get noRoomsFound => 'Keine Räume gefunden';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noContactsOrRoomsAvailable =>
|
|
||||||
'Keine Kontakte oder Räume verfügbar';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noRecipientsAvailable => 'Keine Empfänger verfügbar';
|
String get noRecipientsAvailable => 'Keine Empfänger verfügbar';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noChannelsFound => 'Keine Kanäle gefunden';
|
String get noChannelsFound => 'Keine Kanäle gefunden';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messagesWillBeSentToPublicChannel =>
|
|
||||||
'Nachrichten werden an öffentlichen Kanal gesendet';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get newMessage => 'Neue Nachricht';
|
String get newMessage => 'Neue Nachricht';
|
||||||
|
|
||||||
@@ -1932,10 +1366,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get rectangleDrawing => 'Rechteck';
|
String get rectangleDrawing => 'Rechteck';
|
||||||
|
|
||||||
@override
|
|
||||||
String get coordinatesCopiedToClipboard =>
|
|
||||||
'Koordinaten in Zwischenablage kopiert';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get manualCoordinates => 'Manuelle Koordinaten';
|
String get manualCoordinates => 'Manuelle Koordinaten';
|
||||||
|
|
||||||
@@ -1948,31 +1378,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get longitudeLabel => 'Längengrad';
|
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
|
@override
|
||||||
String get exampleCoordinates => 'Beispiel: 46.0569, 14.5058';
|
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
|
@override
|
||||||
String get shareDrawing => 'Zeichnung teilen';
|
String get shareDrawing => 'Zeichnung teilen';
|
||||||
|
|
||||||
@@ -2028,12 +1436,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get forestRoads => 'Waldwege';
|
String get forestRoads => 'Waldwege';
|
||||||
|
|
||||||
@override
|
|
||||||
String get showCadastralParcels => 'Katasterparzellen anzeigen';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get showForestRoads => 'Waldwege anzeigen';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get wmsOverlays => 'WMS Überlagerungen';
|
String get wmsOverlays => 'WMS Überlagerungen';
|
||||||
|
|
||||||
@@ -2138,10 +1540,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
return 'Kanal konnte nicht gelöscht werden: $error';
|
return 'Kanal konnte nicht gelöscht werden: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String get allChannelSlotsInUse =>
|
|
||||||
'Alle Kanalplätze sind belegt (maximal 39 benutzerdefinierte Kanäle)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get createChannel => 'Kanal erstellen';
|
String get createChannel => 'Kanal erstellen';
|
||||||
|
|
||||||
@@ -2183,21 +1581,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String get wizardConnectingFeature3 =>
|
String get wizardConnectingFeature3 =>
|
||||||
'Funktioniert vollständig offline - kein Internet erforderlich';
|
'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
|
@override
|
||||||
String get wizardChannelTitle => 'Kanäle';
|
String get wizardChannelTitle => 'Kanäle';
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -29,9 +29,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disconnect => 'Disconnect';
|
String get disconnect => 'Disconnect';
|
||||||
|
|
||||||
@override
|
|
||||||
String get scanningForDevices => 'Scanning for devices...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDevicesFound => 'No devices found';
|
String get noDevicesFound => 'No devices found';
|
||||||
|
|
||||||
@@ -62,27 +59,14 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get failedToGetGpsLocation => 'Failed to get GPS location';
|
String get failedToGetGpsLocation => 'Failed to get GPS location';
|
||||||
|
|
||||||
@override
|
|
||||||
String advertisedAtLocation(String latitude, String longitude) {
|
|
||||||
return 'Advertised at $latitude, $longitude';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String failedToAdvertise(String error) {
|
String failedToAdvertise(String error) {
|
||||||
return 'Failed to advertise: $error';
|
return 'Failed to advertise: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String reconnecting(int attempt, int max) {
|
|
||||||
return 'Reconnecting... ($attempt/$max)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get cancelReconnection => 'Cancel reconnection';
|
String get cancelReconnection => 'Cancel reconnection';
|
||||||
|
|
||||||
@override
|
|
||||||
String get mapManagement => 'Map Management';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get general => 'General';
|
String get general => 'General';
|
||||||
|
|
||||||
@@ -129,13 +113,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get displayPacketActivity =>
|
String get displayPacketActivity =>
|
||||||
'Display packet activity indicators in top bar';
|
'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
|
@override
|
||||||
String get disableMap => 'Disable Map';
|
String get disableMap => 'Disable Map';
|
||||||
|
|
||||||
@@ -149,74 +126,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chooseLanguage => 'Choose Language';
|
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
|
@override
|
||||||
String get save => 'Save';
|
String get save => 'Save';
|
||||||
|
|
||||||
@@ -252,12 +161,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get moreInfo => 'More Info';
|
String get moreInfo => 'More Info';
|
||||||
|
|
||||||
@override
|
|
||||||
String get learnMoreAbout => 'Learn more about MeshCore SAR';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get developer => 'Developer';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get packageName => 'Package Name';
|
String get packageName => 'Package Name';
|
||||||
|
|
||||||
@@ -333,9 +236,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get refresh => 'Refresh';
|
String get refresh => 'Refresh';
|
||||||
|
|
||||||
@override
|
|
||||||
String get sendDirectMessage => 'Send';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get resetPath => 'Reset Path (Re-route)';
|
String get resetPath => 'Reset Path (Re-route)';
|
||||||
|
|
||||||
@@ -455,21 +355,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
return 'Share $count Drawing$plural';
|
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
|
@override
|
||||||
String get showReceivedDrawings => 'Show Received Drawings';
|
String get showReceivedDrawings => 'Show Received Drawings';
|
||||||
|
|
||||||
@@ -491,9 +376,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get clearAll => 'Clear All';
|
String get clearAll => 'Clear All';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noLocalDrawings => 'No local drawings to share';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get publicChannel => 'Public Channel';
|
String get publicChannel => 'Public Channel';
|
||||||
|
|
||||||
@@ -503,37 +385,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get storedPermanently => 'Stored permanently in room';
|
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
|
@override
|
||||||
String get notConnectedToDevice => 'Not connected to device';
|
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
|
@override
|
||||||
String get typeYourMessage => 'Type your message...';
|
String get typeYourMessage => 'Type your message...';
|
||||||
|
|
||||||
@@ -563,9 +417,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get location => 'Location';
|
String get location => 'Location';
|
||||||
|
|
||||||
@override
|
|
||||||
String get myLocation => 'My Location';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fromMap => 'From Map';
|
String get fromMap => 'From Map';
|
||||||
|
|
||||||
@@ -627,9 +478,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get flood => 'Flood';
|
String get flood => 'Flood';
|
||||||
|
|
||||||
@override
|
|
||||||
String get admin => 'Admin';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get loggedIn => 'Logged In';
|
String get loggedIn => 'Logged In';
|
||||||
|
|
||||||
@@ -639,29 +487,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get distance => 'Distance';
|
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
|
@override
|
||||||
String directPingTimeout(String name) {
|
String directPingTimeout(String name) {
|
||||||
return 'Direct ping timeout - retrying $name with flooding...';
|
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
|
@override
|
||||||
String pingFailed(String name) {
|
String pingFailed(String name) {
|
||||||
return 'Ping failed to $name - no response received';
|
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.';
|
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
|
@override
|
||||||
String failedToRemoveContact(String error) {
|
String failedToRemoveContact(String error) {
|
||||||
return 'Failed to remove contact: $error';
|
return 'Failed to remove contact: $error';
|
||||||
@@ -726,11 +546,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get telemetry => 'Telemetry';
|
String get telemetry => 'Telemetry';
|
||||||
|
|
||||||
@override
|
|
||||||
String requestingTelemetry(String name) {
|
|
||||||
return 'Requesting telemetry from $name...';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get voltage => 'Voltage';
|
String get voltage => 'Voltage';
|
||||||
|
|
||||||
@@ -802,9 +617,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get showAll => 'Show All';
|
String get showAll => 'Show All';
|
||||||
|
|
||||||
@override
|
|
||||||
String get nearbyContacts => 'Nearby Contacts';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationUnavailable => 'Location unavailable';
|
String get locationUnavailable => 'Location unavailable';
|
||||||
|
|
||||||
@@ -845,173 +657,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels => 'Channels';
|
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
|
@override
|
||||||
String get selectMapLayer => 'Select Map Layer';
|
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
|
@override
|
||||||
String get openStreetMap => 'OpenStreetMap';
|
String get openStreetMap => 'OpenStreetMap';
|
||||||
|
|
||||||
@@ -1030,12 +678,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get googleTerrain => 'Google Terrain';
|
String get googleTerrain => 'Google Terrain';
|
||||||
|
|
||||||
@override
|
|
||||||
String get downloadVisibleArea => 'Download visible area';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get initializingMap => 'Initializing map...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dragToPosition => 'Drag to Position';
|
String get dragToPosition => 'Drag to Position';
|
||||||
|
|
||||||
@@ -1051,50 +693,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarAlert => 'SAR ALERT';
|
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
|
@override
|
||||||
String get textCopiedToClipboard => 'Text copied to clipboard';
|
String get textCopiedToClipboard => 'Text copied to clipboard';
|
||||||
|
|
||||||
@@ -1105,9 +703,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get cannotReplyContactNotFound => 'Cannot reply: contact not found';
|
String get cannotReplyContactNotFound => 'Cannot reply: contact not found';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messageDeleted => 'Message deleted';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get copyText => 'Copy text';
|
String get copyText => 'Copy text';
|
||||||
|
|
||||||
@@ -1143,12 +738,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarLocationShare => 'SAR Location';
|
String get sarLocationShare => 'SAR Location';
|
||||||
|
|
||||||
@override
|
|
||||||
String get locationShared => 'Location shared';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get refreshedContacts => 'Refreshed contacts';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get justNow => 'Just now';
|
String get justNow => 'Just now';
|
||||||
|
|
||||||
@@ -1256,79 +845,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get you => 'You';
|
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
|
@override
|
||||||
String exportFailed(String error) {
|
String exportFailed(String error) {
|
||||||
return 'Export failed: $error';
|
return 'Export failed: $error';
|
||||||
@@ -1339,40 +855,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
return 'Import failed: $error';
|
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
|
@override
|
||||||
String get unknown => 'Unknown';
|
String get unknown => 'Unknown';
|
||||||
|
|
||||||
@override
|
|
||||||
String get bounds => 'Bounds';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onlineLayers => 'Online Layers';
|
String get onlineLayers => 'Online Layers';
|
||||||
|
|
||||||
@override
|
|
||||||
String get offlineLayers => 'Offline Layers';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationTrail => 'Location Trail';
|
String get locationTrail => 'Location Trail';
|
||||||
|
|
||||||
@@ -1411,47 +899,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => 'Trail Controls';
|
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
|
@override
|
||||||
String get contactTrails => 'Contact Trails';
|
String get contactTrails => 'Contact Trails';
|
||||||
|
|
||||||
@@ -1721,19 +1168,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get noRoomsFound => 'No rooms found';
|
String get noRoomsFound => 'No rooms found';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noContactsOrRoomsAvailable => 'No contacts or rooms available';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noRecipientsAvailable => 'No recipients available';
|
String get noRecipientsAvailable => 'No recipients available';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noChannelsFound => 'No channels found';
|
String get noChannelsFound => 'No channels found';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messagesWillBeSentToPublicChannel =>
|
|
||||||
'Messages will be sent to public channel';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get newMessage => 'New message';
|
String get newMessage => 'New message';
|
||||||
|
|
||||||
@@ -1908,9 +1348,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get rectangleDrawing => 'Rectangle Drawing';
|
String get rectangleDrawing => 'Rectangle Drawing';
|
||||||
|
|
||||||
@override
|
|
||||||
String get coordinatesCopiedToClipboard => 'Coordinates copied to clipboard';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get manualCoordinates => 'Manual Coordinates';
|
String get manualCoordinates => 'Manual Coordinates';
|
||||||
|
|
||||||
@@ -1923,37 +1360,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get longitudeLabel => 'Longitude';
|
String get longitudeLabel => 'Longitude';
|
||||||
|
|
||||||
@override
|
|
||||||
String get invalidLatitude => 'Invalid latitude (-90 to 90)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get invalidLongitude => 'Invalid longitude (-180 to 180)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get exampleCoordinates => 'Example: 46.0569, 14.5058';
|
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
|
@override
|
||||||
String get shareDrawing => 'Share Drawing';
|
String get shareDrawing => 'Share Drawing';
|
||||||
|
|
||||||
@@ -2008,12 +1417,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get forestRoads => 'Forest Roads';
|
String get forestRoads => 'Forest Roads';
|
||||||
|
|
||||||
@override
|
|
||||||
String get showCadastralParcels => 'Show Cadastral Parcels';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get showForestRoads => 'Show Forest Roads';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get wmsOverlays => 'WMS Overlays';
|
String get wmsOverlays => 'WMS Overlays';
|
||||||
|
|
||||||
@@ -2117,10 +1520,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
return 'Failed to delete channel: $error';
|
return 'Failed to delete channel: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String get allChannelSlotsInUse =>
|
|
||||||
'All channel slots are in use (maximum 39 custom channels)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get createChannel => 'Create Channel';
|
String get createChannel => 'Create Channel';
|
||||||
|
|
||||||
@@ -2160,21 +1559,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String get wizardConnectingFeature3 =>
|
String get wizardConnectingFeature3 =>
|
||||||
'Works completely offline - no internet required';
|
'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
|
@override
|
||||||
String get wizardChannelTitle => 'Channels';
|
String get wizardChannelTitle => 'Channels';
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disconnect => 'Desconectar';
|
String get disconnect => 'Desconectar';
|
||||||
|
|
||||||
@override
|
|
||||||
String get scanningForDevices => 'Buscando dispositivos...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDevicesFound => 'No se encontraron dispositivos';
|
String get noDevicesFound => 'No se encontraron dispositivos';
|
||||||
|
|
||||||
@@ -62,27 +59,14 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get failedToGetGpsLocation => 'Error al obtener la ubicación GPS';
|
String get failedToGetGpsLocation => 'Error al obtener la ubicación GPS';
|
||||||
|
|
||||||
@override
|
|
||||||
String advertisedAtLocation(String latitude, String longitude) {
|
|
||||||
return 'Anunciado en $latitude, $longitude';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String failedToAdvertise(String error) {
|
String failedToAdvertise(String error) {
|
||||||
return 'Error al anunciar: $error';
|
return 'Error al anunciar: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String reconnecting(int attempt, int max) {
|
|
||||||
return 'Reconectando... ($attempt/$max)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get cancelReconnection => 'Cancelar reconexión';
|
String get cancelReconnection => 'Cancelar reconexión';
|
||||||
|
|
||||||
@override
|
|
||||||
String get mapManagement => 'Gestión de mapas';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get general => 'General';
|
String get general => 'General';
|
||||||
|
|
||||||
@@ -129,13 +113,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String get displayPacketActivity =>
|
String get displayPacketActivity =>
|
||||||
'Mostrar indicadores de actividad de paquetes en la barra superior';
|
'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
|
@override
|
||||||
String get disableMap => 'Desactivar mapa';
|
String get disableMap => 'Desactivar mapa';
|
||||||
|
|
||||||
@@ -149,75 +126,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chooseLanguage => 'Elegir idioma';
|
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
|
@override
|
||||||
String get save => 'Guardar';
|
String get save => 'Guardar';
|
||||||
|
|
||||||
@@ -253,12 +161,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get moreInfo => 'Más información';
|
String get moreInfo => 'Más información';
|
||||||
|
|
||||||
@override
|
|
||||||
String get learnMoreAbout => 'Más información sobre MeshCore SAR';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get developer => 'Desarrollador';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get packageName => 'Nombre del paquete';
|
String get packageName => 'Nombre del paquete';
|
||||||
|
|
||||||
@@ -334,9 +236,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get refresh => 'Actualizar';
|
String get refresh => 'Actualizar';
|
||||||
|
|
||||||
@override
|
|
||||||
String get sendDirectMessage => 'Enviar';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get resetPath => 'Restablecer ruta (Re-enrutar)';
|
String get resetPath => 'Restablecer ruta (Re-enrutar)';
|
||||||
|
|
||||||
@@ -459,21 +358,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
return 'Compartir $count dibujo$plural';
|
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
|
@override
|
||||||
String get showReceivedDrawings => 'Mostrar dibujos recibidos';
|
String get showReceivedDrawings => 'Mostrar dibujos recibidos';
|
||||||
|
|
||||||
@@ -495,9 +379,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get clearAll => 'Borrar todo';
|
String get clearAll => 'Borrar todo';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noLocalDrawings => 'No hay dibujos locales para compartir';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get publicChannel => 'Canal público';
|
String get publicChannel => 'Canal público';
|
||||||
|
|
||||||
@@ -507,37 +388,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get storedPermanently => 'Almacenado permanentemente en la sala';
|
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
|
@override
|
||||||
String get notConnectedToDevice => 'No conectado al dispositivo';
|
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
|
@override
|
||||||
String get typeYourMessage => 'Escribe tu mensaje...';
|
String get typeYourMessage => 'Escribe tu mensaje...';
|
||||||
|
|
||||||
@@ -567,9 +420,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get location => 'Ubicación';
|
String get location => 'Ubicación';
|
||||||
|
|
||||||
@override
|
|
||||||
String get myLocation => 'Mi ubicación';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fromMap => 'Desde el mapa';
|
String get fromMap => 'Desde el mapa';
|
||||||
|
|
||||||
@@ -631,9 +481,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get flood => 'Inundación';
|
String get flood => 'Inundación';
|
||||||
|
|
||||||
@override
|
|
||||||
String get admin => 'Admin';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get loggedIn => 'Sesión iniciada';
|
String get loggedIn => 'Sesión iniciada';
|
||||||
|
|
||||||
@@ -643,29 +490,11 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get distance => 'Distancia';
|
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
|
@override
|
||||||
String directPingTimeout(String name) {
|
String directPingTimeout(String name) {
|
||||||
return 'Tiempo de espera de ping directo agotado - reintentando $name con inundación...';
|
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
|
@override
|
||||||
String pingFailed(String name) {
|
String pingFailed(String name) {
|
||||||
return 'Ping fallido a $name - no se recibió respuesta';
|
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.';
|
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
|
@override
|
||||||
String failedToRemoveContact(String error) {
|
String failedToRemoveContact(String error) {
|
||||||
return 'Error al eliminar contacto: $error';
|
return 'Error al eliminar contacto: $error';
|
||||||
@@ -730,11 +549,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get telemetry => 'Telemetría';
|
String get telemetry => 'Telemetría';
|
||||||
|
|
||||||
@override
|
|
||||||
String requestingTelemetry(String name) {
|
|
||||||
return 'Solicitando telemetría de $name...';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get voltage => 'Voltaje';
|
String get voltage => 'Voltaje';
|
||||||
|
|
||||||
@@ -806,9 +620,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get showAll => 'Mostrar todo';
|
String get showAll => 'Mostrar todo';
|
||||||
|
|
||||||
@override
|
|
||||||
String get nearbyContacts => 'Contactos cercanos';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationUnavailable => 'Ubicación no disponible';
|
String get locationUnavailable => 'Ubicación no disponible';
|
||||||
|
|
||||||
@@ -849,176 +660,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels => 'Canales';
|
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
|
@override
|
||||||
String get selectMapLayer => 'Seleccionar capa de mapa';
|
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
|
@override
|
||||||
String get openStreetMap => 'OpenStreetMap';
|
String get openStreetMap => 'OpenStreetMap';
|
||||||
|
|
||||||
@@ -1037,12 +681,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get googleTerrain => 'Google Terreno';
|
String get googleTerrain => 'Google Terreno';
|
||||||
|
|
||||||
@override
|
|
||||||
String get downloadVisibleArea => 'Descargar área visible';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get initializingMap => 'Inicializando mapa...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dragToPosition => 'Arrastrar a posición';
|
String get dragToPosition => 'Arrastrar a posición';
|
||||||
|
|
||||||
@@ -1058,51 +696,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarAlert => 'ALERTA SAR';
|
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
|
@override
|
||||||
String get textCopiedToClipboard => 'Texto copiado al portapapeles';
|
String get textCopiedToClipboard => 'Texto copiado al portapapeles';
|
||||||
|
|
||||||
@@ -1114,9 +707,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String get cannotReplyContactNotFound =>
|
String get cannotReplyContactNotFound =>
|
||||||
'No se puede responder: contacto no encontrado';
|
'No se puede responder: contacto no encontrado';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messageDeleted => 'Mensaje eliminado';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get copyText => 'Copiar texto';
|
String get copyText => 'Copiar texto';
|
||||||
|
|
||||||
@@ -1152,12 +742,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarLocationShare => 'Ubicación SAR';
|
String get sarLocationShare => 'Ubicación SAR';
|
||||||
|
|
||||||
@override
|
|
||||||
String get locationShared => 'Ubicación compartida';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get refreshedContacts => 'Contactos actualizados';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get justNow => 'Justo ahora';
|
String get justNow => 'Justo ahora';
|
||||||
|
|
||||||
@@ -1265,125 +849,22 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get you => 'Tú';
|
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
|
@override
|
||||||
String exportFailed(String error) {
|
String exportFailed(String error) {
|
||||||
return 'Export failed: $error';
|
return 'Error al exportar: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String importFailed(String error) {
|
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
|
@override
|
||||||
String get unknown => 'Desconocido';
|
String get unknown => 'Desconocido';
|
||||||
|
|
||||||
@override
|
|
||||||
String get bounds => 'Límites';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onlineLayers => 'Capas en línea';
|
String get onlineLayers => 'Capas en línea';
|
||||||
|
|
||||||
@override
|
|
||||||
String get offlineLayers => 'Capas sin conexión';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationTrail => 'Rastro de ubicación';
|
String get locationTrail => 'Rastro de ubicación';
|
||||||
|
|
||||||
@@ -1422,47 +903,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => 'Controles del rastro';
|
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
|
@override
|
||||||
String get contactTrails => 'Rastros de contactos';
|
String get contactTrails => 'Rastros de contactos';
|
||||||
|
|
||||||
@@ -1583,7 +1023,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String get templateName => 'Nombre de plantilla';
|
String get templateName => 'Nombre de plantilla';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateNameHint => 'e.g. Found Person';
|
String get templateNameHint => 'p. ej., Persona encontrada';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateEmoji => 'Emoji';
|
String get templateEmoji => 'Emoji';
|
||||||
@@ -1595,16 +1035,16 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String get nameRequired => 'Se requiere nombre';
|
String get nameRequired => 'Se requiere nombre';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateDescription => 'Description (Optional)';
|
String get templateDescription => 'Descripcion (opcional)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateDescriptionHint => 'Add additional context...';
|
String get templateDescriptionHint => 'Anade contexto adicional...';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateColor => 'Color';
|
String get templateColor => 'Color';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get previewFormat => 'Preview (SAR Message Format)';
|
String get previewFormat => 'Vista previa (formato de mensaje SAR)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get importFromClipboard => 'Importar';
|
String get importFromClipboard => 'Importar';
|
||||||
@@ -1614,26 +1054,26 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String deleteTemplateConfirmation(String name) {
|
String deleteTemplateConfirmation(String name) {
|
||||||
return 'Delete template \'$name\'?';
|
return '¿Eliminar la plantilla \'$name\'?';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateAdded => 'Template added';
|
String get templateAdded => 'Plantilla anadida';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateUpdated => 'Template updated';
|
String get templateUpdated => 'Plantilla actualizada';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateDeleted => 'Template deleted';
|
String get templateDeleted => 'Plantilla eliminada';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String templatesImported(int count) {
|
String templatesImported(int count) {
|
||||||
String _temp0 = intl.Intl.pluralLogic(
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
count,
|
count,
|
||||||
locale: localeName,
|
locale: localeName,
|
||||||
other: 'Imported $count templates',
|
other: 'Se importaron $count plantillas',
|
||||||
one: 'Imported 1 template',
|
one: 'Se importo 1 plantilla',
|
||||||
zero: 'No templates imported',
|
zero: 'No se importaron plantillas',
|
||||||
);
|
);
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
@@ -1643,8 +1083,8 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String _temp0 = intl.Intl.pluralLogic(
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
count,
|
count,
|
||||||
locale: localeName,
|
locale: localeName,
|
||||||
other: 'Exported $count templates to clipboard',
|
other: 'Se exportaron $count plantillas al portapapeles',
|
||||||
one: 'Exported 1 template to clipboard',
|
one: 'Se exporto 1 plantilla al portapapeles',
|
||||||
);
|
);
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
@@ -1664,10 +1104,10 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
'Plantillas restablecidas a valores predeterminados';
|
'Plantillas restablecidas a valores predeterminados';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noTemplates => 'No templates available';
|
String get noTemplates => 'No hay plantillas disponibles';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get tapAddToCreate => 'Tap + to create your first template';
|
String get tapAddToCreate => 'Toca + para crear tu primera plantilla';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get ok => 'OK';
|
String get ok => 'OK';
|
||||||
@@ -1735,20 +1175,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get noRoomsFound => 'No se encontraron salas';
|
String get noRoomsFound => 'No se encontraron salas';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noContactsOrRoomsAvailable =>
|
|
||||||
'No hay contactos o salas disponibles';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noRecipientsAvailable => 'No hay destinatarios disponibles';
|
String get noRecipientsAvailable => 'No hay destinatarios disponibles';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noChannelsFound => 'No se encontraron canales';
|
String get noChannelsFound => 'No se encontraron canales';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messagesWillBeSentToPublicChannel =>
|
|
||||||
'Los mensajes se enviarán al canal público';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get newMessage => 'Nuevo mensaje';
|
String get newMessage => 'Nuevo mensaje';
|
||||||
|
|
||||||
@@ -1936,10 +1368,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get rectangleDrawing => 'Rectángulo';
|
String get rectangleDrawing => 'Rectángulo';
|
||||||
|
|
||||||
@override
|
|
||||||
String get coordinatesCopiedToClipboard =>
|
|
||||||
'Coordenadas copiadas al portapapeles';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get manualCoordinates => 'Coordenadas Manuales';
|
String get manualCoordinates => 'Coordenadas Manuales';
|
||||||
|
|
||||||
@@ -1952,31 +1380,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get longitudeLabel => 'Longitud';
|
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
|
@override
|
||||||
String get exampleCoordinates => 'Ejemplo: 46.0569, 14.5058';
|
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
|
@override
|
||||||
String get shareDrawing => 'Compartir Dibujo';
|
String get shareDrawing => 'Compartir Dibujo';
|
||||||
|
|
||||||
@@ -2033,12 +1439,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get forestRoads => 'Caminos Forestales';
|
String get forestRoads => 'Caminos Forestales';
|
||||||
|
|
||||||
@override
|
|
||||||
String get showCadastralParcels => 'Mostrar parcelas catastrales';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get showForestRoads => 'Mostrar caminos forestales';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get wmsOverlays => 'Superposiciones WMS';
|
String get wmsOverlays => 'Superposiciones WMS';
|
||||||
|
|
||||||
@@ -2143,10 +1543,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
return 'Error al eliminar el canal: $error';
|
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
|
@override
|
||||||
String get createChannel => 'Crear Canal';
|
String get createChannel => 'Crear Canal';
|
||||||
|
|
||||||
@@ -2187,21 +1583,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String get wizardConnectingFeature3 =>
|
String get wizardConnectingFeature3 =>
|
||||||
'Funciona completamente sin conexión - no se requiere internet';
|
'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
|
@override
|
||||||
String get wizardChannelTitle => 'Canales';
|
String get wizardChannelTitle => 'Canales';
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disconnect => 'Déconnecter';
|
String get disconnect => 'Déconnecter';
|
||||||
|
|
||||||
@override
|
|
||||||
String get scanningForDevices => 'Recherche d\'appareils...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDevicesFound => 'Aucun appareil trouvé';
|
String get noDevicesFound => 'Aucun appareil trouvé';
|
||||||
|
|
||||||
@@ -63,27 +60,14 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get failedToGetGpsLocation =>
|
String get failedToGetGpsLocation =>
|
||||||
'Échec de l\'obtention de la position GPS';
|
'Échec de l\'obtention de la position GPS';
|
||||||
|
|
||||||
@override
|
|
||||||
String advertisedAtLocation(String latitude, String longitude) {
|
|
||||||
return 'Annoncé à $latitude, $longitude';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String failedToAdvertise(String error) {
|
String failedToAdvertise(String error) {
|
||||||
return 'Échec de l\'annonce : $error';
|
return 'Échec de l\'annonce : $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String reconnecting(int attempt, int max) {
|
|
||||||
return 'Reconnexion... ($attempt/$max)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get cancelReconnection => 'Annuler la reconnexion';
|
String get cancelReconnection => 'Annuler la reconnexion';
|
||||||
|
|
||||||
@override
|
|
||||||
String get mapManagement => 'Gestion des cartes';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get general => 'Général';
|
String get general => 'Général';
|
||||||
|
|
||||||
@@ -130,13 +114,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get displayPacketActivity =>
|
String get displayPacketActivity =>
|
||||||
'Afficher les indicateurs d\'activité des paquets dans la barre supérieure';
|
'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
|
@override
|
||||||
String get disableMap => 'Désactiver la carte';
|
String get disableMap => 'Désactiver la carte';
|
||||||
|
|
||||||
@@ -150,75 +127,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chooseLanguage => 'Choisir la langue';
|
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
|
@override
|
||||||
String get save => 'Enregistrer';
|
String get save => 'Enregistrer';
|
||||||
|
|
||||||
@@ -254,12 +162,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get moreInfo => 'Plus d\'infos';
|
String get moreInfo => 'Plus d\'infos';
|
||||||
|
|
||||||
@override
|
|
||||||
String get learnMoreAbout => 'En savoir plus sur MeshCore SAR';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get developer => 'Développeur';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get packageName => 'Nom du package';
|
String get packageName => 'Nom du package';
|
||||||
|
|
||||||
@@ -336,9 +238,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get refresh => 'Actualiser';
|
String get refresh => 'Actualiser';
|
||||||
|
|
||||||
@override
|
|
||||||
String get sendDirectMessage => 'Envoyer';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get resetPath => 'Réinitialiser le chemin (Re-router)';
|
String get resetPath => 'Réinitialiser le chemin (Re-router)';
|
||||||
|
|
||||||
@@ -459,21 +358,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
return 'Partager $count dessin$plural';
|
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
|
@override
|
||||||
String get showReceivedDrawings => 'Afficher les dessins reçus';
|
String get showReceivedDrawings => 'Afficher les dessins reçus';
|
||||||
|
|
||||||
@@ -495,9 +379,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get clearAll => 'Tout effacer';
|
String get clearAll => 'Tout effacer';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noLocalDrawings => 'Aucun dessin local à partager';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get publicChannel => 'Canal public';
|
String get publicChannel => 'Canal public';
|
||||||
|
|
||||||
@@ -508,37 +389,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get storedPermanently => 'Stocké de manière permanente dans le salon';
|
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
|
@override
|
||||||
String get notConnectedToDevice => 'Non connecté à l\'appareil';
|
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
|
@override
|
||||||
String get typeYourMessage => 'Saisissez votre message...';
|
String get typeYourMessage => 'Saisissez votre message...';
|
||||||
|
|
||||||
@@ -568,9 +421,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get location => 'Position';
|
String get location => 'Position';
|
||||||
|
|
||||||
@override
|
|
||||||
String get myLocation => 'Ma position';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fromMap => 'Depuis la carte';
|
String get fromMap => 'Depuis la carte';
|
||||||
|
|
||||||
@@ -633,9 +483,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get flood => 'Inondation';
|
String get flood => 'Inondation';
|
||||||
|
|
||||||
@override
|
|
||||||
String get admin => 'Admin';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get loggedIn => 'Connecté';
|
String get loggedIn => 'Connecté';
|
||||||
|
|
||||||
@@ -645,29 +492,11 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get distance => 'Distance';
|
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
|
@override
|
||||||
String directPingTimeout(String name) {
|
String directPingTimeout(String name) {
|
||||||
return 'Délai d\'attente du ping direct - nouvelle tentative de $name avec inondation...';
|
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
|
@override
|
||||||
String pingFailed(String name) {
|
String pingFailed(String name) {
|
||||||
return 'Échec du ping vers $name - aucune réponse reçue';
|
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.';
|
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
|
@override
|
||||||
String failedToRemoveContact(String error) {
|
String failedToRemoveContact(String error) {
|
||||||
return 'Échec de la suppression du contact : $error';
|
return 'Échec de la suppression du contact : $error';
|
||||||
@@ -732,11 +551,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get telemetry => 'Télémétrie';
|
String get telemetry => 'Télémétrie';
|
||||||
|
|
||||||
@override
|
|
||||||
String requestingTelemetry(String name) {
|
|
||||||
return 'Demande de télémétrie à $name...';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get voltage => 'Tension';
|
String get voltage => 'Tension';
|
||||||
|
|
||||||
@@ -808,9 +622,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get showAll => 'Tout afficher';
|
String get showAll => 'Tout afficher';
|
||||||
|
|
||||||
@override
|
|
||||||
String get nearbyContacts => 'Contacts à proximité';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationUnavailable => 'Position non disponible';
|
String get locationUnavailable => 'Position non disponible';
|
||||||
|
|
||||||
@@ -851,177 +662,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels => 'Canaux';
|
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
|
@override
|
||||||
String get selectMapLayer => 'Sélectionner la couche de carte';
|
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
|
@override
|
||||||
String get openStreetMap => 'OpenStreetMap';
|
String get openStreetMap => 'OpenStreetMap';
|
||||||
|
|
||||||
@@ -1040,12 +683,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get googleTerrain => 'Google Terrain';
|
String get googleTerrain => 'Google Terrain';
|
||||||
|
|
||||||
@override
|
|
||||||
String get downloadVisibleArea => 'Télécharger la zone visible';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get initializingMap => 'Initialisation de la carte...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dragToPosition => 'Faire glisser vers la position';
|
String get dragToPosition => 'Faire glisser vers la position';
|
||||||
|
|
||||||
@@ -1061,51 +698,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarAlert => 'ALERTE SAR';
|
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
|
@override
|
||||||
String get textCopiedToClipboard => 'Texte copié dans le presse-papiers';
|
String get textCopiedToClipboard => 'Texte copié dans le presse-papiers';
|
||||||
|
|
||||||
@@ -1117,9 +709,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get cannotReplyContactNotFound =>
|
String get cannotReplyContactNotFound =>
|
||||||
'Impossible de répondre : contact non trouvé';
|
'Impossible de répondre : contact non trouvé';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messageDeleted => 'Message supprimé';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get copyText => 'Copier le texte';
|
String get copyText => 'Copier le texte';
|
||||||
|
|
||||||
@@ -1155,12 +744,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarLocationShare => 'Position SAR';
|
String get sarLocationShare => 'Position SAR';
|
||||||
|
|
||||||
@override
|
|
||||||
String get locationShared => 'Position partagée';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get refreshedContacts => 'Contacts actualisés';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get justNow => 'À l\'instant';
|
String get justNow => 'À l\'instant';
|
||||||
|
|
||||||
@@ -1268,127 +851,22 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get you => 'Vous';
|
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
|
@override
|
||||||
String exportFailed(String error) {
|
String exportFailed(String error) {
|
||||||
return 'Export failed: $error';
|
return 'Echec de l\'exportation : $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String importFailed(String error) {
|
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
|
@override
|
||||||
String get unknown => 'Inconnu';
|
String get unknown => 'Inconnu';
|
||||||
|
|
||||||
@override
|
|
||||||
String get bounds => 'Limites';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onlineLayers => 'Couches en ligne';
|
String get onlineLayers => 'Couches en ligne';
|
||||||
|
|
||||||
@override
|
|
||||||
String get offlineLayers => 'Couches hors ligne';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationTrail => 'Trace de déplacement';
|
String get locationTrail => 'Trace de déplacement';
|
||||||
|
|
||||||
@@ -1428,47 +906,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => 'Contrôles de la trace';
|
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
|
@override
|
||||||
String get contactTrails => 'Traces des contacts';
|
String get contactTrails => 'Traces des contacts';
|
||||||
|
|
||||||
@@ -1589,7 +1026,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get templateName => 'Nom du modèle';
|
String get templateName => 'Nom du modèle';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateNameHint => 'e.g. Found Person';
|
String get templateNameHint => 'p. ex. Personne trouvee';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateEmoji => 'Emoji';
|
String get templateEmoji => 'Emoji';
|
||||||
@@ -1601,16 +1038,16 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get nameRequired => 'Nom est requis';
|
String get nameRequired => 'Nom est requis';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateDescription => 'Description (Optional)';
|
String get templateDescription => 'Description (facultative)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateDescriptionHint => 'Add additional context...';
|
String get templateDescriptionHint => 'Ajoutez un contexte supplementaire...';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateColor => 'Color';
|
String get templateColor => 'Couleur';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get previewFormat => 'Preview (SAR Message Format)';
|
String get previewFormat => 'Apercu (format de message SAR)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get importFromClipboard => 'Importer';
|
String get importFromClipboard => 'Importer';
|
||||||
@@ -1620,26 +1057,26 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String deleteTemplateConfirmation(String name) {
|
String deleteTemplateConfirmation(String name) {
|
||||||
return 'Delete template \'$name\'?';
|
return 'Supprimer le modele \'$name\' ?';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateAdded => 'Template added';
|
String get templateAdded => 'Modele ajoute';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateUpdated => 'Template updated';
|
String get templateUpdated => 'Modele mis a jour';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateDeleted => 'Template deleted';
|
String get templateDeleted => 'Modele supprime';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String templatesImported(int count) {
|
String templatesImported(int count) {
|
||||||
String _temp0 = intl.Intl.pluralLogic(
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
count,
|
count,
|
||||||
locale: localeName,
|
locale: localeName,
|
||||||
other: 'Imported $count templates',
|
other: '$count modeles importes',
|
||||||
one: 'Imported 1 template',
|
one: '1 modele importe',
|
||||||
zero: 'No templates imported',
|
zero: 'Aucun modele importe',
|
||||||
);
|
);
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
@@ -1649,8 +1086,8 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String _temp0 = intl.Intl.pluralLogic(
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
count,
|
count,
|
||||||
locale: localeName,
|
locale: localeName,
|
||||||
other: 'Exported $count templates to clipboard',
|
other: '$count modeles exportes vers le presse-papiers',
|
||||||
one: 'Exported 1 template to clipboard',
|
one: '1 modele exporte vers le presse-papiers',
|
||||||
);
|
);
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
@@ -1669,10 +1106,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get resetComplete => 'Modèles réinitialisés aux valeurs par défaut';
|
String get resetComplete => 'Modèles réinitialisés aux valeurs par défaut';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noTemplates => 'No templates available';
|
String get noTemplates => 'Aucun modele disponible';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get tapAddToCreate => 'Tap + to create your first template';
|
String get tapAddToCreate => 'Appuyez sur + pour creer votre premier modele';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get ok => 'OK';
|
String get ok => 'OK';
|
||||||
@@ -1741,19 +1178,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get noRoomsFound => 'Aucune salle trouvée';
|
String get noRoomsFound => 'Aucune salle trouvée';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noContactsOrRoomsAvailable => 'Aucun contact ou salle disponible';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noRecipientsAvailable => 'Aucun destinataire disponible';
|
String get noRecipientsAvailable => 'Aucun destinataire disponible';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noChannelsFound => 'Aucun canal trouvé';
|
String get noChannelsFound => 'Aucun canal trouvé';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messagesWillBeSentToPublicChannel =>
|
|
||||||
'Les messages seront envoyés au canal public';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get newMessage => 'Nouveau message';
|
String get newMessage => 'Nouveau message';
|
||||||
|
|
||||||
@@ -1940,10 +1370,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get rectangleDrawing => 'Rectangle';
|
String get rectangleDrawing => 'Rectangle';
|
||||||
|
|
||||||
@override
|
|
||||||
String get coordinatesCopiedToClipboard =>
|
|
||||||
'Coordonnées copiées dans le presse-papiers';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get manualCoordinates => 'Coordonnées Manuelles';
|
String get manualCoordinates => 'Coordonnées Manuelles';
|
||||||
|
|
||||||
@@ -1956,31 +1382,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get longitudeLabel => 'Longitude';
|
String get longitudeLabel => 'Longitude';
|
||||||
|
|
||||||
@override
|
|
||||||
String get invalidLatitude => 'Latitude invalide (-90 à 90)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get invalidLongitude => 'Longitude invalide (-180 à 180)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get exampleCoordinates => 'Exemple: 46.0569, 14.5058';
|
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
|
@override
|
||||||
String get shareDrawing => 'Partager le Dessin';
|
String get shareDrawing => 'Partager le Dessin';
|
||||||
|
|
||||||
@@ -2037,12 +1441,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get forestRoads => 'Chemins Forestiers';
|
String get forestRoads => 'Chemins Forestiers';
|
||||||
|
|
||||||
@override
|
|
||||||
String get showCadastralParcels => 'Afficher les parcelles cadastrales';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get showForestRoads => 'Afficher les chemins forestiers';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get wmsOverlays => 'Superpositions WMS';
|
String get wmsOverlays => 'Superpositions WMS';
|
||||||
|
|
||||||
@@ -2148,10 +1546,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
return 'Échec de la suppression du canal : $error';
|
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
|
@override
|
||||||
String get createChannel => 'Créer un Canal';
|
String get createChannel => 'Créer un Canal';
|
||||||
|
|
||||||
@@ -2193,21 +1587,6 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String get wizardConnectingFeature3 =>
|
String get wizardConnectingFeature3 =>
|
||||||
'Fonctionne entièrement hors ligne - aucun internet requis';
|
'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
|
@override
|
||||||
String get wizardChannelTitle => 'Canaux';
|
String get wizardChannelTitle => 'Canaux';
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disconnect => 'Prekini';
|
String get disconnect => 'Prekini';
|
||||||
|
|
||||||
@override
|
|
||||||
String get scanningForDevices => 'Skeniranje uređaja...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDevicesFound => 'Nisu pronađeni uređaji';
|
String get noDevicesFound => 'Nisu pronađeni uređaji';
|
||||||
|
|
||||||
@@ -62,27 +59,14 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get failedToGetGpsLocation => 'Neuspjelo dobivanje GPS lokacije';
|
String get failedToGetGpsLocation => 'Neuspjelo dobivanje GPS lokacije';
|
||||||
|
|
||||||
@override
|
|
||||||
String advertisedAtLocation(String latitude, String longitude) {
|
|
||||||
return 'Objavljeno na $latitude, $longitude';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String failedToAdvertise(String error) {
|
String failedToAdvertise(String error) {
|
||||||
return 'Neuspjela objava: $error';
|
return 'Neuspjela objava: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String reconnecting(int attempt, int max) {
|
|
||||||
return 'Ponovno povezivanje... ($attempt/$max)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get cancelReconnection => 'Otkaži ponovno povezivanje';
|
String get cancelReconnection => 'Otkaži ponovno povezivanje';
|
||||||
|
|
||||||
@override
|
|
||||||
String get mapManagement => 'Upravljanje kartom';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get general => 'Općenito';
|
String get general => 'Općenito';
|
||||||
|
|
||||||
@@ -129,13 +113,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
String get displayPacketActivity =>
|
String get displayPacketActivity =>
|
||||||
'Prikaži indikatore aktivnosti paketa u gornjoj traci';
|
'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
|
@override
|
||||||
String get disableMap => 'Onemogući kartu';
|
String get disableMap => 'Onemogući kartu';
|
||||||
|
|
||||||
@@ -148,74 +125,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chooseLanguage => 'Odaberite jezik';
|
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
|
@override
|
||||||
String get save => 'Spremi';
|
String get save => 'Spremi';
|
||||||
|
|
||||||
@@ -251,12 +160,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get moreInfo => 'Više informacija';
|
String get moreInfo => 'Više informacija';
|
||||||
|
|
||||||
@override
|
|
||||||
String get learnMoreAbout => 'Saznajte više o MeshCore SAR-u';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get developer => 'Programer';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get packageName => 'Ime paketa';
|
String get packageName => 'Ime paketa';
|
||||||
|
|
||||||
@@ -332,9 +235,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get refresh => 'Osvježi';
|
String get refresh => 'Osvježi';
|
||||||
|
|
||||||
@override
|
|
||||||
String get sendDirectMessage => 'Pošalji';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get resetPath => 'Resetiraj put (preusmjeri)';
|
String get resetPath => 'Resetiraj put (preusmjeri)';
|
||||||
|
|
||||||
@@ -453,21 +353,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
return 'Podijeli $count crtež$plural';
|
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
|
@override
|
||||||
String get showReceivedDrawings => 'Prikaži primljene crteže';
|
String get showReceivedDrawings => 'Prikaži primljene crteže';
|
||||||
|
|
||||||
@@ -489,9 +374,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get clearAll => 'Očisti sve';
|
String get clearAll => 'Očisti sve';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noLocalDrawings => 'Nema lokalnih crteža za dijeljenje';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get publicChannel => 'Javni kanal';
|
String get publicChannel => 'Javni kanal';
|
||||||
|
|
||||||
@@ -501,37 +383,9 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get storedPermanently => 'Trajno pohranjeno u sobi';
|
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
|
@override
|
||||||
String get notConnectedToDevice => 'Nije povezano s uređajem';
|
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
|
@override
|
||||||
String get typeYourMessage => 'Upišite svoju poruku...';
|
String get typeYourMessage => 'Upišite svoju poruku...';
|
||||||
|
|
||||||
@@ -561,9 +415,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get location => 'Lokacija';
|
String get location => 'Lokacija';
|
||||||
|
|
||||||
@override
|
|
||||||
String get myLocation => 'Moja lokacija';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fromMap => 'S karte';
|
String get fromMap => 'S karte';
|
||||||
|
|
||||||
@@ -625,9 +476,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get flood => 'Preplavljanje';
|
String get flood => 'Preplavljanje';
|
||||||
|
|
||||||
@override
|
|
||||||
String get admin => 'Administrator';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get loggedIn => 'Prijavljen';
|
String get loggedIn => 'Prijavljen';
|
||||||
|
|
||||||
@@ -637,29 +485,11 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get distance => 'Udaljenost';
|
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
|
@override
|
||||||
String directPingTimeout(String name) {
|
String directPingTimeout(String name) {
|
||||||
return 'Istek izravnog pinga - ponovni pokušaj $name s preplavljanjem...';
|
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
|
@override
|
||||||
String pingFailed(String name) {
|
String pingFailed(String name) {
|
||||||
return 'Ping neuspješan prema $name - nije primljen odgovor';
|
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.';
|
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
|
@override
|
||||||
String failedToRemoveContact(String error) {
|
String failedToRemoveContact(String error) {
|
||||||
return 'Neuspjelo uklanjanje kontakta: $error';
|
return 'Neuspjelo uklanjanje kontakta: $error';
|
||||||
@@ -724,11 +544,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get telemetry => 'Telemetrija';
|
String get telemetry => 'Telemetrija';
|
||||||
|
|
||||||
@override
|
|
||||||
String requestingTelemetry(String name) {
|
|
||||||
return 'Zahtijevanje telemetrije od $name...';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get voltage => 'Napon';
|
String get voltage => 'Napon';
|
||||||
|
|
||||||
@@ -800,9 +615,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get showAll => 'Prikaži sve';
|
String get showAll => 'Prikaži sve';
|
||||||
|
|
||||||
@override
|
|
||||||
String get nearbyContacts => 'Obližnji kontakti';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationUnavailable => 'Lokacija nije dostupna';
|
String get locationUnavailable => 'Lokacija nije dostupna';
|
||||||
|
|
||||||
@@ -843,174 +655,9 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels => 'Kanali';
|
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
|
@override
|
||||||
String get selectMapLayer => 'Odaberite sloj karte';
|
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
|
@override
|
||||||
String get openStreetMap => 'OpenStreetMap';
|
String get openStreetMap => 'OpenStreetMap';
|
||||||
|
|
||||||
@@ -1029,12 +676,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get googleTerrain => 'Google teren';
|
String get googleTerrain => 'Google teren';
|
||||||
|
|
||||||
@override
|
|
||||||
String get downloadVisibleArea => 'Preuzmi vidljivo područje';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get initializingMap => 'Inicijalizacija karte...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dragToPosition => 'Povuci na poziciju';
|
String get dragToPosition => 'Povuci na poziciju';
|
||||||
|
|
||||||
@@ -1050,51 +691,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarAlert => 'SAR UZBUNA';
|
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
|
@override
|
||||||
String get textCopiedToClipboard => 'Tekst kopiran u međuspremnik';
|
String get textCopiedToClipboard => 'Tekst kopiran u međuspremnik';
|
||||||
|
|
||||||
@@ -1106,9 +702,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
String get cannotReplyContactNotFound =>
|
String get cannotReplyContactNotFound =>
|
||||||
'Ne mogu odgovoriti: kontakt nije pronađen';
|
'Ne mogu odgovoriti: kontakt nije pronađen';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messageDeleted => 'Poruka izbrisana';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get copyText => 'Kopiraj tekst';
|
String get copyText => 'Kopiraj tekst';
|
||||||
|
|
||||||
@@ -1144,12 +737,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarLocationShare => 'SAR Lokacija';
|
String get sarLocationShare => 'SAR Lokacija';
|
||||||
|
|
||||||
@override
|
|
||||||
String get locationShared => 'Lokacija podijeljena';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get refreshedContacts => 'Kontakti osvježeni';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get justNow => 'Upravo sada';
|
String get justNow => 'Upravo sada';
|
||||||
|
|
||||||
@@ -1257,78 +844,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get you => 'Ti';
|
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
|
@override
|
||||||
String exportFailed(String error) {
|
String exportFailed(String error) {
|
||||||
return 'Izvoz nije uspio: $error';
|
return 'Izvoz nije uspio: $error';
|
||||||
@@ -1339,40 +854,12 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
return 'Uvoz nije uspio: $error';
|
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
|
@override
|
||||||
String get unknown => 'Nepoznato';
|
String get unknown => 'Nepoznato';
|
||||||
|
|
||||||
@override
|
|
||||||
String get bounds => 'Granice';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onlineLayers => 'Mrežni slojevi';
|
String get onlineLayers => 'Mrežni slojevi';
|
||||||
|
|
||||||
@override
|
|
||||||
String get offlineLayers => 'Offline slojevi';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationTrail => 'Putanja lokacije';
|
String get locationTrail => 'Putanja lokacije';
|
||||||
|
|
||||||
@@ -1411,47 +898,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => 'Upravljanje putanjom';
|
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
|
@override
|
||||||
String get contactTrails => 'Putanje kontakata';
|
String get contactTrails => 'Putanje kontakata';
|
||||||
|
|
||||||
@@ -1492,7 +938,7 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
String get buildDate => 'Datum izgradnje';
|
String get buildDate => 'Datum izgradnje';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get firmware => 'Firmware';
|
String get firmware => 'Firmver';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get maxContacts => 'Maks. kontakata';
|
String get maxContacts => 'Maks. kontakata';
|
||||||
@@ -1658,7 +1104,7 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
String get tapAddToCreate => 'Dodirnite + za izradu prvog predloška';
|
String get tapAddToCreate => 'Dodirnite + za izradu prvog predloška';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get ok => 'OK';
|
String get ok => 'U redu';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get permissionsSection => 'Dozvole';
|
String get permissionsSection => 'Dozvole';
|
||||||
@@ -1723,19 +1169,12 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get noRoomsFound => 'Nema soba';
|
String get noRoomsFound => 'Nema soba';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noContactsOrRoomsAvailable => 'Nema dostupnih kontakata ili soba';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noRecipientsAvailable => 'Nema dostupnih primatelja';
|
String get noRecipientsAvailable => 'Nema dostupnih primatelja';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noChannelsFound => 'Nije pronađen nijedan kanal';
|
String get noChannelsFound => 'Nije pronađen nijedan kanal';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messagesWillBeSentToPublicChannel =>
|
|
||||||
'Poruke će biti poslane na javni kanal';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get newMessage => 'Nova poruka';
|
String get newMessage => 'Nova poruka';
|
||||||
|
|
||||||
@@ -1915,10 +1354,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get rectangleDrawing => 'Pravokutni crtež';
|
String get rectangleDrawing => 'Pravokutni crtež';
|
||||||
|
|
||||||
@override
|
|
||||||
String get coordinatesCopiedToClipboard =>
|
|
||||||
'Koordinate kopirane u međuspremnik';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get manualCoordinates => 'Ručne koordinate';
|
String get manualCoordinates => 'Ručne koordinate';
|
||||||
|
|
||||||
@@ -1931,37 +1366,9 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get longitudeLabel => 'Geografska dužina';
|
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
|
@override
|
||||||
String get exampleCoordinates => 'Primjer: 46.0569, 14.5058';
|
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
|
@override
|
||||||
String get shareDrawing => 'Podijeli crtež';
|
String get shareDrawing => 'Podijeli crtež';
|
||||||
|
|
||||||
@@ -2017,12 +1424,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get forestRoads => 'Šumske ceste';
|
String get forestRoads => 'Šumske ceste';
|
||||||
|
|
||||||
@override
|
|
||||||
String get showCadastralParcels => 'Prikaži katastarske čestice';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get showForestRoads => 'Prikaži šumske ceste';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get wmsOverlays => 'WMS prekrivanja';
|
String get wmsOverlays => 'WMS prekrivanja';
|
||||||
|
|
||||||
@@ -2126,10 +1527,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
return 'Neuspješno brisanje kanala: $error';
|
return 'Neuspješno brisanje kanala: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String get allChannelSlotsInUse =>
|
|
||||||
'Svi slotovi kanala su zauzeti (maksimalno 39 prilagođenih kanala)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get createChannel => 'Kreiraj kanal';
|
String get createChannel => 'Kreiraj kanal';
|
||||||
|
|
||||||
@@ -2170,21 +1567,6 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
String get wizardConnectingFeature3 =>
|
String get wizardConnectingFeature3 =>
|
||||||
'Radi potpuno izvan mreže — internet nije potreban';
|
'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
|
@override
|
||||||
String get wizardChannelTitle => 'Kanali';
|
String get wizardChannelTitle => 'Kanali';
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disconnect => 'Disconnetti';
|
String get disconnect => 'Disconnetti';
|
||||||
|
|
||||||
@override
|
|
||||||
String get scanningForDevices => 'Ricerca dispositivi in corso...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDevicesFound => 'Nessun dispositivo trovato';
|
String get noDevicesFound => 'Nessun dispositivo trovato';
|
||||||
|
|
||||||
@@ -62,27 +59,14 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get failedToGetGpsLocation => 'Impossibile ottenere la posizione GPS';
|
String get failedToGetGpsLocation => 'Impossibile ottenere la posizione GPS';
|
||||||
|
|
||||||
@override
|
|
||||||
String advertisedAtLocation(String latitude, String longitude) {
|
|
||||||
return 'Annunciato a $latitude, $longitude';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String failedToAdvertise(String error) {
|
String failedToAdvertise(String error) {
|
||||||
return 'Annuncio fallito: $error';
|
return 'Annuncio fallito: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String reconnecting(int attempt, int max) {
|
|
||||||
return 'Riconnessione in corso... ($attempt/$max)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get cancelReconnection => 'Annulla riconnessione';
|
String get cancelReconnection => 'Annulla riconnessione';
|
||||||
|
|
||||||
@override
|
|
||||||
String get mapManagement => 'Gestione Mappa';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get general => 'Generale';
|
String get general => 'Generale';
|
||||||
|
|
||||||
@@ -129,13 +113,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String get displayPacketActivity =>
|
String get displayPacketActivity =>
|
||||||
'Mostra indicatori di attività pacchetti nella barra superiore';
|
'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
|
@override
|
||||||
String get disableMap => 'Disabilita mappa';
|
String get disableMap => 'Disabilita mappa';
|
||||||
|
|
||||||
@@ -149,75 +126,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chooseLanguage => 'Scegli Lingua';
|
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
|
@override
|
||||||
String get save => 'Salva';
|
String get save => 'Salva';
|
||||||
|
|
||||||
@@ -253,12 +161,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get moreInfo => 'Maggiori informazioni';
|
String get moreInfo => 'Maggiori informazioni';
|
||||||
|
|
||||||
@override
|
|
||||||
String get learnMoreAbout => 'Ulteriori informazioni su MeshCore SAR';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get developer => 'Sviluppatore';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get packageName => 'Nome Pacchetto';
|
String get packageName => 'Nome Pacchetto';
|
||||||
|
|
||||||
@@ -335,9 +237,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get refresh => 'Aggiorna';
|
String get refresh => 'Aggiorna';
|
||||||
|
|
||||||
@override
|
|
||||||
String get sendDirectMessage => 'Invia';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get resetPath => 'Resetta Percorso (Ri-instrada)';
|
String get resetPath => 'Resetta Percorso (Ri-instrada)';
|
||||||
|
|
||||||
@@ -458,21 +357,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
return 'Condividi $count disegno$plural';
|
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
|
@override
|
||||||
String get showReceivedDrawings => 'Mostra Disegni Ricevuti';
|
String get showReceivedDrawings => 'Mostra Disegni Ricevuti';
|
||||||
|
|
||||||
@@ -494,9 +378,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get clearAll => 'Cancella Tutto';
|
String get clearAll => 'Cancella Tutto';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noLocalDrawings => 'Nessun disegno locale da condividere';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get publicChannel => 'Canale Pubblico';
|
String get publicChannel => 'Canale Pubblico';
|
||||||
|
|
||||||
@@ -506,37 +387,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get storedPermanently => 'Archiviato permanentemente nella stanza';
|
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
|
@override
|
||||||
String get notConnectedToDevice => 'Non connesso al dispositivo';
|
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
|
@override
|
||||||
String get typeYourMessage => 'Scrivi il tuo messaggio...';
|
String get typeYourMessage => 'Scrivi il tuo messaggio...';
|
||||||
|
|
||||||
@@ -566,9 +419,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get location => 'Posizione';
|
String get location => 'Posizione';
|
||||||
|
|
||||||
@override
|
|
||||||
String get myLocation => 'La mia posizione';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fromMap => 'Dalla Mappa';
|
String get fromMap => 'Dalla Mappa';
|
||||||
|
|
||||||
@@ -630,9 +480,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get flood => 'Flood';
|
String get flood => 'Flood';
|
||||||
|
|
||||||
@override
|
|
||||||
String get admin => 'Admin';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get loggedIn => 'Connesso';
|
String get loggedIn => 'Connesso';
|
||||||
|
|
||||||
@@ -642,29 +489,11 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get distance => 'Distanza';
|
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
|
@override
|
||||||
String directPingTimeout(String name) {
|
String directPingTimeout(String name) {
|
||||||
return 'Timeout ping diretto - nuovo tentativo $name con flooding...';
|
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
|
@override
|
||||||
String pingFailed(String name) {
|
String pingFailed(String name) {
|
||||||
return 'Ping fallito a $name - nessuna risposta ricevuta';
|
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.';
|
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
|
@override
|
||||||
String failedToRemoveContact(String error) {
|
String failedToRemoveContact(String error) {
|
||||||
return 'Impossibile rimuovere il contatto: $error';
|
return 'Impossibile rimuovere il contatto: $error';
|
||||||
@@ -729,11 +548,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get telemetry => 'Telemetria';
|
String get telemetry => 'Telemetria';
|
||||||
|
|
||||||
@override
|
|
||||||
String requestingTelemetry(String name) {
|
|
||||||
return 'Richiesta telemetria da $name...';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get voltage => 'Tensione';
|
String get voltage => 'Tensione';
|
||||||
|
|
||||||
@@ -805,9 +619,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get showAll => 'Mostra Tutto';
|
String get showAll => 'Mostra Tutto';
|
||||||
|
|
||||||
@override
|
|
||||||
String get nearbyContacts => 'Contatti Vicini';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationUnavailable => 'Posizione non disponibile';
|
String get locationUnavailable => 'Posizione non disponibile';
|
||||||
|
|
||||||
@@ -848,176 +659,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels => 'Canali';
|
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
|
@override
|
||||||
String get selectMapLayer => 'Seleziona Livello Mappa';
|
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
|
@override
|
||||||
String get openStreetMap => 'OpenStreetMap';
|
String get openStreetMap => 'OpenStreetMap';
|
||||||
|
|
||||||
@@ -1036,12 +680,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get googleTerrain => 'Google Terreno';
|
String get googleTerrain => 'Google Terreno';
|
||||||
|
|
||||||
@override
|
|
||||||
String get downloadVisibleArea => 'Scarica area visibile';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get initializingMap => 'Inizializzazione mappa...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dragToPosition => 'Trascina per Posizionare';
|
String get dragToPosition => 'Trascina per Posizionare';
|
||||||
|
|
||||||
@@ -1057,52 +695,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarAlert => 'ALLERTA SAR';
|
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
|
@override
|
||||||
String get textCopiedToClipboard => 'Testo copiato negli appunti';
|
String get textCopiedToClipboard => 'Testo copiato negli appunti';
|
||||||
|
|
||||||
@@ -1114,9 +706,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String get cannotReplyContactNotFound =>
|
String get cannotReplyContactNotFound =>
|
||||||
'Impossibile rispondere: contatto non trovato';
|
'Impossibile rispondere: contatto non trovato';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messageDeleted => 'Messaggio eliminato';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get copyText => 'Copia testo';
|
String get copyText => 'Copia testo';
|
||||||
|
|
||||||
@@ -1152,12 +741,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarLocationShare => 'Posizione SAR';
|
String get sarLocationShare => 'Posizione SAR';
|
||||||
|
|
||||||
@override
|
|
||||||
String get locationShared => 'Posizione condivisa';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get refreshedContacts => 'Contatti aggiornati';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get justNow => 'Proprio ora';
|
String get justNow => 'Proprio ora';
|
||||||
|
|
||||||
@@ -1265,124 +848,22 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get you => 'Tu';
|
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
|
@override
|
||||||
String exportFailed(String error) {
|
String exportFailed(String error) {
|
||||||
return 'Export failed: $error';
|
return 'Esportazione non riuscita: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String importFailed(String error) {
|
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
|
@override
|
||||||
String get unknown => 'Sconosciuto';
|
String get unknown => 'Sconosciuto';
|
||||||
|
|
||||||
@override
|
|
||||||
String get bounds => 'Limiti';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onlineLayers => 'Livelli Online';
|
String get onlineLayers => 'Livelli Online';
|
||||||
|
|
||||||
@override
|
|
||||||
String get offlineLayers => 'Livelli Offline';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationTrail => 'Traccia Posizione';
|
String get locationTrail => 'Traccia Posizione';
|
||||||
|
|
||||||
@@ -1422,47 +903,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => 'Controlli Traccia';
|
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
|
@override
|
||||||
String get contactTrails => 'Tracce contatti';
|
String get contactTrails => 'Tracce contatti';
|
||||||
|
|
||||||
@@ -1584,7 +1024,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String get templateName => 'Nome modello';
|
String get templateName => 'Nome modello';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateNameHint => 'e.g. Found Person';
|
String get templateNameHint => 'ad es. Persona trovata';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateEmoji => 'Emoji';
|
String get templateEmoji => 'Emoji';
|
||||||
@@ -1596,16 +1036,16 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String get nameRequired => 'Nome è obbligatorio';
|
String get nameRequired => 'Nome è obbligatorio';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateDescription => 'Description (Optional)';
|
String get templateDescription => 'Descrizione (facoltativa)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateDescriptionHint => 'Add additional context...';
|
String get templateDescriptionHint => 'Aggiungi contesto aggiuntivo...';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateColor => 'Color';
|
String get templateColor => 'Colore';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get previewFormat => 'Preview (SAR Message Format)';
|
String get previewFormat => 'Anteprima (formato messaggio SAR)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get importFromClipboard => 'Importa';
|
String get importFromClipboard => 'Importa';
|
||||||
@@ -1615,26 +1055,26 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String deleteTemplateConfirmation(String name) {
|
String deleteTemplateConfirmation(String name) {
|
||||||
return 'Delete template \'$name\'?';
|
return 'Eliminare il modello \'$name\'?';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateAdded => 'Template added';
|
String get templateAdded => 'Modello aggiunto';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateUpdated => 'Template updated';
|
String get templateUpdated => 'Modello aggiornato';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get templateDeleted => 'Template deleted';
|
String get templateDeleted => 'Modello eliminato';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String templatesImported(int count) {
|
String templatesImported(int count) {
|
||||||
String _temp0 = intl.Intl.pluralLogic(
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
count,
|
count,
|
||||||
locale: localeName,
|
locale: localeName,
|
||||||
other: 'Imported $count templates',
|
other: 'Importati $count modelli',
|
||||||
one: 'Imported 1 template',
|
one: 'Importato 1 modello',
|
||||||
zero: 'No templates imported',
|
zero: 'Nessun modello importato',
|
||||||
);
|
);
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
@@ -1644,8 +1084,8 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String _temp0 = intl.Intl.pluralLogic(
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
count,
|
count,
|
||||||
locale: localeName,
|
locale: localeName,
|
||||||
other: 'Exported $count templates to clipboard',
|
other: 'Esportati $count modelli negli appunti',
|
||||||
one: 'Exported 1 template to clipboard',
|
one: 'Esportato 1 modello negli appunti',
|
||||||
);
|
);
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
@@ -1664,10 +1104,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String get resetComplete => 'Modelli ripristinati ai valori predefiniti';
|
String get resetComplete => 'Modelli ripristinati ai valori predefiniti';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noTemplates => 'No templates available';
|
String get noTemplates => 'Nessun modello disponibile';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get tapAddToCreate => 'Tap + to create your first template';
|
String get tapAddToCreate => 'Tocca + per creare il tuo primo modello';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get ok => 'OK';
|
String get ok => 'OK';
|
||||||
@@ -1734,20 +1174,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get noRoomsFound => 'Nessuna stanza trovata';
|
String get noRoomsFound => 'Nessuna stanza trovata';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noContactsOrRoomsAvailable =>
|
|
||||||
'Nessun contatto o stanza disponibile';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noRecipientsAvailable => 'Nessun destinatario disponibile';
|
String get noRecipientsAvailable => 'Nessun destinatario disponibile';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noChannelsFound => 'Nessun canale trovato';
|
String get noChannelsFound => 'Nessun canale trovato';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messagesWillBeSentToPublicChannel =>
|
|
||||||
'I messaggi saranno inviati al canale pubblico';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get newMessage => 'Nuovo messaggio';
|
String get newMessage => 'Nuovo messaggio';
|
||||||
|
|
||||||
@@ -1932,9 +1364,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get rectangleDrawing => 'Rettangolo';
|
String get rectangleDrawing => 'Rettangolo';
|
||||||
|
|
||||||
@override
|
|
||||||
String get coordinatesCopiedToClipboard => 'Coordinate copiate negli appunti';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get manualCoordinates => 'Coordinate Manuali';
|
String get manualCoordinates => 'Coordinate Manuali';
|
||||||
|
|
||||||
@@ -1947,31 +1376,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get longitudeLabel => 'Longitudine';
|
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
|
@override
|
||||||
String get exampleCoordinates => 'Esempio: 46.0569, 14.5058';
|
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
|
@override
|
||||||
String get shareDrawing => 'Condividi Disegno';
|
String get shareDrawing => 'Condividi Disegno';
|
||||||
|
|
||||||
@@ -2028,12 +1435,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get forestRoads => 'Strade Forestali';
|
String get forestRoads => 'Strade Forestali';
|
||||||
|
|
||||||
@override
|
|
||||||
String get showCadastralParcels => 'Mostra particelle catastali';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get showForestRoads => 'Mostra strade forestali';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get wmsOverlays => 'Sovrapposizioni WMS';
|
String get wmsOverlays => 'Sovrapposizioni WMS';
|
||||||
|
|
||||||
@@ -2138,10 +1539,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
return 'Eliminazione del canale fallita: $error';
|
return 'Eliminazione del canale fallita: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String get allChannelSlotsInUse =>
|
|
||||||
'Tutti gli slot dei canali sono in uso (massimo 39 canali personalizzati)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get createChannel => 'Crea Canale';
|
String get createChannel => 'Crea Canale';
|
||||||
|
|
||||||
@@ -2183,21 +1580,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String get wizardConnectingFeature3 =>
|
String get wizardConnectingFeature3 =>
|
||||||
'Funziona completamente offline - non è richiesta connessione internet';
|
'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
|
@override
|
||||||
String get wizardChannelTitle => 'Canali';
|
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
|
@override
|
||||||
String get disconnect => 'Отключить';
|
String get disconnect => 'Отключить';
|
||||||
|
|
||||||
@override
|
|
||||||
String get scanningForDevices => 'Поиск устройств...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDevicesFound => 'Устройства не найдены';
|
String get noDevicesFound => 'Устройства не найдены';
|
||||||
|
|
||||||
@@ -62,27 +59,14 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get failedToGetGpsLocation => 'Не удалось получить GPS-координаты';
|
String get failedToGetGpsLocation => 'Не удалось получить GPS-координаты';
|
||||||
|
|
||||||
@override
|
|
||||||
String advertisedAtLocation(String latitude, String longitude) {
|
|
||||||
return 'Транслируется в $latitude, $longitude';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String failedToAdvertise(String error) {
|
String failedToAdvertise(String error) {
|
||||||
return 'Ошибка трансляции: $error';
|
return 'Ошибка трансляции: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String reconnecting(int attempt, int max) {
|
|
||||||
return 'Переподключение... ($attempt/$max)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get cancelReconnection => 'Отменить переподключение';
|
String get cancelReconnection => 'Отменить переподключение';
|
||||||
|
|
||||||
@override
|
|
||||||
String get mapManagement => 'Управление картой';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get general => 'Основные';
|
String get general => 'Основные';
|
||||||
|
|
||||||
@@ -129,13 +113,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
String get displayPacketActivity =>
|
String get displayPacketActivity =>
|
||||||
'Отображать активность пакетов в верхней панели';
|
'Отображать активность пакетов в верхней панели';
|
||||||
|
|
||||||
@override
|
|
||||||
String get simpleMode => 'Простой режим';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get simpleModeDescription =>
|
|
||||||
'Скрыть второстепенную информацию в сообщениях и контактах';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get disableMap => 'Отключить карту';
|
String get disableMap => 'Отключить карту';
|
||||||
|
|
||||||
@@ -149,75 +126,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chooseLanguage => 'Выбрать язык';
|
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
|
@override
|
||||||
String get save => 'Сохранить';
|
String get save => 'Сохранить';
|
||||||
|
|
||||||
@@ -253,12 +161,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get moreInfo => 'Подробнее';
|
String get moreInfo => 'Подробнее';
|
||||||
|
|
||||||
@override
|
|
||||||
String get learnMoreAbout => 'Узнать больше о MeshCore SAR';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get developer => 'Разработчик';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get packageName => 'Имя пакета';
|
String get packageName => 'Имя пакета';
|
||||||
|
|
||||||
@@ -334,9 +236,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get refresh => 'Обновить';
|
String get refresh => 'Обновить';
|
||||||
|
|
||||||
@override
|
|
||||||
String get sendDirectMessage => 'Отправить';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get resetPath => 'Сбросить маршрут (перепроложить)';
|
String get resetPath => 'Сбросить маршрут (перепроложить)';
|
||||||
|
|
||||||
@@ -457,21 +356,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
return 'Поделиться $count рисун$plural';
|
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
|
@override
|
||||||
String get showReceivedDrawings => 'Показать полученные рисунки';
|
String get showReceivedDrawings => 'Показать полученные рисунки';
|
||||||
|
|
||||||
@@ -493,9 +377,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get clearAll => 'Очистить всё';
|
String get clearAll => 'Очистить всё';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noLocalDrawings => 'Нет локальных рисунков для отправки';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get publicChannel => 'Публичный канал';
|
String get publicChannel => 'Публичный канал';
|
||||||
|
|
||||||
@@ -505,37 +386,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get storedPermanently => 'Сохранено постоянно в комнате';
|
String get storedPermanently => 'Сохранено постоянно в комнате';
|
||||||
|
|
||||||
@override
|
|
||||||
String drawingsSentToPublicChannel(int count, String plural) {
|
|
||||||
return 'Отправлено $count рисун$plural в публичный канал';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String drawingsSharedToPublicChannel(int success, int total) {
|
|
||||||
return 'Передано $success/$total рисунков в публичный канал';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get notConnectedToDevice => 'Устройство не подключено';
|
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
|
@override
|
||||||
String get typeYourMessage => 'Введите сообщение...';
|
String get typeYourMessage => 'Введите сообщение...';
|
||||||
|
|
||||||
@@ -565,9 +418,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get location => 'Местоположение';
|
String get location => 'Местоположение';
|
||||||
|
|
||||||
@override
|
|
||||||
String get myLocation => 'Моё местоположение';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fromMap => 'С карты';
|
String get fromMap => 'С карты';
|
||||||
|
|
||||||
@@ -630,9 +480,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get flood => 'Широковещательно';
|
String get flood => 'Широковещательно';
|
||||||
|
|
||||||
@override
|
|
||||||
String get admin => 'Администратор';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get loggedIn => 'Вход выполнен';
|
String get loggedIn => 'Вход выполнен';
|
||||||
|
|
||||||
@@ -642,29 +489,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get distance => 'Расстояние';
|
String get distance => 'Расстояние';
|
||||||
|
|
||||||
@override
|
|
||||||
String pingingDirect(String name) {
|
|
||||||
return 'Пинг $name (напрямую по маршруту)...';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String pingingFlood(String name) {
|
|
||||||
return 'Пинг $name (широковещательно — нет маршрута)...';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String directPingTimeout(String name) {
|
String directPingTimeout(String name) {
|
||||||
return 'Таймаут прямого пинга — повтор $name широковещательно...';
|
return 'Таймаут прямого пинга — повтор $name широковещательно...';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String pingSuccessful(String name, String fallback) {
|
|
||||||
return 'Пинг успешен: $name$fallback';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get viaFloodingFallback => ' (через широковещательный резерв)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String pingFailed(String name) {
|
String pingFailed(String name) {
|
||||||
return 'Пинг не удался: $name — ответ не получен';
|
return 'Пинг не удался: $name — ответ не получен';
|
||||||
@@ -675,16 +504,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
return 'Вы уверены, что хотите удалить \"$name\"?\n\nЭто удалит контакт как из приложения, так и с сопряжённого радиоустройства.';
|
return 'Вы уверены, что хотите удалить \"$name\"?\n\nЭто удалит контакт как из приложения, так и с сопряжённого радиоустройства.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String removingContact(String name) {
|
|
||||||
return 'Удаление $name...';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String contactRemoved(String name) {
|
|
||||||
return 'Контакт \"$name\" удалён';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String failedToRemoveContact(String error) {
|
String failedToRemoveContact(String error) {
|
||||||
return 'Не удалось удалить контакт: $error';
|
return 'Не удалось удалить контакт: $error';
|
||||||
@@ -729,11 +548,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get telemetry => 'Телеметрия';
|
String get telemetry => 'Телеметрия';
|
||||||
|
|
||||||
@override
|
|
||||||
String requestingTelemetry(String name) {
|
|
||||||
return 'Запрос телеметрии от $name...';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get voltage => 'Напряжение';
|
String get voltage => 'Напряжение';
|
||||||
|
|
||||||
@@ -805,9 +619,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get showAll => 'Показать все';
|
String get showAll => 'Показать все';
|
||||||
|
|
||||||
@override
|
|
||||||
String get nearbyContacts => 'Ближайшие контакты';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationUnavailable => 'Местоположение недоступно';
|
String get locationUnavailable => 'Местоположение недоступно';
|
||||||
|
|
||||||
@@ -848,173 +659,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels => 'Каналы';
|
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
|
@override
|
||||||
String get selectMapLayer => 'Выбрать слой карты';
|
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
|
@override
|
||||||
String get openStreetMap => 'OpenStreetMap';
|
String get openStreetMap => 'OpenStreetMap';
|
||||||
|
|
||||||
@@ -1033,12 +680,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get googleTerrain => 'Google Рельеф';
|
String get googleTerrain => 'Google Рельеф';
|
||||||
|
|
||||||
@override
|
|
||||||
String get downloadVisibleArea => 'Скачать видимую область';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get initializingMap => 'Инициализация карты...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dragToPosition => 'Перетащите для позиционирования';
|
String get dragToPosition => 'Перетащите для позиционирования';
|
||||||
|
|
||||||
@@ -1054,52 +695,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarAlert => 'ТРЕВОГА SAR';
|
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
|
@override
|
||||||
String get textCopiedToClipboard => 'Текст скопирован в буфер обмена';
|
String get textCopiedToClipboard => 'Текст скопирован в буфер обмена';
|
||||||
|
|
||||||
@@ -1111,9 +706,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
String get cannotReplyContactNotFound =>
|
String get cannotReplyContactNotFound =>
|
||||||
'Не удаётся ответить: контакт не найден';
|
'Не удаётся ответить: контакт не найден';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messageDeleted => 'Сообщение удалено';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get copyText => 'Копировать текст';
|
String get copyText => 'Копировать текст';
|
||||||
|
|
||||||
@@ -1149,12 +741,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarLocationShare => 'Местоположение SAR';
|
String get sarLocationShare => 'Местоположение SAR';
|
||||||
|
|
||||||
@override
|
|
||||||
String get locationShared => 'Местоположение передано';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get refreshedContacts => 'Контакты обновлены';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get justNow => 'Только что';
|
String get justNow => 'Только что';
|
||||||
|
|
||||||
@@ -1262,78 +848,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get you => 'Вы';
|
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
|
@override
|
||||||
String exportFailed(String error) {
|
String exportFailed(String error) {
|
||||||
return 'Ошибка экспорта: $error';
|
return 'Ошибка экспорта: $error';
|
||||||
@@ -1344,40 +858,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
return 'Ошибка импорта: $error';
|
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
|
@override
|
||||||
String get unknown => 'Неизвестно';
|
String get unknown => 'Неизвестно';
|
||||||
|
|
||||||
@override
|
|
||||||
String get bounds => 'Границы';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onlineLayers => 'Онлайн-слои';
|
String get onlineLayers => 'Онлайн-слои';
|
||||||
|
|
||||||
@override
|
|
||||||
String get offlineLayers => 'Офлайн-слои';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationTrail => 'Трек местоположения';
|
String get locationTrail => 'Трек местоположения';
|
||||||
|
|
||||||
@@ -1416,47 +902,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => 'Управление треком';
|
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
|
@override
|
||||||
String get contactTrails => 'Треки контактов';
|
String get contactTrails => 'Треки контактов';
|
||||||
|
|
||||||
@@ -1729,19 +1174,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get noRoomsFound => 'Комнаты не найдены';
|
String get noRoomsFound => 'Комнаты не найдены';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noContactsOrRoomsAvailable => 'Нет доступных контактов или комнат';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noRecipientsAvailable => 'Нет доступных получателей';
|
String get noRecipientsAvailable => 'Нет доступных получателей';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noChannelsFound => 'Каналы не найдены';
|
String get noChannelsFound => 'Каналы не найдены';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messagesWillBeSentToPublicChannel =>
|
|
||||||
'Сообщения будут отправлены в публичный канал';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get newMessage => 'Новое сообщение';
|
String get newMessage => 'Новое сообщение';
|
||||||
|
|
||||||
@@ -1922,10 +1360,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get rectangleDrawing => 'Прямоугольник';
|
String get rectangleDrawing => 'Прямоугольник';
|
||||||
|
|
||||||
@override
|
|
||||||
String get coordinatesCopiedToClipboard =>
|
|
||||||
'Координаты скопированы в буфер обмена';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get manualCoordinates => 'Ввод координат вручную';
|
String get manualCoordinates => 'Ввод координат вручную';
|
||||||
|
|
||||||
@@ -1938,37 +1372,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get longitudeLabel => 'Долгота';
|
String get longitudeLabel => 'Долгота';
|
||||||
|
|
||||||
@override
|
|
||||||
String get invalidLatitude => 'Неверная широта (от -90 до 90)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get invalidLongitude => 'Неверная долгота (от -180 до 180)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get exampleCoordinates => 'Пример: 55.7558, 37.6173';
|
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
|
@override
|
||||||
String get shareDrawing => 'Поделиться рисунком';
|
String get shareDrawing => 'Поделиться рисунком';
|
||||||
|
|
||||||
@@ -2025,12 +1431,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get forestRoads => 'Лесные дороги';
|
String get forestRoads => 'Лесные дороги';
|
||||||
|
|
||||||
@override
|
|
||||||
String get showCadastralParcels => 'Показать кадастровые участки';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get showForestRoads => 'Показать лесные дороги';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get wmsOverlays => 'WMS-наложения';
|
String get wmsOverlays => 'WMS-наложения';
|
||||||
|
|
||||||
@@ -2135,10 +1535,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
return 'Не удалось удалить канал: $error';
|
return 'Не удалось удалить канал: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String get allChannelSlotsInUse =>
|
|
||||||
'Все слоты каналов заняты (максимум 39 пользовательских каналов)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get createChannel => 'Создать канал';
|
String get createChannel => 'Создать канал';
|
||||||
|
|
||||||
@@ -2179,21 +1575,6 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
String get wizardConnectingFeature3 =>
|
String get wizardConnectingFeature3 =>
|
||||||
'Работает полностью офлайн — интернет не нужен';
|
'Работает полностью офлайн — интернет не нужен';
|
||||||
|
|
||||||
@override
|
|
||||||
String get wizardSimpleModeTitle => 'Простой режим';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get wizardSimpleModeDescription =>
|
|
||||||
'Впервые работаете с mesh-сетью? Включите простой режим для упрощённого интерфейса с основными функциями.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get wizardSimpleModeFeature1 =>
|
|
||||||
'Удобный для новичков интерфейс с основными функциями';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get wizardSimpleModeFeature2 =>
|
|
||||||
'Переключиться в расширенный режим можно в любое время в настройках';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get wizardChannelTitle => 'Каналы';
|
String get wizardChannelTitle => 'Каналы';
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get disconnect => 'Prekini';
|
String get disconnect => 'Prekini';
|
||||||
|
|
||||||
@override
|
|
||||||
String get scanningForDevices => 'Iskanje naprav...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDevicesFound => 'Ni najdenih naprav';
|
String get noDevicesFound => 'Ni najdenih naprav';
|
||||||
|
|
||||||
@@ -62,27 +59,14 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get failedToGetGpsLocation => 'Pridobitev GPS lokacije ni uspela';
|
String get failedToGetGpsLocation => 'Pridobitev GPS lokacije ni uspela';
|
||||||
|
|
||||||
@override
|
|
||||||
String advertisedAtLocation(String latitude, String longitude) {
|
|
||||||
return 'Objavljeno na $latitude, $longitude';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String failedToAdvertise(String error) {
|
String failedToAdvertise(String error) {
|
||||||
return 'Objava ni uspela: $error';
|
return 'Objava ni uspela: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String reconnecting(int attempt, int max) {
|
|
||||||
return 'Ponovno povezovanje... ($attempt/$max)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get cancelReconnection => 'Prekliči ponovno povezovanje';
|
String get cancelReconnection => 'Prekliči ponovno povezovanje';
|
||||||
|
|
||||||
@override
|
|
||||||
String get mapManagement => 'Upravljanje zemljevida';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get general => 'Splošno';
|
String get general => 'Splošno';
|
||||||
|
|
||||||
@@ -129,13 +113,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
String get displayPacketActivity =>
|
String get displayPacketActivity =>
|
||||||
'Prikaži kazalnike aktivnosti paketov v zgornji vrstici';
|
'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
|
@override
|
||||||
String get disableMap => 'Onemogoči zemljevid';
|
String get disableMap => 'Onemogoči zemljevid';
|
||||||
|
|
||||||
@@ -149,74 +126,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chooseLanguage => 'Izberite jezik';
|
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
|
@override
|
||||||
String get save => 'Shrani';
|
String get save => 'Shrani';
|
||||||
|
|
||||||
@@ -252,12 +161,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get moreInfo => 'Več informacij';
|
String get moreInfo => 'Več informacij';
|
||||||
|
|
||||||
@override
|
|
||||||
String get learnMoreAbout => 'Več o MeshCore SAR';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get developer => 'Razvijalec';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get packageName => 'Ime paketa';
|
String get packageName => 'Ime paketa';
|
||||||
|
|
||||||
@@ -333,9 +236,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get refresh => 'Osveži';
|
String get refresh => 'Osveži';
|
||||||
|
|
||||||
@override
|
|
||||||
String get sendDirectMessage => 'Pošlji';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get resetPath => 'Ponastavi pot (preusmeri)';
|
String get resetPath => 'Ponastavi pot (preusmeri)';
|
||||||
|
|
||||||
@@ -454,21 +354,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
return 'Deli $count risb$plural';
|
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
|
@override
|
||||||
String get showReceivedDrawings => 'Prikaži prejete risbe';
|
String get showReceivedDrawings => 'Prikaži prejete risbe';
|
||||||
|
|
||||||
@@ -490,9 +375,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get clearAll => 'Počisti vse';
|
String get clearAll => 'Počisti vse';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noLocalDrawings => 'Ni lokalnih risb za deljenje';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get publicChannel => 'Javni kanal';
|
String get publicChannel => 'Javni kanal';
|
||||||
|
|
||||||
@@ -502,37 +384,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get storedPermanently => 'Trajno shranjeno v sobi';
|
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
|
@override
|
||||||
String get notConnectedToDevice => 'Ni povezano z napravo';
|
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
|
@override
|
||||||
String get typeYourMessage => 'Vnesite svoje sporočilo...';
|
String get typeYourMessage => 'Vnesite svoje sporočilo...';
|
||||||
|
|
||||||
@@ -562,9 +416,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get location => 'Lokacija';
|
String get location => 'Lokacija';
|
||||||
|
|
||||||
@override
|
|
||||||
String get myLocation => 'Moja lokacija';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fromMap => 'Z zemljevida';
|
String get fromMap => 'Z zemljevida';
|
||||||
|
|
||||||
@@ -626,9 +477,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get flood => 'Razpršitev';
|
String get flood => 'Razpršitev';
|
||||||
|
|
||||||
@override
|
|
||||||
String get admin => 'Administrator';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get loggedIn => 'Prijavljen';
|
String get loggedIn => 'Prijavljen';
|
||||||
|
|
||||||
@@ -638,29 +486,11 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get distance => 'Razdalja';
|
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
|
@override
|
||||||
String directPingTimeout(String name) {
|
String directPingTimeout(String name) {
|
||||||
return 'Časovna omejitev neposrednega pinga - ponovni poskus $name z razprševanjem...';
|
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
|
@override
|
||||||
String pingFailed(String name) {
|
String pingFailed(String name) {
|
||||||
return 'Ping neuspešen do $name - odgovor ni prejet';
|
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.';
|
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
|
@override
|
||||||
String failedToRemoveContact(String error) {
|
String failedToRemoveContact(String error) {
|
||||||
return 'Odstranjevanje stika ni uspelo: $error';
|
return 'Odstranjevanje stika ni uspelo: $error';
|
||||||
@@ -725,11 +545,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get telemetry => 'Telemetrija';
|
String get telemetry => 'Telemetrija';
|
||||||
|
|
||||||
@override
|
|
||||||
String requestingTelemetry(String name) {
|
|
||||||
return 'Zahtevanje telemetrije od $name...';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get voltage => 'Napetost';
|
String get voltage => 'Napetost';
|
||||||
|
|
||||||
@@ -801,9 +616,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get showAll => 'Prikaži vse';
|
String get showAll => 'Prikaži vse';
|
||||||
|
|
||||||
@override
|
|
||||||
String get nearbyContacts => 'Bližnji stiki';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationUnavailable => 'Lokacija ni na voljo';
|
String get locationUnavailable => 'Lokacija ni na voljo';
|
||||||
|
|
||||||
@@ -844,174 +656,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels => 'Kanali';
|
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
|
@override
|
||||||
String get selectMapLayer => 'Izberite sloj zemljevida';
|
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
|
@override
|
||||||
String get openStreetMap => 'OpenStreetMap';
|
String get openStreetMap => 'OpenStreetMap';
|
||||||
|
|
||||||
@@ -1030,12 +677,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get googleTerrain => 'Google teren';
|
String get googleTerrain => 'Google teren';
|
||||||
|
|
||||||
@override
|
|
||||||
String get downloadVisibleArea => 'Prenesi vidno območje';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get initializingMap => 'Inicializacija zemljevida...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dragToPosition => 'Povleci na položaj';
|
String get dragToPosition => 'Povleci na položaj';
|
||||||
|
|
||||||
@@ -1051,51 +692,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarAlert => 'SAR ALARM';
|
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
|
@override
|
||||||
String get textCopiedToClipboard => 'Besedilo kopirano v odložišče';
|
String get textCopiedToClipboard => 'Besedilo kopirano v odložišče';
|
||||||
|
|
||||||
@@ -1107,9 +703,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
String get cannotReplyContactNotFound =>
|
String get cannotReplyContactNotFound =>
|
||||||
'Ni mogoče odgovoriti: stik ni najden';
|
'Ni mogoče odgovoriti: stik ni najden';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messageDeleted => 'Sporočilo izbrisano';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get copyText => 'Kopiraj besedilo';
|
String get copyText => 'Kopiraj besedilo';
|
||||||
|
|
||||||
@@ -1145,12 +738,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarLocationShare => 'SAR Lokacija';
|
String get sarLocationShare => 'SAR Lokacija';
|
||||||
|
|
||||||
@override
|
|
||||||
String get locationShared => 'Lokacija deljena';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get refreshedContacts => 'Stiki osveženi';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get justNow => 'Pravkar';
|
String get justNow => 'Pravkar';
|
||||||
|
|
||||||
@@ -1258,81 +845,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get you => 'Ti';
|
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
|
@override
|
||||||
String exportFailed(String error) {
|
String exportFailed(String error) {
|
||||||
return 'Izvoz ni uspel: $error';
|
return 'Izvoz ni uspel: $error';
|
||||||
@@ -1343,40 +855,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
return 'Uvoz ni uspel: $error';
|
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
|
@override
|
||||||
String get unknown => 'Neznano';
|
String get unknown => 'Neznano';
|
||||||
|
|
||||||
@override
|
|
||||||
String get bounds => 'Meje';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onlineLayers => 'Spletne plasti';
|
String get onlineLayers => 'Spletne plasti';
|
||||||
|
|
||||||
@override
|
|
||||||
String get offlineLayers => 'Brezpovezne plasti';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationTrail => 'Sledilna pot';
|
String get locationTrail => 'Sledilna pot';
|
||||||
|
|
||||||
@@ -1415,47 +899,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => 'Nadzor poti';
|
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
|
@override
|
||||||
String get contactTrails => 'Poti stikov';
|
String get contactTrails => 'Poti stikov';
|
||||||
|
|
||||||
@@ -1661,7 +1104,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
String get tapAddToCreate => 'Tapnite + za ustvarjanje prve predloge';
|
String get tapAddToCreate => 'Tapnite + za ustvarjanje prve predloge';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get ok => 'OK';
|
String get ok => 'V redu';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get permissionsSection => 'Dovoljenja';
|
String get permissionsSection => 'Dovoljenja';
|
||||||
@@ -1725,19 +1168,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get noRoomsFound => 'Ni sob';
|
String get noRoomsFound => 'Ni sob';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noContactsOrRoomsAvailable => 'Ni na voljo kontaktov ali sob';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noRecipientsAvailable => 'Ni na voljo prejemnikov';
|
String get noRecipientsAvailable => 'Ni na voljo prejemnikov';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noChannelsFound => 'Ni najdenih kanalov';
|
String get noChannelsFound => 'Ni najdenih kanalov';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messagesWillBeSentToPublicChannel =>
|
|
||||||
'Sporočila bodo poslana na javni kanal';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get newMessage => 'Novo sporočilo';
|
String get newMessage => 'Novo sporočilo';
|
||||||
|
|
||||||
@@ -1918,9 +1354,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get rectangleDrawing => 'Pravokotna risba';
|
String get rectangleDrawing => 'Pravokotna risba';
|
||||||
|
|
||||||
@override
|
|
||||||
String get coordinatesCopiedToClipboard => 'Koordinate kopirane v odložišče';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get manualCoordinates => 'Ročne koordinate';
|
String get manualCoordinates => 'Ročne koordinate';
|
||||||
|
|
||||||
@@ -1933,37 +1366,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get longitudeLabel => 'Geografska dolžina';
|
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
|
@override
|
||||||
String get exampleCoordinates => 'Primer: 46.0569, 14.5058';
|
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
|
@override
|
||||||
String get shareDrawing => 'Deli risbo';
|
String get shareDrawing => 'Deli risbo';
|
||||||
|
|
||||||
@@ -2018,12 +1423,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get forestRoads => 'Gozdne ceste';
|
String get forestRoads => 'Gozdne ceste';
|
||||||
|
|
||||||
@override
|
|
||||||
String get showCadastralParcels => 'Prikaži katastrske parcele';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get showForestRoads => 'Prikaži gozdne ceste';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get wmsOverlays => 'WMS prekrivanja';
|
String get wmsOverlays => 'WMS prekrivanja';
|
||||||
|
|
||||||
@@ -2126,10 +1525,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
return 'Neuspešno brisanje kanala: $error';
|
return 'Neuspešno brisanje kanala: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String get allChannelSlotsInUse =>
|
|
||||||
'Vsa mesta za kanale so zasedena (maksimalno 39 prilagojenih kanalov)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get createChannel => 'Ustvari kanal';
|
String get createChannel => 'Ustvari kanal';
|
||||||
|
|
||||||
@@ -2170,21 +1565,6 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
String get wizardConnectingFeature3 =>
|
String get wizardConnectingFeature3 =>
|
||||||
'Deluje povsem brez povezave — internet ni potreben';
|
'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
|
@override
|
||||||
String get wizardChannelTitle => 'Kanali';
|
String get wizardChannelTitle => 'Kanali';
|
||||||
|
|
||||||
@@ -2279,7 +1659,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
String get delivery => 'Dostava';
|
String get delivery => 'Dostava';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get status => 'Status';
|
String get status => 'Stanje';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get expectedAckTag => 'Pričakovana oznaka ACK';
|
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
|
@override
|
||||||
String get disconnect => '断开连接';
|
String get disconnect => '断开连接';
|
||||||
|
|
||||||
@override
|
|
||||||
String get scanningForDevices => '正在扫描设备...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noDevicesFound => '未找到设备';
|
String get noDevicesFound => '未找到设备';
|
||||||
|
|
||||||
@@ -59,27 +56,14 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get failedToGetGpsLocation => '获取 GPS 位置失败';
|
String get failedToGetGpsLocation => '获取 GPS 位置失败';
|
||||||
|
|
||||||
@override
|
|
||||||
String advertisedAtLocation(String latitude, String longitude) {
|
|
||||||
return '已广播位置 $latitude, $longitude';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String failedToAdvertise(String error) {
|
String failedToAdvertise(String error) {
|
||||||
return '广播失败: $error';
|
return '广播失败: $error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String reconnecting(int attempt, int max) {
|
|
||||||
return '正在重新连接... ($attempt/$max)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get cancelReconnection => '取消重新连接';
|
String get cancelReconnection => '取消重新连接';
|
||||||
|
|
||||||
@override
|
|
||||||
String get mapManagement => '地图管理';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get general => '通用';
|
String get general => '通用';
|
||||||
|
|
||||||
@@ -125,12 +109,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get displayPacketActivity => '在顶部栏显示数据包活动指示器';
|
String get displayPacketActivity => '在顶部栏显示数据包活动指示器';
|
||||||
|
|
||||||
@override
|
|
||||||
String get simpleMode => '简洁模式';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get simpleModeDescription => '在消息和联系人中隐藏非必要信息';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get disableMap => '禁用地图';
|
String get disableMap => '禁用地图';
|
||||||
|
|
||||||
@@ -143,72 +121,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get chooseLanguage => '选择语言';
|
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
|
@override
|
||||||
String get save => '保存';
|
String get save => '保存';
|
||||||
|
|
||||||
@@ -244,12 +156,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get moreInfo => '更多信息';
|
String get moreInfo => '更多信息';
|
||||||
|
|
||||||
@override
|
|
||||||
String get learnMoreAbout => '了解更多关于 MeshCore 搜救助手的信息';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get developer => '开发者';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get packageName => '包名';
|
String get packageName => '包名';
|
||||||
|
|
||||||
@@ -321,9 +227,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get refresh => '刷新';
|
String get refresh => '刷新';
|
||||||
|
|
||||||
@override
|
|
||||||
String get sendDirectMessage => '发送';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get resetPath => '重置路径 (重新路由)';
|
String get resetPath => '重置路径 (重新路由)';
|
||||||
|
|
||||||
@@ -441,21 +344,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
return '共享 $count 个绘图';
|
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
|
@override
|
||||||
String get showReceivedDrawings => '显示接收到的绘图';
|
String get showReceivedDrawings => '显示接收到的绘图';
|
||||||
|
|
||||||
@@ -477,9 +365,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get clearAll => '全部清除';
|
String get clearAll => '全部清除';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noLocalDrawings => '没有可共享的本地绘图';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get publicChannel => '公共频道';
|
String get publicChannel => '公共频道';
|
||||||
|
|
||||||
@@ -489,37 +374,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get storedPermanently => '永久存储在房间中';
|
String get storedPermanently => '永久存储在房间中';
|
||||||
|
|
||||||
@override
|
|
||||||
String drawingsSentToPublicChannel(int count, String plural) {
|
|
||||||
return '已将 $count 个地图绘制发送到公共频道';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String drawingsSharedToPublicChannel(int success, int total) {
|
|
||||||
return '已将 $success/$total 个绘图共享到公共频道';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get notConnectedToDevice => '未连接到设备';
|
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
|
@override
|
||||||
String get typeYourMessage => '输入您的消息...';
|
String get typeYourMessage => '输入您的消息...';
|
||||||
|
|
||||||
@@ -547,9 +404,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get location => '位置';
|
String get location => '位置';
|
||||||
|
|
||||||
@override
|
|
||||||
String get myLocation => '我的位置';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get fromMap => '来自地图';
|
String get fromMap => '来自地图';
|
||||||
|
|
||||||
@@ -610,9 +464,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get flood => '泛洪';
|
String get flood => '泛洪';
|
||||||
|
|
||||||
@override
|
|
||||||
String get admin => '管理员';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get loggedIn => '已登录';
|
String get loggedIn => '已登录';
|
||||||
|
|
||||||
@@ -622,29 +473,11 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get distance => '距离';
|
String get distance => '距离';
|
||||||
|
|
||||||
@override
|
|
||||||
String pingingDirect(String name) {
|
|
||||||
return '正在 Ping $name(通过路径直连发送)...';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String pingingFlood(String name) {
|
|
||||||
return '正在 Ping $name(泛洪 - 无路径)...';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String directPingTimeout(String name) {
|
String directPingTimeout(String name) {
|
||||||
return '直连 Ping 超时 - 正在使用泛洪重试 $name...';
|
return '直连 Ping 超时 - 正在使用泛洪重试 $name...';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String pingSuccessful(String name, String fallback) {
|
|
||||||
return '成功 Ping 到 $name$fallback';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get viaFloodingFallback => '(通过泛洪回退)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String pingFailed(String name) {
|
String pingFailed(String name) {
|
||||||
return 'Ping $name 失败 - 未收到响应';
|
return 'Ping $name 失败 - 未收到响应';
|
||||||
@@ -655,16 +488,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
return '您确定要删除“$name”吗?\n\n这将从应用程序和配套的无线电设备中移除该联系人。';
|
return '您确定要删除“$name”吗?\n\n这将从应用程序和配套的无线电设备中移除该联系人。';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String removingContact(String name) {
|
|
||||||
return '正在移除 $name...';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String contactRemoved(String name) {
|
|
||||||
return '联系人“$name”已移除';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String failedToRemoveContact(String error) {
|
String failedToRemoveContact(String error) {
|
||||||
return '移除联系人失败:$error';
|
return '移除联系人失败:$error';
|
||||||
@@ -709,11 +532,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get telemetry => '遥测';
|
String get telemetry => '遥测';
|
||||||
|
|
||||||
@override
|
|
||||||
String requestingTelemetry(String name) {
|
|
||||||
return '正在向 $name 请求遥测数据...';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get voltage => '电压';
|
String get voltage => '电压';
|
||||||
|
|
||||||
@@ -785,9 +603,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get showAll => '显示全部';
|
String get showAll => '显示全部';
|
||||||
|
|
||||||
@override
|
|
||||||
String get nearbyContacts => '附近联系人';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationUnavailable => '位置不可用';
|
String get locationUnavailable => '位置不可用';
|
||||||
|
|
||||||
@@ -827,169 +642,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get channels => '频道';
|
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
|
@override
|
||||||
String get selectMapLayer => '选择地图图层';
|
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
|
@override
|
||||||
String get openStreetMap => 'OpenStreetMap';
|
String get openStreetMap => 'OpenStreetMap';
|
||||||
|
|
||||||
@@ -1008,12 +663,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get googleTerrain => 'Google 地形图';
|
String get googleTerrain => 'Google 地形图';
|
||||||
|
|
||||||
@override
|
|
||||||
String get downloadVisibleArea => '下载可见区域';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get initializingMap => '正在初始化地图...';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get dragToPosition => '拖动以定位';
|
String get dragToPosition => '拖动以定位';
|
||||||
|
|
||||||
@@ -1029,49 +678,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarAlert => '搜救警报';
|
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
|
@override
|
||||||
String get textCopiedToClipboard => '文本已复制到剪贴板';
|
String get textCopiedToClipboard => '文本已复制到剪贴板';
|
||||||
|
|
||||||
@@ -1081,9 +687,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get cannotReplyContactNotFound => '无法回复:未找到联系人';
|
String get cannotReplyContactNotFound => '无法回复:未找到联系人';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messageDeleted => '消息已删除';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get copyText => '复制文本';
|
String get copyText => '复制文本';
|
||||||
|
|
||||||
@@ -1118,12 +721,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get sarLocationShare => '搜救位置';
|
String get sarLocationShare => '搜救位置';
|
||||||
|
|
||||||
@override
|
|
||||||
String get locationShared => '位置已共享';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get refreshedContacts => '联系人已刷新';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get justNow => '刚刚';
|
String get justNow => '刚刚';
|
||||||
|
|
||||||
@@ -1231,77 +828,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get you => '您';
|
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
|
@override
|
||||||
String exportFailed(String error) {
|
String exportFailed(String error) {
|
||||||
return '导出失败:$error';
|
return '导出失败:$error';
|
||||||
@@ -1312,38 +838,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
return '导入失败:$error';
|
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
|
@override
|
||||||
String get unknown => '未知';
|
String get unknown => '未知';
|
||||||
|
|
||||||
@override
|
|
||||||
String get bounds => '边界';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get onlineLayers => '在线图层';
|
String get onlineLayers => '在线图层';
|
||||||
|
|
||||||
@override
|
|
||||||
String get offlineLayers => '离线图层';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get locationTrail => '位置轨迹';
|
String get locationTrail => '位置轨迹';
|
||||||
|
|
||||||
@@ -1380,47 +880,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get trailControls => '轨迹控制';
|
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
|
@override
|
||||||
String get contactTrails => '联系人轨迹';
|
String get contactTrails => '联系人轨迹';
|
||||||
|
|
||||||
@@ -1672,18 +1131,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get noRoomsFound => '未找到房间';
|
String get noRoomsFound => '未找到房间';
|
||||||
|
|
||||||
@override
|
|
||||||
String get noContactsOrRoomsAvailable => '没有可用的联系人或房间';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noRecipientsAvailable => '没有可用的收件人';
|
String get noRecipientsAvailable => '没有可用的收件人';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noChannelsFound => '未找到频道';
|
String get noChannelsFound => '未找到频道';
|
||||||
|
|
||||||
@override
|
|
||||||
String get messagesWillBeSentToPublicChannel => '消息将发送到公共频道';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get newMessage => '新消息';
|
String get newMessage => '新消息';
|
||||||
|
|
||||||
@@ -1855,9 +1308,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get rectangleDrawing => '矩形绘图';
|
String get rectangleDrawing => '矩形绘图';
|
||||||
|
|
||||||
@override
|
|
||||||
String get coordinatesCopiedToClipboard => '坐标已复制到剪贴板';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get manualCoordinates => '手动输入坐标';
|
String get manualCoordinates => '手动输入坐标';
|
||||||
|
|
||||||
@@ -1870,31 +1320,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get longitudeLabel => '经度';
|
String get longitudeLabel => '经度';
|
||||||
|
|
||||||
@override
|
|
||||||
String get invalidLatitude => '无效的纬度(-90 到 90)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get invalidLongitude => '无效的经度(-180 到 180)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get exampleCoordinates => '例如:46.0569, 14.5058';
|
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
|
@override
|
||||||
String get shareDrawing => '共享绘图';
|
String get shareDrawing => '共享绘图';
|
||||||
|
|
||||||
@@ -1948,12 +1376,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get forestRoads => '森林道路';
|
String get forestRoads => '森林道路';
|
||||||
|
|
||||||
@override
|
|
||||||
String get showCadastralParcels => '显示地籍地块';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get showForestRoads => '显示森林道路';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get wmsOverlays => 'WMS 覆盖层';
|
String get wmsOverlays => 'WMS 覆盖层';
|
||||||
|
|
||||||
@@ -2054,9 +1476,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
return '删除频道失败:$error';
|
return '删除频道失败:$error';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
String get allChannelSlotsInUse => '所有频道槽位都已在使用中(最多 39 个自定义频道)';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get createChannel => '创建频道';
|
String get createChannel => '创建频道';
|
||||||
|
|
||||||
@@ -2095,19 +1514,6 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
@override
|
@override
|
||||||
String get wizardConnectingFeature3 => '完全离线工作 - 无需互联网';
|
String get wizardConnectingFeature3 => '完全离线工作 - 无需互联网';
|
||||||
|
|
||||||
@override
|
|
||||||
String get wizardSimpleModeTitle => '简洁模式';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get wizardSimpleModeDescription =>
|
|
||||||
'刚接触 Mesh 网络?启用简洁模式以获得仅包含基本功能的简化界面。';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get wizardSimpleModeFeature1 => '对初学者友好的界面,包含核心功能';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get wizardSimpleModeFeature2 => '随时在设置中切换到高级模式';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get wizardChannelTitle => '频道';
|
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
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"@@locale": "ru",
|
"@@locale": "ru",
|
||||||
|
|
||||||
"appTitle": "MeshCore SAR",
|
"appTitle": "MeshCore SAR",
|
||||||
"messages": "Сообщения",
|
"messages": "Сообщения",
|
||||||
"contacts": "Контакты",
|
"contacts": "Контакты",
|
||||||
@@ -8,7 +7,6 @@
|
|||||||
"settings": "Настройки",
|
"settings": "Настройки",
|
||||||
"connect": "Подключить",
|
"connect": "Подключить",
|
||||||
"disconnect": "Отключить",
|
"disconnect": "Отключить",
|
||||||
"scanningForDevices": "Поиск устройств...",
|
|
||||||
"noDevicesFound": "Устройства не найдены",
|
"noDevicesFound": "Устройства не найдены",
|
||||||
"scanAgain": "Повторить поиск",
|
"scanAgain": "Повторить поиск",
|
||||||
"tapToConnect": "Нажмите для подключения",
|
"tapToConnect": "Нажмите для подключения",
|
||||||
@@ -18,11 +16,8 @@
|
|||||||
"locationPermissionRequired": "Доступ к геолокации необходим для GPS-трекинга и координации команды. Вы можете включить его позже в настройках.",
|
"locationPermissionRequired": "Доступ к геолокации необходим для GPS-трекинга и координации команды. Вы можете включить его позже в настройках.",
|
||||||
"locationServicesDisabled": "Службы геолокации отключены. Пожалуйста, включите их в настройках.",
|
"locationServicesDisabled": "Службы геолокации отключены. Пожалуйста, включите их в настройках.",
|
||||||
"failedToGetGpsLocation": "Не удалось получить GPS-координаты",
|
"failedToGetGpsLocation": "Не удалось получить GPS-координаты",
|
||||||
"advertisedAtLocation": "Транслируется в {latitude}, {longitude}",
|
|
||||||
"failedToAdvertise": "Ошибка трансляции: {error}",
|
"failedToAdvertise": "Ошибка трансляции: {error}",
|
||||||
"reconnecting": "Переподключение... ({attempt}/{max})",
|
|
||||||
"cancelReconnection": "Отменить переподключение",
|
"cancelReconnection": "Отменить переподключение",
|
||||||
"mapManagement": "Управление картой",
|
|
||||||
"general": "Основные",
|
"general": "Основные",
|
||||||
"theme": "Тема",
|
"theme": "Тема",
|
||||||
"chooseTheme": "Выбрать тему",
|
"chooseTheme": "Выбрать тему",
|
||||||
@@ -38,33 +33,10 @@
|
|||||||
"followSystemTheme": "Следовать системной теме",
|
"followSystemTheme": "Следовать системной теме",
|
||||||
"showRxTxIndicators": "Показывать индикаторы RX/TX",
|
"showRxTxIndicators": "Показывать индикаторы RX/TX",
|
||||||
"displayPacketActivity": "Отображать активность пакетов в верхней панели",
|
"displayPacketActivity": "Отображать активность пакетов в верхней панели",
|
||||||
"simpleMode": "Простой режим",
|
|
||||||
"simpleModeDescription": "Скрыть второстепенную информацию в сообщениях и контактах",
|
|
||||||
"disableMap": "Отключить карту",
|
"disableMap": "Отключить карту",
|
||||||
"disableMapDescription": "Скрыть вкладку карты для экономии заряда батареи",
|
"disableMapDescription": "Скрыть вкладку карты для экономии заряда батареи",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
"chooseLanguage": "Выбрать язык",
|
"chooseLanguage": "Выбрать язык",
|
||||||
"english": "Английский",
|
|
||||||
"slovenian": "Словенский",
|
|
||||||
"croatian": "Хорватский",
|
|
||||||
"german": "Немецкий",
|
|
||||||
"spanish": "Испанский",
|
|
||||||
"french": "Французский",
|
|
||||||
"italian": "Итальянский",
|
|
||||||
"russian": "Русский",
|
|
||||||
"locationBroadcasting": "Трансляция местоположения",
|
|
||||||
"autoLocationTracking": "Авто-отслеживание геолокации",
|
|
||||||
"automaticallyBroadcastPosition": "Автоматически транслировать обновления позиции",
|
|
||||||
"configureTracking": "Настроить отслеживание",
|
|
||||||
"distanceAndTimeThresholds": "Пороги расстояния и времени",
|
|
||||||
"locationTrackingConfiguration": "Настройка отслеживания геолокации",
|
|
||||||
"configureWhenLocationBroadcasts": "Настройте условия отправки обновлений геолокации в mesh-сеть",
|
|
||||||
"minimumDistance": "Минимальное расстояние",
|
|
||||||
"broadcastAfterMoving": "Транслировать только после перемещения на {distance} м",
|
|
||||||
"maximumDistance": "Максимальное расстояние",
|
|
||||||
"alwaysBroadcastAfterMoving": "Всегда транслировать после перемещения на {distance} м",
|
|
||||||
"minimumTimeInterval": "Минимальный интервал времени",
|
|
||||||
"alwaysBroadcastEvery": "Всегда транслировать каждые {duration}",
|
|
||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"cancel": "Отмена",
|
"cancel": "Отмена",
|
||||||
"close": "Закрыть",
|
"close": "Закрыть",
|
||||||
@@ -76,8 +48,6 @@
|
|||||||
"technologiesUsed": "Используемые технологии:",
|
"technologiesUsed": "Используемые технологии:",
|
||||||
"technologiesList": "• Flutter для кросс-платформенной разработки\n• BLE (Bluetooth Low Energy) для mesh-сети\n• OpenStreetMap для карт\n• Provider для управления состоянием\n• SharedPreferences для локального хранилища",
|
"technologiesList": "• Flutter для кросс-платформенной разработки\n• BLE (Bluetooth Low Energy) для mesh-сети\n• OpenStreetMap для карт\n• Provider для управления состоянием\n• SharedPreferences для локального хранилища",
|
||||||
"moreInfo": "Подробнее",
|
"moreInfo": "Подробнее",
|
||||||
"learnMoreAbout": "Узнать больше о MeshCore SAR",
|
|
||||||
"developer": "Разработчик",
|
|
||||||
"packageName": "Имя пакета",
|
"packageName": "Имя пакета",
|
||||||
"sampleData": "Тестовые данные",
|
"sampleData": "Тестовые данные",
|
||||||
"sampleDataDescription": "Загрузить или очистить тестовые контакты, сообщения каналов и маркеры SAR",
|
"sampleDataDescription": "Загрузить или очистить тестовые контакты, сообщения каналов и маркеры SAR",
|
||||||
@@ -98,7 +68,6 @@
|
|||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"viewOnMap": "Показать на карте",
|
"viewOnMap": "Показать на карте",
|
||||||
"refresh": "Обновить",
|
"refresh": "Обновить",
|
||||||
"sendDirectMessage": "Отправить",
|
|
||||||
"resetPath": "Сбросить маршрут (перепроложить)",
|
"resetPath": "Сбросить маршрут (перепроложить)",
|
||||||
"publicKeyCopied": "Публичный ключ скопирован в буфер обмена",
|
"publicKeyCopied": "Публичный ключ скопирован в буфер обмена",
|
||||||
"copiedToClipboard": "{label} скопировано в буфер обмена",
|
"copiedToClipboard": "{label} скопировано в буфер обмена",
|
||||||
@@ -132,8 +101,6 @@
|
|||||||
"deleteAllDrawingsConfirm": "Удалить все {count} рисун{plural} с карты?",
|
"deleteAllDrawingsConfirm": "Удалить все {count} рисун{plural} с карты?",
|
||||||
"drawing": "Рисунок",
|
"drawing": "Рисунок",
|
||||||
"shareDrawingsCount": "Поделиться {count} рисун{plural}",
|
"shareDrawingsCount": "Поделиться {count} рисун{plural}",
|
||||||
"sentDrawingsToRoom": "Отправлено {count} рисун{plural} в {roomName}",
|
|
||||||
"sharedDrawingsToRoom": "Передано {success}/{total} рисун{plural} в {roomName}",
|
|
||||||
"showReceivedDrawings": "Показать полученные рисунки",
|
"showReceivedDrawings": "Показать полученные рисунки",
|
||||||
"showingAllDrawings": "Показаны все рисунки",
|
"showingAllDrawings": "Показаны все рисунки",
|
||||||
"showingOnlyYourDrawings": "Показаны только ваши рисунки",
|
"showingOnlyYourDrawings": "Показаны только ваши рисунки",
|
||||||
@@ -141,17 +108,10 @@
|
|||||||
"showingSarMarkers": "Маркеры SAR отображаются",
|
"showingSarMarkers": "Маркеры SAR отображаются",
|
||||||
"hidingSarMarkers": "Маркеры SAR скрыты",
|
"hidingSarMarkers": "Маркеры SAR скрыты",
|
||||||
"clearAll": "Очистить всё",
|
"clearAll": "Очистить всё",
|
||||||
"noLocalDrawings": "Нет локальных рисунков для отправки",
|
|
||||||
"publicChannel": "Публичный канал",
|
"publicChannel": "Публичный канал",
|
||||||
"broadcastToAll": "Трансляция всем ближайшим узлам (временно)",
|
"broadcastToAll": "Трансляция всем ближайшим узлам (временно)",
|
||||||
"storedPermanently": "Сохранено постоянно в комнате",
|
"storedPermanently": "Сохранено постоянно в комнате",
|
||||||
"drawingsSentToPublicChannel": "Отправлено {count} рисун{plural} в публичный канал",
|
|
||||||
"drawingsSharedToPublicChannel": "Передано {success}/{total} рисунков в публичный канал",
|
|
||||||
"notConnectedToDevice": "Устройство не подключено",
|
"notConnectedToDevice": "Устройство не подключено",
|
||||||
"directMessage": "Личное сообщение",
|
|
||||||
"directMessageSentTo": "Личное сообщение отправлено {contactName}",
|
|
||||||
"failedToSend": "Не удалось отправить: {error}",
|
|
||||||
"directMessageInfo": "Это сообщение будет отправлено напрямую {contactName}. Оно также появится в общей ленте сообщений.",
|
|
||||||
"typeYourMessage": "Введите сообщение...",
|
"typeYourMessage": "Введите сообщение...",
|
||||||
"quickLocationMarker": "Быстрый маркер местоположения",
|
"quickLocationMarker": "Быстрый маркер местоположения",
|
||||||
"markerType": "Тип маркера",
|
"markerType": "Тип маркера",
|
||||||
@@ -161,7 +121,6 @@
|
|||||||
"ephemeralBroadcastInfo": "Временно: передаётся по эфиру. Не сохраняется — узлы должны быть онлайн.",
|
"ephemeralBroadcastInfo": "Временно: передаётся по эфиру. Не сохраняется — узлы должны быть онлайн.",
|
||||||
"persistentRoomInfo": "Постоянно: хранится неизменно в комнате. Синхронизируется автоматически и доступно офлайн.",
|
"persistentRoomInfo": "Постоянно: хранится неизменно в комнате. Синхронизируется автоматически и доступно офлайн.",
|
||||||
"location": "Местоположение",
|
"location": "Местоположение",
|
||||||
"myLocation": "Моё местоположение",
|
|
||||||
"fromMap": "С карты",
|
"fromMap": "С карты",
|
||||||
"gettingLocation": "Получение местоположения...",
|
"gettingLocation": "Получение местоположения...",
|
||||||
"locationError": "Ошибка геолокации",
|
"locationError": "Ошибка геолокации",
|
||||||
@@ -180,19 +139,12 @@
|
|||||||
"failedToAddRoom": "Не удалось добавить комнату на устройство: {error}\n\nВозможно, комната ещё не объявила себя.\nПопробуйте подождать, пока комната не выйдет на связь.",
|
"failedToAddRoom": "Не удалось добавить комнату на устройство: {error}\n\nВозможно, комната ещё не объявила себя.\nПопробуйте подождать, пока комната не выйдет на связь.",
|
||||||
"direct": "Напрямую",
|
"direct": "Напрямую",
|
||||||
"flood": "Широковещательно",
|
"flood": "Широковещательно",
|
||||||
"admin": "Администратор",
|
|
||||||
"loggedIn": "Вход выполнен",
|
"loggedIn": "Вход выполнен",
|
||||||
"noGpsData": "Нет данных GPS",
|
"noGpsData": "Нет данных GPS",
|
||||||
"distance": "Расстояние",
|
"distance": "Расстояние",
|
||||||
"pingingDirect": "Пинг {name} (напрямую по маршруту)...",
|
|
||||||
"pingingFlood": "Пинг {name} (широковещательно — нет маршрута)...",
|
|
||||||
"directPingTimeout": "Таймаут прямого пинга — повтор {name} широковещательно...",
|
"directPingTimeout": "Таймаут прямого пинга — повтор {name} широковещательно...",
|
||||||
"pingSuccessful": "Пинг успешен: {name}{fallback}",
|
|
||||||
"viaFloodingFallback": " (через широковещательный резерв)",
|
|
||||||
"pingFailed": "Пинг не удался: {name} — ответ не получен",
|
"pingFailed": "Пинг не удался: {name} — ответ не получен",
|
||||||
"deleteContactConfirmation": "Вы уверены, что хотите удалить \"{name}\"?\n\nЭто удалит контакт как из приложения, так и с сопряжённого радиоустройства.",
|
"deleteContactConfirmation": "Вы уверены, что хотите удалить \"{name}\"?\n\nЭто удалит контакт как из приложения, так и с сопряжённого радиоустройства.",
|
||||||
"removingContact": "Удаление {name}...",
|
|
||||||
"contactRemoved": "Контакт \"{name}\" удалён",
|
|
||||||
"failedToRemoveContact": "Не удалось удалить контакт: {error}",
|
"failedToRemoveContact": "Не удалось удалить контакт: {error}",
|
||||||
"type": "Тип",
|
"type": "Тип",
|
||||||
"publicKey": "Публичный ключ",
|
"publicKey": "Публичный ключ",
|
||||||
@@ -207,7 +159,6 @@
|
|||||||
"passwordSaved": "Пароль сохранён",
|
"passwordSaved": "Пароль сохранён",
|
||||||
"locationColon": "Местоположение:",
|
"locationColon": "Местоположение:",
|
||||||
"telemetry": "Телеметрия",
|
"telemetry": "Телеметрия",
|
||||||
"requestingTelemetry": "Запрос телеметрии от {name}...",
|
|
||||||
"voltage": "Напряжение",
|
"voltage": "Напряжение",
|
||||||
"battery": "Батарея",
|
"battery": "Батарея",
|
||||||
"temperature": "Температура",
|
"temperature": "Температура",
|
||||||
@@ -231,7 +182,6 @@
|
|||||||
"fire": "Пожар",
|
"fire": "Пожар",
|
||||||
"stagingArea": "Место сбора",
|
"stagingArea": "Место сбора",
|
||||||
"showAll": "Показать все",
|
"showAll": "Показать все",
|
||||||
"nearbyContacts": "Ближайшие контакты",
|
|
||||||
"locationUnavailable": "Местоположение недоступно",
|
"locationUnavailable": "Местоположение недоступно",
|
||||||
"ahead": "впереди",
|
"ahead": "впереди",
|
||||||
"degreesRight": "{degrees}° вправо",
|
"degreesRight": "{degrees}° вправо",
|
||||||
@@ -243,83 +193,21 @@
|
|||||||
"repeaters": "Ретрансляторы",
|
"repeaters": "Ретрансляторы",
|
||||||
"rooms": "Комнаты",
|
"rooms": "Комнаты",
|
||||||
"channels": "Каналы",
|
"channels": "Каналы",
|
||||||
"cacheStatistics": "Статистика кэша",
|
|
||||||
"totalTiles": "Всего тайлов",
|
|
||||||
"cacheSize": "Размер кэша",
|
|
||||||
"storeName": "Имя хранилища",
|
|
||||||
"noCacheStatistics": "Статистика кэша недоступна",
|
|
||||||
"downloadRegion": "Скачать регион",
|
|
||||||
"mapLayer": "Слой карты",
|
|
||||||
"regionBounds": "Границы региона",
|
|
||||||
"north": "Север",
|
|
||||||
"south": "Юг",
|
|
||||||
"east": "Восток",
|
|
||||||
"west": "Запад",
|
|
||||||
"zoomLevels": "Уровни масштаба",
|
|
||||||
"minZoom": "Мин: {zoom}",
|
|
||||||
"maxZoom": "Макс: {zoom}",
|
|
||||||
"downloadingDots": "Загрузка...",
|
|
||||||
"cancelDownload": "Отменить загрузку",
|
|
||||||
"downloadRegionButton": "Скачать регион",
|
|
||||||
"downloadNote": "Внимание: большие регионы или высокие уровни масштаба могут потребовать значительного времени и места.",
|
|
||||||
"cacheManagement": "Управление кэшем",
|
|
||||||
"clearAllMaps": "Очистить все карты",
|
|
||||||
"clearMapsConfirmTitle": "Очистить все карты",
|
|
||||||
"clearMapsConfirmMessage": "Вы уверены, что хотите удалить все загруженные карты? Это действие нельзя отменить.",
|
|
||||||
"mapDownloadCompleted": "Загрузка карты завершена!",
|
|
||||||
"cacheClearedSuccessfully": "Кэш успешно очищен!",
|
|
||||||
"downloadCancelled": "Загрузка отменена",
|
|
||||||
"startingDownload": "Начало загрузки...",
|
|
||||||
"downloadingMapTiles": "Загрузка тайлов карты...",
|
|
||||||
"downloadCompletedSuccessfully": "Загрузка успешно завершена!",
|
|
||||||
"cancellingDownload": "Отмена загрузки...",
|
|
||||||
"errorLoadingStats": "Ошибка загрузки статистики: {error}",
|
|
||||||
"downloadFailed": "Ошибка загрузки: {error}",
|
|
||||||
"cancelFailed": "Ошибка отмены: {error}",
|
|
||||||
"clearCacheFailed": "Ошибка очистки кэша: {error}",
|
|
||||||
"minZoomError": "Мин. масштаб: {error}",
|
|
||||||
"maxZoomError": "Макс. масштаб: {error}",
|
|
||||||
"minZoomGreaterThanMax": "Минимальный масштаб должен быть меньше или равен максимальному",
|
|
||||||
"selectMapLayer": "Выбрать слой карты",
|
"selectMapLayer": "Выбрать слой карты",
|
||||||
"mapOptions": "Параметры карты",
|
|
||||||
"showLegend": "Показать легенду",
|
|
||||||
"displayMarkerTypeCounts": "Отображать счётчики типов маркеров",
|
|
||||||
"rotateMapWithHeading": "Поворачивать карту по курсу",
|
|
||||||
"mapFollowsDirection": "Карта следует вашему направлению движения",
|
|
||||||
"resetMapRotation": "Сбросить поворот",
|
|
||||||
"resetMapRotationTooltip": "Вернуть карту на север",
|
|
||||||
"showMapDebugInfo": "Показать отладочную информацию карты",
|
|
||||||
"displayZoomLevelBounds": "Отображать уровень масштаба и границы",
|
|
||||||
"fullscreenMode": "Полноэкранный режим",
|
|
||||||
"hideUiFullMapView": "Скрыть все элементы интерфейса для полного вида карты",
|
|
||||||
"openStreetMap": "OpenStreetMap",
|
"openStreetMap": "OpenStreetMap",
|
||||||
"openTopoMap": "OpenTopoMap",
|
"openTopoMap": "OpenTopoMap",
|
||||||
"esriSatellite": "ESRI Спутник",
|
"esriSatellite": "ESRI Спутник",
|
||||||
"googleHybrid": "Google Гибрид",
|
"googleHybrid": "Google Гибрид",
|
||||||
"googleRoadmap": "Google Дороги",
|
"googleRoadmap": "Google Дороги",
|
||||||
"googleTerrain": "Google Рельеф",
|
"googleTerrain": "Google Рельеф",
|
||||||
"downloadVisibleArea": "Скачать видимую область",
|
|
||||||
"initializingMap": "Инициализация карты...",
|
|
||||||
"dragToPosition": "Перетащите для позиционирования",
|
"dragToPosition": "Перетащите для позиционирования",
|
||||||
"createSarMarker": "Создать маркер SAR",
|
"createSarMarker": "Создать маркер SAR",
|
||||||
"compass": "Компас",
|
"compass": "Компас",
|
||||||
"navigationAndContacts": "Навигация и контакты",
|
"navigationAndContacts": "Навигация и контакты",
|
||||||
"sarAlert": "ТРЕВОГА SAR",
|
"sarAlert": "ТРЕВОГА SAR",
|
||||||
"messageSentToPublicChannel": "Сообщение отправлено в публичный канал",
|
|
||||||
"pleaseSelectRoomToSendSar": "Пожалуйста, выберите комнату для отправки маркера SAR",
|
|
||||||
"failedToSendSarMarker": "Не удалось отправить маркер SAR: {error}",
|
|
||||||
"sarMarkerSentTo": "Маркер SAR отправлен в {roomName}",
|
|
||||||
"notConnectedCannotSync": "Нет подключения — синхронизация невозможна",
|
|
||||||
"syncedMessageCount": "Синхронизировано {count} сообщение(й)",
|
|
||||||
"noNewMessages": "Новых сообщений нет",
|
|
||||||
"syncFailed": "Синхронизация не удалась: {error}",
|
|
||||||
"failedToResendMessage": "Не удалось повторно отправить сообщение",
|
|
||||||
"retryingMessage": "Повторная отправка сообщения...",
|
|
||||||
"retryFailed": "Повтор не удался: {error}",
|
|
||||||
"textCopiedToClipboard": "Текст скопирован в буфер обмена",
|
"textCopiedToClipboard": "Текст скопирован в буфер обмена",
|
||||||
"cannotReplySenderMissing": "Не удаётся ответить: нет информации об отправителе",
|
"cannotReplySenderMissing": "Не удаётся ответить: нет информации об отправителе",
|
||||||
"cannotReplyContactNotFound": "Не удаётся ответить: контакт не найден",
|
"cannotReplyContactNotFound": "Не удаётся ответить: контакт не найден",
|
||||||
"messageDeleted": "Сообщение удалено",
|
|
||||||
"copyText": "Копировать текст",
|
"copyText": "Копировать текст",
|
||||||
"saveAsTemplate": "Сохранить как шаблон",
|
"saveAsTemplate": "Сохранить как шаблон",
|
||||||
"templateSaved": "Шаблон успешно сохранён",
|
"templateSaved": "Шаблон успешно сохранён",
|
||||||
@@ -329,8 +217,6 @@
|
|||||||
"shareLocation": "Поделиться местоположением",
|
"shareLocation": "Поделиться местоположением",
|
||||||
"shareLocationText": "{markerInfo}\n\nКоординаты: {lat}, {lon}\n\nGoogle Maps: {url}",
|
"shareLocationText": "{markerInfo}\n\nКоординаты: {lat}, {lon}\n\nGoogle Maps: {url}",
|
||||||
"sarLocationShare": "Местоположение SAR",
|
"sarLocationShare": "Местоположение SAR",
|
||||||
"locationShared": "Местоположение передано",
|
|
||||||
"refreshedContacts": "Контакты обновлены",
|
|
||||||
"justNow": "Только что",
|
"justNow": "Только что",
|
||||||
"minutesAgo": "{minutes}м назад",
|
"minutesAgo": "{minutes}м назад",
|
||||||
"hoursAgo": "{hours}ч назад",
|
"hoursAgo": "{hours}ч назад",
|
||||||
@@ -362,39 +248,10 @@
|
|||||||
"txPowerDbm": "Мощность TX (дБм)",
|
"txPowerDbm": "Мощность TX (дБм)",
|
||||||
"maxPowerDbm": "Макс: {power} дБм",
|
"maxPowerDbm": "Макс: {power} дБм",
|
||||||
"you": "Вы",
|
"you": "Вы",
|
||||||
"offlineVectorMaps": "Офлайн-векторные карты",
|
|
||||||
"offlineVectorMapsDescription": "Импортируйте офлайн-векторные тайлы карт (формат MBTiles) для использования без интернета",
|
|
||||||
"importMbtiles": "Импортировать файл MBTiles",
|
|
||||||
"importMbtilesNote": "Поддерживаются файлы MBTiles с векторными тайлами (формат PBF/MVT). Отлично подходят выгрузки Geofabrik!",
|
|
||||||
"noMbtilesFiles": "Офлайн-векторные карты не найдены",
|
|
||||||
"mbtilesImportedSuccessfully": "Файл MBTiles успешно импортирован",
|
|
||||||
"failedToImportMbtiles": "Не удалось импортировать файл MBTiles",
|
|
||||||
"deleteMbtilesConfirmTitle": "Удалить офлайн-карту",
|
|
||||||
"deleteMbtilesConfirmMessage": "Вы уверены, что хотите удалить \"{name}\"? Офлайн-карта будет удалена безвозвратно.",
|
|
||||||
"mbtilesDeletedSuccessfully": "Офлайн-карта успешно удалена",
|
|
||||||
"failedToDeleteMbtiles": "Не удалось удалить офлайн-карту",
|
|
||||||
"importExportCachedTiles": "Импорт/Экспорт кэшированных тайлов",
|
|
||||||
"importExportDescription": "Резервное копирование, обмен и восстановление загруженных тайлов карт между устройствами",
|
|
||||||
"exportTilesToFile": "Экспортировать тайлы в файл",
|
|
||||||
"importTilesFromFile": "Импортировать тайлы из файла",
|
|
||||||
"selectExportLocation": "Выберите место для экспорта",
|
|
||||||
"selectImportFile": "Выберите архив тайлов",
|
|
||||||
"exportingTiles": "Экспорт тайлов...",
|
|
||||||
"importingTiles": "Импорт тайлов...",
|
|
||||||
"exportSuccess": "Экспортировано {count} тайлов",
|
|
||||||
"importSuccess": "Импортировано {count} хранилищ",
|
|
||||||
"exportFailed": "Ошибка экспорта: {error}",
|
"exportFailed": "Ошибка экспорта: {error}",
|
||||||
"importFailed": "Ошибка импорта: {error}",
|
"importFailed": "Ошибка импорта: {error}",
|
||||||
"exportNote": "Создаёт сжатый архив (.fmtc), которым можно поделиться и импортировать на других устройствах.",
|
|
||||||
"importNote": "Импортирует тайлы карт из ранее экспортированного архива. Тайлы будут объединены с существующим кэшем.",
|
|
||||||
"noTilesToExport": "Нет доступных тайлов для экспорта",
|
|
||||||
"archiveContainsStores": "Архив содержит {count} хранилищ",
|
|
||||||
"vectorTiles": "Векторные тайлы",
|
|
||||||
"schema": "Схема",
|
|
||||||
"unknown": "Неизвестно",
|
"unknown": "Неизвестно",
|
||||||
"bounds": "Границы",
|
|
||||||
"onlineLayers": "Онлайн-слои",
|
"onlineLayers": "Онлайн-слои",
|
||||||
"offlineLayers": "Офлайн-слои",
|
|
||||||
"locationTrail": "Трек местоположения",
|
"locationTrail": "Трек местоположения",
|
||||||
"showTrailOnMap": "Показать трек на карте",
|
"showTrailOnMap": "Показать трек на карте",
|
||||||
"trailVisible": "Трек отображается на карте",
|
"trailVisible": "Трек отображается на карте",
|
||||||
@@ -407,17 +264,6 @@
|
|||||||
"noTrailRecorded": "Трек ещё не записан",
|
"noTrailRecorded": "Трек ещё не записан",
|
||||||
"startTrackingToRecord": "Запустите отслеживание геолокации для записи трека",
|
"startTrackingToRecord": "Запустите отслеживание геолокации для записи трека",
|
||||||
"trailControls": "Управление треком",
|
"trailControls": "Управление треком",
|
||||||
"exportTrailToGpx": "Экспортировать трек в GPX",
|
|
||||||
"importTrailFromGpx": "Импортировать трек из GPX",
|
|
||||||
"trailExportedSuccessfully": "Трек успешно экспортирован!",
|
|
||||||
"failedToExportTrail": "Не удалось экспортировать трек",
|
|
||||||
"failedToImportTrail": "Не удалось импортировать трек: {error}",
|
|
||||||
"importTrail": "Импортировать трек",
|
|
||||||
"importTrailQuestion": "Импортировать трек из {pointCount} точек?\n\nВы можете заменить текущий трек или просмотреть его рядом.",
|
|
||||||
"viewAlongside": "Просмотреть рядом",
|
|
||||||
"replaceCurrent": "Заменить текущий",
|
|
||||||
"trailImported": "Трек импортирован! ({pointCount} точек)",
|
|
||||||
"trailReplaced": "Трек заменён! ({pointCount} точек)",
|
|
||||||
"contactTrails": "Треки контактов",
|
"contactTrails": "Треки контактов",
|
||||||
"showAllContactTrails": "Показать все треки контактов",
|
"showAllContactTrails": "Показать все треки контактов",
|
||||||
"noContactsWithLocationHistory": "Нет контактов с историей местоположения",
|
"noContactsWithLocationHistory": "Нет контактов с историей местоположения",
|
||||||
@@ -496,10 +342,8 @@
|
|||||||
"searchRecipients": "Поиск получателей...",
|
"searchRecipients": "Поиск получателей...",
|
||||||
"noContactsFound": "Контакты не найдены",
|
"noContactsFound": "Контакты не найдены",
|
||||||
"noRoomsFound": "Комнаты не найдены",
|
"noRoomsFound": "Комнаты не найдены",
|
||||||
"noContactsOrRoomsAvailable": "Нет доступных контактов или комнат",
|
|
||||||
"noRecipientsAvailable": "Нет доступных получателей",
|
"noRecipientsAvailable": "Нет доступных получателей",
|
||||||
"noChannelsFound": "Каналы не найдены",
|
"noChannelsFound": "Каналы не найдены",
|
||||||
"messagesWillBeSentToPublicChannel": "Сообщения будут отправлены в публичный канал",
|
|
||||||
"newMessage": "Новое сообщение",
|
"newMessage": "Новое сообщение",
|
||||||
"channel": "Канал",
|
"channel": "Канал",
|
||||||
"samplePoliceLead": "Руководитель группы полиции",
|
"samplePoliceLead": "Руководитель группы полиции",
|
||||||
@@ -557,18 +401,11 @@
|
|||||||
"hideFromMap": "Скрыть с карты",
|
"hideFromMap": "Скрыть с карты",
|
||||||
"lineDrawing": "Линия",
|
"lineDrawing": "Линия",
|
||||||
"rectangleDrawing": "Прямоугольник",
|
"rectangleDrawing": "Прямоугольник",
|
||||||
"coordinatesCopiedToClipboard": "Координаты скопированы в буфер обмена",
|
|
||||||
"manualCoordinates": "Ввод координат вручную",
|
"manualCoordinates": "Ввод координат вручную",
|
||||||
"enterCoordinatesManually": "Ввести координаты вручную",
|
"enterCoordinatesManually": "Ввести координаты вручную",
|
||||||
"latitudeLabel": "Широта",
|
"latitudeLabel": "Широта",
|
||||||
"longitudeLabel": "Долгота",
|
"longitudeLabel": "Долгота",
|
||||||
"invalidLatitude": "Неверная широта (от -90 до 90)",
|
|
||||||
"invalidLongitude": "Неверная долгота (от -180 до 180)",
|
|
||||||
"exampleCoordinates": "Пример: 55.7558, 37.6173",
|
"exampleCoordinates": "Пример: 55.7558, 37.6173",
|
||||||
"drawingShared": "Рисунок на карте",
|
|
||||||
"drawingHidden": "Рисунок скрыт с карты",
|
|
||||||
"alreadyShared": "{count, plural, =1{1 уже передан} other{{count} уже передано}}",
|
|
||||||
"newDrawingsShared": "Передано {count} новых рисунк{plural}",
|
|
||||||
"shareDrawing": "Поделиться рисунком",
|
"shareDrawing": "Поделиться рисунком",
|
||||||
"shareWithAllNearbyDevices": "Поделиться со всеми ближайшими устройствами",
|
"shareWithAllNearbyDevices": "Поделиться со всеми ближайшими устройствами",
|
||||||
"shareToRoom": "Отправить в комнату",
|
"shareToRoom": "Отправить в комнату",
|
||||||
@@ -586,8 +423,6 @@
|
|||||||
"updateLater": "Позже",
|
"updateLater": "Позже",
|
||||||
"cadastralParcels": "Кадастровые участки",
|
"cadastralParcels": "Кадастровые участки",
|
||||||
"forestRoads": "Лесные дороги",
|
"forestRoads": "Лесные дороги",
|
||||||
"showCadastralParcels": "Показать кадастровые участки",
|
|
||||||
"showForestRoads": "Показать лесные дороги",
|
|
||||||
"wmsOverlays": "WMS-наложения",
|
"wmsOverlays": "WMS-наложения",
|
||||||
"hikingTrails": "Туристические маршруты",
|
"hikingTrails": "Туристические маршруты",
|
||||||
"mainRoads": "Главные дороги",
|
"mainRoads": "Главные дороги",
|
||||||
@@ -619,7 +454,6 @@
|
|||||||
"deleteChannelConfirmation": "Вы уверены, что хотите удалить канал \"{channelName}\"? Это действие нельзя отменить.",
|
"deleteChannelConfirmation": "Вы уверены, что хотите удалить канал \"{channelName}\"? Это действие нельзя отменить.",
|
||||||
"channelDeletedSuccessfully": "Канал успешно удалён",
|
"channelDeletedSuccessfully": "Канал успешно удалён",
|
||||||
"channelDeletionFailed": "Не удалось удалить канал: {error}",
|
"channelDeletionFailed": "Не удалось удалить канал: {error}",
|
||||||
"allChannelSlotsInUse": "Все слоты каналов заняты (максимум 39 пользовательских каналов)",
|
|
||||||
"createChannel": "Создать канал",
|
"createChannel": "Создать канал",
|
||||||
"wizardBack": "Назад",
|
"wizardBack": "Назад",
|
||||||
"wizardSkip": "Пропустить",
|
"wizardSkip": "Пропустить",
|
||||||
@@ -632,10 +466,6 @@
|
|||||||
"wizardConnectingFeature1": "Поиск ближайших устройств MeshCore",
|
"wizardConnectingFeature1": "Поиск ближайших устройств MeshCore",
|
||||||
"wizardConnectingFeature2": "Сопряжение с радиоустройством по Bluetooth",
|
"wizardConnectingFeature2": "Сопряжение с радиоустройством по Bluetooth",
|
||||||
"wizardConnectingFeature3": "Работает полностью офлайн — интернет не нужен",
|
"wizardConnectingFeature3": "Работает полностью офлайн — интернет не нужен",
|
||||||
"wizardSimpleModeTitle": "Простой режим",
|
|
||||||
"wizardSimpleModeDescription": "Впервые работаете с mesh-сетью? Включите простой режим для упрощённого интерфейса с основными функциями.",
|
|
||||||
"wizardSimpleModeFeature1": "Удобный для новичков интерфейс с основными функциями",
|
|
||||||
"wizardSimpleModeFeature2": "Переключиться в расширенный режим можно в любое время в настройках",
|
|
||||||
"wizardChannelTitle": "Каналы",
|
"wizardChannelTitle": "Каналы",
|
||||||
"wizardChannelDescription": "Транслируйте сообщения всем участникам канала — идеально для общих объявлений и координации команды.",
|
"wizardChannelDescription": "Транслируйте сообщения всем участникам канала — идеально для общих объявлений и координации команды.",
|
||||||
"wizardChannelFeature1": "Публичный канал для общей связи команды",
|
"wizardChannelFeature1": "Публичный канал для общей связи команды",
|
||||||
|
|||||||
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
1003
lib/l10n/app_zh.arb
1003
lib/l10n/app_zh.arb
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,7 @@
|
|||||||
import 'dart:io' show Platform;
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart'
|
||||||
|
show TargetPlatform, defaultTargetPlatform, kIsWeb;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -19,6 +22,7 @@ import 'services/voice_codec_service.dart';
|
|||||||
import 'services/voice_player_service.dart';
|
import 'services/voice_player_service.dart';
|
||||||
import 'services/notification_service.dart';
|
import 'services/notification_service.dart';
|
||||||
import 'services/locale_preferences.dart';
|
import 'services/locale_preferences.dart';
|
||||||
|
import 'services/mesh_map_nodes_service.dart';
|
||||||
import 'services/update_checker_service.dart';
|
import 'services/update_checker_service.dart';
|
||||||
import 'services/wizard_preferences.dart';
|
import 'services/wizard_preferences.dart';
|
||||||
import 'screens/home_screen.dart';
|
import 'screens/home_screen.dart';
|
||||||
@@ -70,6 +74,9 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
|||||||
// Shows notification if update is available
|
// Shows notification if update is available
|
||||||
_checkForUpdates();
|
_checkForUpdates();
|
||||||
|
|
||||||
|
// Refresh remote mesh node cache in background when the app starts.
|
||||||
|
unawaited(MeshMapNodesService.syncInBackgroundIfStale());
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_wizardCompleted = wizardCompleted;
|
_wizardCompleted = wizardCompleted;
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
@@ -158,7 +165,7 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
|||||||
/// Shows notification if update is available
|
/// Shows notification if update is available
|
||||||
Future<void> _checkForUpdates() async {
|
Future<void> _checkForUpdates() async {
|
||||||
// Only check for updates on Android
|
// Only check for updates on Android
|
||||||
if (!Platform.isAndroid) {
|
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
|
||||||
debugPrint('[UpdateChecker] Skipping update check (not Android)');
|
debugPrint('[UpdateChecker] Skipping update check (not Android)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -324,7 +331,7 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
|||||||
|
|
||||||
// Wrap in SafeArea for Android only to fix navigation bar overlap (API ≥36)
|
// Wrap in SafeArea for Android only to fix navigation bar overlap (API ≥36)
|
||||||
// iOS doesn't need SafeArea wrapping (causes extra black space at bottom)
|
// iOS doesn't need SafeArea wrapping (causes extra black space at bottom)
|
||||||
if (Platform.isAndroid) {
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
left: false,
|
left: false,
|
||||||
right: false,
|
right: false,
|
||||||
|
|||||||
@@ -240,6 +240,5 @@ extension ContactLocalization on Contact {
|
|||||||
return '$routeHopCount hop${routeHopCount == 1 ? '' : 's'} via $routeHashSize-byte hashes';
|
return '$routeHopCount hop${routeHopCount == 1 ? '' : 's'} via $routeHashSize-byte hashes';
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get routeSupportsLegacyRawTransport =>
|
bool get routeSupportsLegacyRawTransport => routeHasPath;
|
||||||
routeHasPath && routeSignedPathLen >= 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
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/fragment_ack_wait_registry.dart';
|
||||||
import 'helpers/session_metadata_restore.dart';
|
import 'helpers/session_metadata_restore.dart';
|
||||||
import '../services/location_tracking_service.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/packet_capture_storage_service.dart';
|
||||||
|
import '../services/path_history_service.dart';
|
||||||
|
import '../services/route_hash_preferences.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
import '../models/message.dart';
|
import '../models/message.dart';
|
||||||
import '../models/ble_packet_log.dart';
|
import '../models/ble_packet_log.dart';
|
||||||
|
import '../models/path_selection.dart';
|
||||||
import '../models/message_reception_details.dart';
|
import '../models/message_reception_details.dart';
|
||||||
import '../utils/drawing_message_parser.dart';
|
import '../utils/drawing_message_parser.dart';
|
||||||
import '../utils/raw_route_probe.dart';
|
import '../utils/raw_route_probe.dart';
|
||||||
@@ -24,6 +29,32 @@ import '../utils/image_message_parser.dart';
|
|||||||
import '../utils/media_swarm_protocol.dart';
|
import '../utils/media_swarm_protocol.dart';
|
||||||
import '../utils/message_airtime_estimator.dart';
|
import '../utils/message_airtime_estimator.dart';
|
||||||
import '../utils/fast_gps_packet.dart';
|
import '../utils/fast_gps_packet.dart';
|
||||||
|
import '../utils/log_rx_route_decoder.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
|
/// Main App Provider - coordinates all other providers
|
||||||
class AppProvider with ChangeNotifier {
|
class AppProvider with ChangeNotifier {
|
||||||
@@ -60,8 +91,24 @@ class AppProvider with ChangeNotifier {
|
|||||||
bool get isVoiceCompressorEnabled => _isVoiceCompressorEnabled;
|
bool get isVoiceCompressorEnabled => _isVoiceCompressorEnabled;
|
||||||
bool _isVoiceLimiterEnabled = true;
|
bool _isVoiceLimiterEnabled = true;
|
||||||
bool get isVoiceLimiterEnabled => _isVoiceLimiterEnabled;
|
bool get isVoiceLimiterEnabled => _isVoiceLimiterEnabled;
|
||||||
|
double _messageFontScale = 1.0;
|
||||||
|
double get messageFontScale => _messageFontScale;
|
||||||
bool _autoAddDiscoveredContacts = false;
|
bool _autoAddDiscoveredContacts = false;
|
||||||
bool get autoAddDiscoveredContacts => _autoAddDiscoveredContacts;
|
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 _packetRetryDelay = Duration(milliseconds: 1200);
|
||||||
static const Duration _mediaSwarmResponseWindow = Duration(seconds: 10);
|
static const Duration _mediaSwarmResponseWindow = Duration(seconds: 10);
|
||||||
@@ -100,7 +147,10 @@ class AppProvider with ChangeNotifier {
|
|||||||
_loadVoiceBandPassFilterEnabled();
|
_loadVoiceBandPassFilterEnabled();
|
||||||
_loadVoiceCompressorEnabled();
|
_loadVoiceCompressorEnabled();
|
||||||
_loadVoiceLimiterEnabled();
|
_loadVoiceLimiterEnabled();
|
||||||
|
_loadMessageFontScale();
|
||||||
_loadAutoAddDiscoveredContacts();
|
_loadAutoAddDiscoveredContacts();
|
||||||
|
_loadMessagingRouteSettings();
|
||||||
|
unawaited(_pathHistoryService.initialize());
|
||||||
_startPacketCapturePersistence();
|
_startPacketCapturePersistence();
|
||||||
_syncDrawingsOnStartup(); // Sync drawings immediately after providers load
|
_syncDrawingsOnStartup(); // Sync drawings immediately after providers load
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
@@ -385,6 +435,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.
|
/// Load auto-add discovered contacts setting from shared preferences.
|
||||||
Future<void> _loadAutoAddDiscoveredContacts() async {
|
Future<void> _loadAutoAddDiscoveredContacts() async {
|
||||||
try {
|
try {
|
||||||
@@ -409,6 +483,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
|
/// Initialize location tracking service
|
||||||
Future<void> _initializeLocationTracking() async {
|
Future<void> _initializeLocationTracking() async {
|
||||||
try {
|
try {
|
||||||
@@ -488,6 +606,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
contact,
|
contact,
|
||||||
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||||
);
|
);
|
||||||
|
unawaited(_pathHistoryService.recordLearnedPath(contact));
|
||||||
|
|
||||||
// Broadcast to SSE clients if server is running
|
// Broadcast to SSE clients if server is running
|
||||||
connectionProvider.broadcastContactToSseClients(contact);
|
connectionProvider.broadcastContactToSseClients(contact);
|
||||||
@@ -500,6 +619,9 @@ class AppProvider with ChangeNotifier {
|
|||||||
contacts,
|
contacts,
|
||||||
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||||
);
|
);
|
||||||
|
for (final contact in contacts) {
|
||||||
|
unawaited(_pathHistoryService.recordLearnedPath(contact));
|
||||||
|
}
|
||||||
debugPrint('Received ${contacts.length} contacts');
|
debugPrint('Received ${contacts.length} contacts');
|
||||||
|
|
||||||
// Broadcast all contacts to SSE clients if server is running
|
// Broadcast all contacts to SSE clients if server is running
|
||||||
@@ -634,6 +756,19 @@ class AppProvider with ChangeNotifier {
|
|||||||
final receptionDetailsSnapshot = _buildReceptionDetailsSnapshot(
|
final receptionDetailsSnapshot = _buildReceptionDetailsSnapshot(
|
||||||
enrichedMessage,
|
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
|
// Check if message is a drawing broadcast
|
||||||
if (DrawingMessageParser.isDrawingMessage(enrichedMessage.text)) {
|
if (DrawingMessageParser.isDrawingMessage(enrichedMessage.text)) {
|
||||||
@@ -1117,6 +1252,14 @@ class AppProvider with ChangeNotifier {
|
|||||||
messagesProvider.handleMessageEcho(messageId, echoCount, snrRaw, rssiDbm);
|
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
|
// Wire up MessagesProvider's sendMessageCallback for retry logic
|
||||||
messagesProvider.sendMessageCallback =
|
messagesProvider.sendMessageCallback =
|
||||||
({
|
({
|
||||||
@@ -1134,32 +1277,310 @@ class AppProvider with ChangeNotifier {
|
|||||||
retryAttempt: retryAttempt,
|
retryAttempt: retryAttempt,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
messagesProvider.onFinalRouterFallbackCallback =
|
||||||
messagesProvider.onDirectPathFailedCallback =
|
({required messageId, required contact, required message}) async {
|
||||||
({required contact, required failureStreak}) async {
|
return _sendWithFinalNearestRouterFallback(
|
||||||
debugPrint(
|
messageId: messageId,
|
||||||
'🧭 [AppProvider] Clearing unhealthy path for ${contact.advName} after $failureStreak failed send chain(s)',
|
contact: contact,
|
||||||
|
message: message,
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
contactsProvider.markPathUnhealthy(contact.publicKey);
|
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) {
|
if (!connectionProvider.deviceInfo.isConnected) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (route == null) {
|
||||||
await connectionProvider.resetPath(contact.publicKey);
|
await connectionProvider.resetPath(contact.publicKey);
|
||||||
Future.delayed(const Duration(milliseconds: 150), () {
|
return;
|
||||||
if (connectionProvider.deviceInfo.isConnected) {
|
|
||||||
connectionProvider.getContact(contact.publicKey);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
} catch (e) {
|
await connectionProvider.setContactRoute(
|
||||||
debugPrint(
|
contact,
|
||||||
'⚠️ [AppProvider] Failed to reset path for ${contact.advName}: $e',
|
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.)
|
/// Initialize the app (load contacts, sync time, etc.)
|
||||||
@@ -1553,14 +1974,13 @@ class AppProvider with ChangeNotifier {
|
|||||||
if (requester == null ||
|
if (requester == null ||
|
||||||
!requester.routeHasPath ||
|
!requester.routeHasPath ||
|
||||||
requester.routeHopCount > _maxDirectPayloadHops ||
|
requester.routeHopCount > _maxDirectPayloadHops ||
|
||||||
!requester.routeSupportsLegacyRawTransport ||
|
|
||||||
requester.outPath.isEmpty) {
|
requester.outPath.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unawaited(
|
unawaited(
|
||||||
connectionProvider.sendRawVoicePacket(
|
connectionProvider.sendRawVoicePacket(
|
||||||
contactPath: requester.outPath,
|
contactPath: requester.outPath,
|
||||||
contactPathLen: requester.routeSignedPathLen,
|
contactPathLen: requester.routeEncodedPathLen,
|
||||||
payload: availability.encodeBinary(),
|
payload: availability.encodeBinary(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1630,7 +2050,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
for (final peer in peers) {
|
for (final peer in peers) {
|
||||||
await connectionProvider.sendRawVoicePacket(
|
await connectionProvider.sendRawVoicePacket(
|
||||||
contactPath: peer.outPath,
|
contactPath: peer.outPath,
|
||||||
contactPathLen: peer.routeSignedPathLen,
|
contactPathLen: peer.routeEncodedPathLen,
|
||||||
payload: request.encodeBinary(),
|
payload: request.encodeBinary(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1653,7 +2073,6 @@ class AppProvider with ChangeNotifier {
|
|||||||
if (responder == null ||
|
if (responder == null ||
|
||||||
!responder.routeHasPath ||
|
!responder.routeHasPath ||
|
||||||
responder.routeHopCount > _maxDirectPayloadHops ||
|
responder.routeHopCount > _maxDirectPayloadHops ||
|
||||||
!responder.routeSupportsLegacyRawTransport ||
|
|
||||||
responder.outPath.isEmpty) {
|
responder.outPath.isEmpty) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1745,7 +2164,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
|
|
||||||
await connectionProvider.sendRawVoicePacket(
|
await connectionProvider.sendRawVoicePacket(
|
||||||
contactPath: target.outPath,
|
contactPath: target.outPath,
|
||||||
contactPathLen: target.routeSignedPathLen,
|
contactPathLen: target.routeEncodedPathLen,
|
||||||
payload: payload,
|
payload: payload,
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
@@ -1986,9 +2405,6 @@ class AppProvider with ChangeNotifier {
|
|||||||
if (!target.routeHasPath || target.routeHopCount > _maxDirectPayloadHops) {
|
if (!target.routeHasPath || target.routeHopCount > _maxDirectPayloadHops) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!target.routeSupportsLegacyRawTransport) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (target.outPath.isEmpty) {
|
if (target.outPath.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2021,7 +2437,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
await connectionProvider.sendRawVoicePacket(
|
await connectionProvider.sendRawVoicePacket(
|
||||||
contactPath: target.outPath,
|
contactPath: target.outPath,
|
||||||
contactPathLen: target.routeSignedPathLen,
|
contactPathLen: target.routeEncodedPathLen,
|
||||||
payload: RawRouteProbeRequest(
|
payload: RawRouteProbeRequest(
|
||||||
nonce: nonce,
|
nonce: nonce,
|
||||||
requesterKey6: requesterKey6,
|
requesterKey6: requesterKey6,
|
||||||
@@ -2049,7 +2465,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
if (target.publicKeyHex.isNotEmpty) {
|
if (target.publicKeyHex.isNotEmpty) {
|
||||||
return 'pk:${target.publicKeyHex}';
|
return 'pk:${target.publicKeyHex}';
|
||||||
}
|
}
|
||||||
return 'name:${target.advName}:${target.routeSignedPathLen}:${target.outPath.map((b) => b.toRadixString(16).padLeft(2, '0')).join()}';
|
return 'name:${target.advName}:${target.routeEncodedPathLen}:${target.outPath.map((b) => b.toRadixString(16).padLeft(2, '0')).join()}';
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleRawRouteProbeRequest(RawRouteProbeRequest request) {
|
void _handleRawRouteProbeRequest(RawRouteProbeRequest request) {
|
||||||
@@ -2067,9 +2483,6 @@ class AppProvider with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!requester.routeSupportsLegacyRawTransport) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (requester.outPath.isEmpty) {
|
if (requester.outPath.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2079,7 +2492,7 @@ class AppProvider with ChangeNotifier {
|
|||||||
unawaited(
|
unawaited(
|
||||||
connectionProvider.sendRawVoicePacket(
|
connectionProvider.sendRawVoicePacket(
|
||||||
contactPath: requester.outPath,
|
contactPath: requester.outPath,
|
||||||
contactPathLen: requester.routeSignedPathLen,
|
contactPathLen: requester.routeEncodedPathLen,
|
||||||
payload: RawRouteProbeAck(nonce: request.nonce).encodeBinary(),
|
payload: RawRouteProbeAck(nonce: request.nonce).encodeBinary(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2153,12 +2566,10 @@ class AppProvider with ChangeNotifier {
|
|||||||
if (log.responseCode != 0x88) continue;
|
if (log.responseCode != 0x88) continue;
|
||||||
if (log.rawData.length < 6) continue;
|
if (log.rawData.length < 6) continue;
|
||||||
|
|
||||||
final raw = log.rawData;
|
final decoded = LogRxRouteDecoder.decode(log.rawData);
|
||||||
final payloadType = (raw[3] >> 2) & 0x0F;
|
if (decoded == null) continue;
|
||||||
final pathLen = raw[4];
|
if (decoded.payloadType != expectedPayloadType) continue;
|
||||||
if (payloadType != expectedPayloadType) continue;
|
if (decoded.hopCount != message.pathLen) continue;
|
||||||
if (pathLen != message.pathLen) continue;
|
|
||||||
if (raw.length < 5 + pathLen) continue;
|
|
||||||
|
|
||||||
final deltaMs =
|
final deltaMs =
|
||||||
(log.timestamp.difference(message.receivedAt).inMilliseconds).abs();
|
(log.timestamp.difference(message.receivedAt).inMilliseconds).abs();
|
||||||
@@ -2174,11 +2585,9 @@ class AppProvider with ChangeNotifier {
|
|||||||
|
|
||||||
List<int>? _extractPathBytesFromLog(BlePacketLog? log) {
|
List<int>? _extractPathBytesFromLog(BlePacketLog? log) {
|
||||||
if (log == null) return null;
|
if (log == null) return null;
|
||||||
final raw = log.rawData;
|
final decoded = LogRxRouteDecoder.decode(log.rawData);
|
||||||
if (raw.length < 6) return null;
|
if (decoded == null || decoded.pathBytes.isEmpty) return null;
|
||||||
final pathLen = raw[4];
|
return decoded.pathBytes;
|
||||||
if (pathLen <= 0 || raw.length < 5 + pathLen) return null;
|
|
||||||
return raw.sublist(5, 5 + pathLen);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removed _syncMessages() - messages are automatically synced via PUSH_CODE_MSG_WAITING events
|
// Removed _syncMessages() - messages are automatically synced via PUSH_CODE_MSG_WAITING events
|
||||||
|
|||||||
@@ -175,6 +175,12 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
Function(String messageId, int expectedAckTag, int suggestedTimeoutMs)?
|
Function(String messageId, int expectedAckTag, int suggestedTimeoutMs)?
|
||||||
onMessageSent;
|
onMessageSent;
|
||||||
Function(int ackCode, int roundTripTimeMs)? onMessageDelivered;
|
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)?
|
Function(String messageId, int echoCount, int snrRaw, int rssiDbm)?
|
||||||
onMessageEchoDetected;
|
onMessageEchoDetected;
|
||||||
Function(Uint8List publicKeyPrefix, Uint8List statusData)? onStatusResponse;
|
Function(Uint8List publicKeyPrefix, Uint8List statusData)? onStatusResponse;
|
||||||
@@ -480,6 +486,14 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
/// Start scanning for MeshCore devices
|
/// Start scanning for MeshCore devices
|
||||||
Future<void> startScan() async {
|
Future<void> startScan() async {
|
||||||
debugPrint('🔍 [Provider] startScan() called');
|
debugPrint('🔍 [Provider] startScan() called');
|
||||||
|
if (_deviceInfo.connectionState == ConnectionState.connecting ||
|
||||||
|
_deviceInfo.connectionState == ConnectionState.connected) {
|
||||||
|
debugPrint(
|
||||||
|
'⏭️ [Provider] Ignoring scan request while connection is active: ${_deviceInfo.connectionState}',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_isScanning = true;
|
_isScanning = true;
|
||||||
_scannedDevices.clear();
|
_scannedDevices.clear();
|
||||||
_error = null;
|
_error = null;
|
||||||
@@ -553,6 +567,11 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
'🔵 [Provider] connect() called for device: ${device.platformName}',
|
'🔵 [Provider] connect() called for device: ${device.platformName}',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (_isScanning) {
|
||||||
|
debugPrint('🔵 [Provider] Stopping active scan before connect()');
|
||||||
|
await stopScan();
|
||||||
|
}
|
||||||
|
|
||||||
_deviceInfo = _deviceInfo.copyWith(
|
_deviceInfo = _deviceInfo.copyWith(
|
||||||
deviceId: device.remoteId.toString(),
|
deviceId: device.remoteId.toString(),
|
||||||
deviceName: device.platformName.isNotEmpty
|
deviceName: device.platformName.isNotEmpty
|
||||||
@@ -1073,7 +1092,7 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
///
|
///
|
||||||
/// [messageId] - optional message ID to track delivery status
|
/// [messageId] - optional message ID to track delivery status
|
||||||
/// [contact] - optional contact object for path status logging
|
/// [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({
|
Future<bool> sendTextMessage({
|
||||||
required Uint8List contactPublicKey,
|
required Uint8List contactPublicKey,
|
||||||
required String text,
|
required String text,
|
||||||
@@ -1087,6 +1106,17 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
return false;
|
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)
|
// CRITICAL: Check firmware ACK limit (8 max in circular buffer)
|
||||||
// Rate limit at 7 to stay under the limit
|
// Rate limit at 7 to stay under the limit
|
||||||
if (_messageDeliveryTracker.shouldRateLimit) {
|
if (_messageDeliveryTracker.shouldRateLimit) {
|
||||||
@@ -1111,33 +1141,33 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Log path status and retry info
|
// Log path status and retry info
|
||||||
if (contact != null) {
|
if (effectiveContact != null) {
|
||||||
if (retryAttempt > 0) {
|
if (retryAttempt > 0) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'🔄 [ConnectionProvider] Sending message to ${contact.advName} (retry $retryAttempt/3)',
|
'🔄 [ConnectionProvider] Sending message to ${effectiveContact.advName} (retry $retryAttempt)',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'📤 [ConnectionProvider] Sending message to ${contact.advName}',
|
'📤 [ConnectionProvider] Sending message to ${effectiveContact.advName}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
debugPrint(' Type: ${contact.type.displayName}');
|
debugPrint(' Type: ${effectiveContact.type.displayName}');
|
||||||
debugPrint(' Path status: ${contact.routeSummary}');
|
debugPrint(' Path status: ${effectiveContact.routeSummary}');
|
||||||
if (contact.routeHasPath) {
|
if (effectiveContact.routeHasPath) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
' ✅ Using learned path (${contact.routeHopCount} hop(s), ${contact.routeHashSize}-byte hashes)',
|
' ✅ Using learned path (${effectiveContact.routeHopCount} hop(s), ${effectiveContact.routeHashSize}-byte hashes)',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
debugPrint(' ⚠️ No path available - will use flood mode');
|
debugPrint(' ⚠️ No path available - will use flood mode');
|
||||||
}
|
}
|
||||||
} else if (retryAttempt > 0) {
|
} else if (retryAttempt > 0) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'🔄 [ConnectionProvider] Sending message (retry $retryAttempt/3)',
|
'🔄 [ConnectionProvider] Sending message (retry $retryAttempt)',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track pending operation for auto-recovery (if contact not found in radio)
|
// Track pending operation for auto-recovery (if contact not found in radio)
|
||||||
if (contact != null) {
|
if (effectiveContact != null) {
|
||||||
final operationId = contactPublicKey
|
final operationId = contactPublicKey
|
||||||
.sublist(0, 6)
|
.sublist(0, 6)
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
@@ -1146,7 +1176,7 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
contactPublicKey: contactPublicKey,
|
contactPublicKey: contactPublicKey,
|
||||||
text: text,
|
text: text,
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
contact: contact,
|
contact: effectiveContact,
|
||||||
retryAttempt: retryAttempt,
|
retryAttempt: retryAttempt,
|
||||||
);
|
);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
@@ -1191,7 +1221,7 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
|
|
||||||
// Clear pending operation after successful send (no error)
|
// Clear pending operation after successful send (no error)
|
||||||
// If ERR_CODE_NOT_FOUND occurs, the operation will be recovered automatically
|
// If ERR_CODE_NOT_FOUND occurs, the operation will be recovered automatically
|
||||||
if (contact != null) {
|
if (effectiveContact != null) {
|
||||||
final operationId = contactPublicKey
|
final operationId = contactPublicKey
|
||||||
.sublist(0, 6)
|
.sublist(0, 6)
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
|||||||
@@ -5,15 +5,13 @@ import '../../models/contact.dart';
|
|||||||
|
|
||||||
/// Manages message retry state and logic
|
/// Manages message retry state and logic
|
||||||
///
|
///
|
||||||
/// This helper class centralizes retry logic for direct messages, implementing
|
/// This helper class centralizes retry logic for direct messages.
|
||||||
/// a progressive timeout strategy (4s, 8s, 12s) for messages sent to contacts
|
|
||||||
/// with learned routing paths.
|
|
||||||
///
|
///
|
||||||
/// IMPORTANT: Based on MeshCore firmware analysis:
|
/// IMPORTANT: Based on MeshCore firmware analysis:
|
||||||
/// - Firmware calculates timeout based on path length and airtime
|
/// - Firmware calculates timeout based on path length and airtime
|
||||||
/// - Direct mode: ~(path_len * airtime * 2) + margin
|
/// - Direct mode: ~(path_len * airtime * 2) + margin
|
||||||
/// - Flood mode: ~10-30 seconds for multi-hop
|
/// - 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
|
/// - Firmware does NOT automatically retry - app must implement
|
||||||
class MessageRetryManager {
|
class MessageRetryManager {
|
||||||
// Track retry state for each message ID
|
// Track retry state for each message ID
|
||||||
@@ -21,10 +19,10 @@ class MessageRetryManager {
|
|||||||
final Map<String, DateTime> _lastRetryTimes = {};
|
final Map<String, DateTime> _lastRetryTimes = {};
|
||||||
final Map<String, int> _pathFailureStreaks = {};
|
final Map<String, int> _pathFailureStreaks = {};
|
||||||
|
|
||||||
// Progressive timeout values in milliseconds
|
static const int maxRetryAttempts = 4;
|
||||||
// These are app-level timeouts, separate from firmware's suggested timeout
|
|
||||||
// Firmware timeout is for ACK arrival, these are for retry attempts
|
// Retry backoff values in milliseconds.
|
||||||
static const List<int> _timeouts = [4000, 8000, 12000];
|
static const List<int> _retryDelays = [1000, 2000, 4000, 8000];
|
||||||
static const int _defaultLoRaSf = 10;
|
static const int _defaultLoRaSf = 10;
|
||||||
static const int _defaultLoRaCr = 5;
|
static const int _defaultLoRaCr = 5;
|
||||||
static const int _defaultLoRaBwHz = 250000;
|
static const int _defaultLoRaBwHz = 250000;
|
||||||
@@ -32,13 +30,12 @@ class MessageRetryManager {
|
|||||||
static const int _defaultLoRaCrcEnabled = 1;
|
static const int _defaultLoRaCrcEnabled = 1;
|
||||||
static const int _defaultLoRaExplicitHeader = 1;
|
static const int _defaultLoRaExplicitHeader = 1;
|
||||||
|
|
||||||
/// Get timeout for a specific retry attempt (0-2)
|
/// Get backoff delay for the next retry attempt.
|
||||||
/// Returns: 4000ms for attempt 0, 8000ms for attempt 1, 12000ms for attempt 2
|
int getDelayForAttempt(int attempt) {
|
||||||
int getTimeoutForAttempt(int attempt) {
|
if (attempt < 0 || attempt >= _retryDelays.length) {
|
||||||
if (attempt < 0 || attempt >= _timeouts.length) {
|
return _retryDelays.last;
|
||||||
return _timeouts.last; // Default to last timeout if out of range
|
|
||||||
}
|
}
|
||||||
return _timeouts[attempt];
|
return _retryDelays[attempt];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate a conservative delivery-ACK timeout when firmware doesn't
|
/// 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);
|
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) {
|
bool canRetry(Message message, Contact contact) {
|
||||||
// Never retry if already tried flood mode
|
return message.retryAttempt < maxRetryAttempts;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Track a retry attempt for a message
|
/// Track a retry attempt for a message
|
||||||
|
|||||||
@@ -38,12 +38,6 @@ Future<bool> serveCachedSessionFragments<T>({
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!requester.routeSupportsLegacyRawTransport) {
|
|
||||||
debugPrint(
|
|
||||||
'⚠️ [$providerLabel] ${requester.advName} route uses unsupported 3-byte raw transport on current client',
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (requester.outPath.isEmpty) {
|
if (requester.outPath.isEmpty) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'⚠️ [$providerLabel] ${requester.advName} has empty outPath payload',
|
'⚠️ [$providerLabel] ${requester.advName} has empty outPath payload',
|
||||||
@@ -64,7 +58,7 @@ Future<bool> serveCachedSessionFragments<T>({
|
|||||||
try {
|
try {
|
||||||
await sendRawPacket(
|
await sendRawPacket(
|
||||||
contactPath: requester.outPath,
|
contactPath: requester.outPath,
|
||||||
contactPathLen: requester.routeSignedPathLen,
|
contactPathLen: requester.routeEncodedPathLen,
|
||||||
payload: encodeBinary(fragment),
|
payload: encodeBinary(fragment),
|
||||||
);
|
);
|
||||||
servedCount++;
|
servedCount++;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import '../models/contact.dart';
|
|||||||
import '../models/message_contact_location.dart';
|
import '../models/message_contact_location.dart';
|
||||||
import '../models/message_reception_details.dart';
|
import '../models/message_reception_details.dart';
|
||||||
import '../models/message_transfer_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/sar_marker.dart';
|
||||||
import '../models/map_drawing.dart';
|
import '../models/map_drawing.dart';
|
||||||
import '../services/message_storage_service.dart';
|
import '../services/message_storage_service.dart';
|
||||||
@@ -27,6 +29,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
final Map<String, MessageContactLocation> _messageContactLocations = {};
|
final Map<String, MessageContactLocation> _messageContactLocations = {};
|
||||||
final Map<String, MessageReceptionDetails> _messageReceptionDetails = {};
|
final Map<String, MessageReceptionDetails> _messageReceptionDetails = {};
|
||||||
final Map<String, MessageTransferDetails> _messageTransferDetails = {};
|
final Map<String, MessageTransferDetails> _messageTransferDetails = {};
|
||||||
|
final Map<String, MessageRouteMetadata> _messageRouteMetadata = {};
|
||||||
|
|
||||||
// Track pending sent messages by expected ACK/TAG
|
// Track pending sent messages by expected ACK/TAG
|
||||||
final Map<int, Message> _pendingSentMessages = {};
|
final Map<int, Message> _pendingSentMessages = {};
|
||||||
@@ -59,6 +62,8 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
|
|
||||||
// Navigation state for message highlighting/scrolling
|
// Navigation state for message highlighting/scrolling
|
||||||
String? _targetMessageId;
|
String? _targetMessageId;
|
||||||
|
String? _targetDestinationType;
|
||||||
|
String? _targetRecipientPublicKeyHex;
|
||||||
|
|
||||||
// Helper function to compare Uint8List for equality
|
// Helper function to compare Uint8List for equality
|
||||||
bool _listEquals(Uint8List a, Uint8List b) {
|
bool _listEquals(Uint8List a, Uint8List b) {
|
||||||
@@ -81,6 +86,25 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> Function({required Contact contact, required int failureStreak})?
|
Future<void> Function({required Contact contact, required int failureStreak})?
|
||||||
onDirectPathFailedCallback;
|
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(Uint8List? publicKey)? resolveContactNameCallback;
|
||||||
String Function(int channelIdx)? resolveChannelNameCallback;
|
String Function(int channelIdx)? resolveChannelNameCallback;
|
||||||
@@ -126,6 +150,30 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
MessageTransferDetails? getMessageTransferDetails(String messageId) =>
|
MessageTransferDetails? getMessageTransferDetails(String messageId) =>
|
||||||
_messageTransferDetails[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
|
/// Set localizations for notifications
|
||||||
void setLocalizations(AppLocalizations localizations) {
|
void setLocalizations(AppLocalizations localizations) {
|
||||||
_localizations = localizations;
|
_localizations = localizations;
|
||||||
@@ -137,16 +185,83 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? get targetDestinationType => _targetDestinationType;
|
||||||
|
String? get targetRecipientPublicKeyHex => _targetRecipientPublicKeyHex;
|
||||||
|
|
||||||
|
void navigateToDestination(String type, {String? recipientPublicKeyHex}) {
|
||||||
|
_targetDestinationType = type;
|
||||||
|
_targetRecipientPublicKeyHex = recipientPublicKeyHex;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
/// Clear message navigation state
|
/// Clear message navigation state
|
||||||
void clearMessageNavigation() {
|
void clearMessageNavigation() {
|
||||||
_targetMessageId = null;
|
_targetMessageId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearDestinationNavigation() {
|
||||||
|
_targetDestinationType = null;
|
||||||
|
_targetRecipientPublicKeyHex = null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get count of unread messages (excluding sent messages and system messages)
|
/// Get count of unread messages (excluding sent messages and system messages)
|
||||||
int get unreadCount => _messages
|
int get unreadCount => _messages
|
||||||
.where((m) => !m.isRead && !m.isSentMessage && !m.isSystemMessage)
|
.where((m) => !m.isRead && !m.isSentMessage && !m.isSystemMessage)
|
||||||
.length;
|
.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
|
/// Initialize and load persisted messages
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (_isInitialized) return;
|
if (_isInitialized) return;
|
||||||
@@ -160,6 +275,8 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
.loadMessageReceptionDetails();
|
.loadMessageReceptionDetails();
|
||||||
final storedTransferDetails = await _storageService
|
final storedTransferDetails = await _storageService
|
||||||
.loadMessageTransferDetails();
|
.loadMessageTransferDetails();
|
||||||
|
final storedRouteMetadata = await _storageService
|
||||||
|
.loadMessageRouteMetadata();
|
||||||
_messageContactLocations
|
_messageContactLocations
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(storedContactLocations);
|
..addAll(storedContactLocations);
|
||||||
@@ -169,6 +286,9 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
_messageTransferDetails
|
_messageTransferDetails
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(storedTransferDetails);
|
..addAll(storedTransferDetails);
|
||||||
|
_messageRouteMetadata
|
||||||
|
..clear()
|
||||||
|
..addAll(storedRouteMetadata);
|
||||||
|
|
||||||
// Add stored messages with enhancement to ensure SAR detection
|
// Add stored messages with enhancement to ensure SAR detection
|
||||||
for (final message in storedMessages) {
|
for (final message in storedMessages) {
|
||||||
@@ -645,14 +765,14 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
final seconds = (voiceEnvelope.durationMs / 1000).ceil();
|
final seconds = (voiceEnvelope.durationMs / 1000).ceil();
|
||||||
final summary =
|
final summary =
|
||||||
'Voice message - ${voiceEnvelope.mode.label} - ${seconds}s - ${voiceEnvelope.total} packets';
|
'Voice message - ${voiceEnvelope.mode.label} - ${seconds}s - ${voiceEnvelope.total} packets';
|
||||||
return isChannelMessage ? '$senderName\n$summary' : summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
final imageEnvelope = ImageEnvelope.tryParse(message.text);
|
final imageEnvelope = ImageEnvelope.tryParse(message.text);
|
||||||
if (imageEnvelope != null) {
|
if (imageEnvelope != null) {
|
||||||
final summary =
|
final summary =
|
||||||
'Image - ${imageEnvelope.format.label} - ${imageEnvelope.width}x${imageEnvelope.height} - ${_formatBytes(imageEnvelope.sizeBytes)}';
|
'Image - ${imageEnvelope.format.label} - ${imageEnvelope.width}x${imageEnvelope.height} - ${_formatBytes(imageEnvelope.sizeBytes)}';
|
||||||
return isChannelMessage ? '$senderName\n$summary' : summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isChannelMessage && message.recipientPublicKey != null) {
|
if (!isChannelMessage && message.recipientPublicKey != null) {
|
||||||
@@ -665,10 +785,6 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isChannelMessage) {
|
|
||||||
return '$senderName\n${message.text}';
|
|
||||||
}
|
|
||||||
|
|
||||||
return message.text;
|
return message.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,6 +804,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
messageContactLocations: _messageContactLocations,
|
messageContactLocations: _messageContactLocations,
|
||||||
messageReceptionDetails: _messageReceptionDetails,
|
messageReceptionDetails: _messageReceptionDetails,
|
||||||
messageTransferDetails: _messageTransferDetails,
|
messageTransferDetails: _messageTransferDetails,
|
||||||
|
messageRouteMetadata: _messageRouteMetadata,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [MessagesProvider] Error persisting messages: $e');
|
debugPrint('❌ [MessagesProvider] Error persisting messages: $e');
|
||||||
@@ -768,6 +885,54 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mark unread messages for a specific destination as read.
|
||||||
|
void markDestinationAsRead({
|
||||||
|
required String destinationType,
|
||||||
|
Contact? contact,
|
||||||
|
}) {
|
||||||
|
if (destinationType == 'all') {
|
||||||
|
markAllAsRead();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasChanges = false;
|
||||||
|
for (int i = 0; i < _messages.length; i++) {
|
||||||
|
final message = _messages[i];
|
||||||
|
if (message.isRead || message.isSentMessage || message.isSystemMessage) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final matchesDestination = switch (destinationType) {
|
||||||
|
'channel' => _isChannelMessageForContact(message, contact),
|
||||||
|
'contact' || 'room' => contact != null && _isMessageForDestination(message, contact),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!matchesDestination) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_messages[i] = message.copyWith(isRead: true);
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
_persistMessages();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isChannelMessageForContact(Message message, Contact? contact) {
|
||||||
|
if (!message.isChannelMessage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selectedChannelIdx = contact != null && contact.publicKey.length > 1
|
||||||
|
? contact.publicKey[1]
|
||||||
|
: 0;
|
||||||
|
return (message.channelIdx ?? 0) == selectedChannelIdx;
|
||||||
|
}
|
||||||
|
|
||||||
/// Mark a specific message as read
|
/// Mark a specific message as read
|
||||||
void markAsRead(String messageId) {
|
void markAsRead(String messageId) {
|
||||||
final index = _messages.indexWhere((m) => m.id == messageId);
|
final index = _messages.indexWhere((m) => m.id == messageId);
|
||||||
@@ -806,6 +971,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
_messageContactLocations.remove(messageId);
|
_messageContactLocations.remove(messageId);
|
||||||
_messageReceptionDetails.remove(messageId);
|
_messageReceptionDetails.remove(messageId);
|
||||||
_messageTransferDetails.remove(messageId);
|
_messageTransferDetails.remove(messageId);
|
||||||
|
_messageRouteMetadata.remove(messageId);
|
||||||
|
|
||||||
debugPrint('🗑️ [MessagesProvider] Message $messageId deleted');
|
debugPrint('🗑️ [MessagesProvider] Message $messageId deleted');
|
||||||
|
|
||||||
@@ -837,6 +1003,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
_messageContactLocations.clear();
|
_messageContactLocations.clear();
|
||||||
_messageReceptionDetails.clear();
|
_messageReceptionDetails.clear();
|
||||||
_messageTransferDetails.clear();
|
_messageTransferDetails.clear();
|
||||||
|
_messageRouteMetadata.clear();
|
||||||
_persistMessages();
|
_persistMessages();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -854,6 +1021,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
_messageContactLocations.clear();
|
_messageContactLocations.clear();
|
||||||
_messageReceptionDetails.clear();
|
_messageReceptionDetails.clear();
|
||||||
_messageTransferDetails.clear();
|
_messageTransferDetails.clear();
|
||||||
|
_messageRouteMetadata.clear();
|
||||||
_persistMessages();
|
_persistMessages();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -1549,6 +1717,12 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
final deliveredContact = _messageContactMap[message.id];
|
final deliveredContact = _messageContactMap[message.id];
|
||||||
if (deliveredContact != null) {
|
if (deliveredContact != null) {
|
||||||
_retryManager.recordDeliverySuccess(deliveredContact);
|
_retryManager.recordDeliverySuccess(deliveredContact);
|
||||||
|
onDirectMessageDeliveredCallback?.call(
|
||||||
|
messageId: message.id,
|
||||||
|
contact: deliveredContact,
|
||||||
|
message: updatedMessage,
|
||||||
|
roundTripTimeMs: roundTripTimeMs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
@@ -1592,6 +1766,12 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
final deliveredContact = _messageContactMap[historicalMessageId];
|
final deliveredContact = _messageContactMap[historicalMessageId];
|
||||||
if (deliveredContact != null) {
|
if (deliveredContact != null) {
|
||||||
_retryManager.recordDeliverySuccess(deliveredContact);
|
_retryManager.recordDeliverySuccess(deliveredContact);
|
||||||
|
onDirectMessageDeliveredCallback?.call(
|
||||||
|
messageId: historicalMessageId,
|
||||||
|
contact: deliveredContact,
|
||||||
|
message: _messages[historicalIndex],
|
||||||
|
roundTripTimeMs: roundTripTimeMs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_persistMessages();
|
_persistMessages();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -1677,29 +1857,29 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
debugPrint(' Contact has path: ${contact?.routeHasPath ?? false}');
|
debugPrint(' Contact has path: ${contact?.routeHasPath ?? false}');
|
||||||
debugPrint(' Used flood fallback: ${message.usedFloodFallback}');
|
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)) {
|
if (contact != null && _retryManager.canRetry(message, contact)) {
|
||||||
// RETRY: Contact has path and retry attempts < 3
|
|
||||||
_scheduleRetry(messageId, message, contact);
|
_scheduleRetry(messageId, message, contact);
|
||||||
} else if (contact != null &&
|
} else if (contact != null && !routerFallbackAttempted) {
|
||||||
_retryManager.shouldUseFloodFallback(message, contact)) {
|
unawaited(_sendWithFinalRouterFallback(messageId, message, contact));
|
||||||
// FLOOD FALLBACK: After 3 retries failed, try flood once
|
|
||||||
_sendWithFloodMode(messageId, message, contact);
|
|
||||||
} else {
|
} else {
|
||||||
// PERMANENTLY FAILED: No retry possible
|
|
||||||
_markAsPermanentlyFailed(messageId, message);
|
_markAsPermanentlyFailed(messageId, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Schedule a retry with progressive timeout
|
/// Schedule a retry with exponential backoff.
|
||||||
void _scheduleRetry(String messageId, Message message, Contact contact) {
|
void _scheduleRetry(String messageId, Message message, Contact contact) {
|
||||||
final nextAttempt = message.retryAttempt + 1;
|
final nextAttempt = message.retryAttempt + 1;
|
||||||
final timeout = _retryManager.getTimeoutForAttempt(message.retryAttempt);
|
final delayMs = _retryManager.getDelayForAttempt(message.retryAttempt);
|
||||||
|
|
||||||
debugPrint(
|
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
|
// Update message with new retry attempt
|
||||||
final index = _messages.indexWhere((m) => m.id == messageId);
|
final index = _messages.indexWhere((m) => m.id == messageId);
|
||||||
@@ -1720,10 +1900,10 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
// Track retry
|
// Track retry
|
||||||
_retryManager.trackRetry(messageId, nextAttempt);
|
_retryManager.trackRetry(messageId, nextAttempt);
|
||||||
|
|
||||||
notifyListeners(); // Update UI to show "Retrying (X/3)..."
|
notifyListeners();
|
||||||
|
|
||||||
// Schedule actual retry after delay
|
// Schedule actual retry after delay
|
||||||
Timer(Duration(milliseconds: timeout), () async {
|
Timer(Duration(milliseconds: delayMs), () async {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'⏰ [MessagesProvider] Executing retry $nextAttempt for message $messageId',
|
'⏰ [MessagesProvider] Executing retry $nextAttempt for message $messageId',
|
||||||
);
|
);
|
||||||
@@ -1759,50 +1939,47 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send message with flood mode as last resort
|
Future<void> _sendWithFinalRouterFallback(
|
||||||
Future<void> _sendWithFloodMode(
|
|
||||||
String messageId,
|
String messageId,
|
||||||
Message message,
|
Message message,
|
||||||
Contact contact,
|
Contact contact,
|
||||||
) async {
|
) async {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'🌊 [MessagesProvider] Trying flood mode for message $messageId',
|
'🛟 [MessagesProvider] Trying final router fallback for $messageId',
|
||||||
);
|
);
|
||||||
|
|
||||||
final index = _messages.indexWhere((m) => m.id == messageId);
|
final index = _messages.indexWhere((m) => m.id == messageId);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
_messages[index] = message.copyWith(
|
_messages[index] = message.copyWith(
|
||||||
usedFloodFallback: true,
|
|
||||||
deliveryStatus: MessageDeliveryStatus.sending,
|
deliveryStatus: MessageDeliveryStatus.sending,
|
||||||
|
lastRetryAt: DateTime.now(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cancel old timeout timer
|
|
||||||
_timeoutTimers[message.id]?.cancel();
|
_timeoutTimers[message.id]?.cancel();
|
||||||
_timeoutTimers.remove(message.id);
|
_timeoutTimers.remove(message.id);
|
||||||
if (message.expectedAckTag != null) {
|
if (message.expectedAckTag != null) {
|
||||||
_pendingSentMessages.remove(message.expectedAckTag);
|
_pendingSentMessages.remove(message.expectedAckTag);
|
||||||
}
|
}
|
||||||
|
_clearAckHistoryForMessage(messageId);
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
// Send with flood mode (no retry after this)
|
if (onFinalRouterFallbackCallback == null) {
|
||||||
if (sendMessageCallback != null) {
|
debugPrint(
|
||||||
final queued = await sendMessageCallback!(
|
'⚠️ [MessagesProvider] onFinalRouterFallbackCallback not set',
|
||||||
contactPublicKey: contact.publicKey,
|
);
|
||||||
text: message.text,
|
_markAsPermanentlyFailed(messageId, _messages[index]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final queued = await onFinalRouterFallbackCallback!(
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
contact: contact,
|
contact: contact,
|
||||||
retryAttempt: 0, // Reset attempt for flood
|
message: _messages[index],
|
||||||
);
|
);
|
||||||
if (!queued) {
|
if (!queued) {
|
||||||
_markAsPermanentlyFailed(messageId, _messages[index]);
|
_markAsPermanentlyFailed(messageId, _messages[index]);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
debugPrint(
|
|
||||||
'⚠️ [MessagesProvider] sendMessageCallback not set, cannot send flood',
|
|
||||||
);
|
|
||||||
_markAsPermanentlyFailed(messageId, _messages[index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
_persistMessages();
|
_persistMessages();
|
||||||
}
|
}
|
||||||
@@ -1830,20 +2007,16 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
_retryManager.clearRetry(messageId);
|
_retryManager.clearRetry(messageId);
|
||||||
|
|
||||||
final failedContact = _messageContactMap[messageId];
|
final failedContact = _messageContactMap[messageId];
|
||||||
if (failedContact != null && failedContact.routeHasPath) {
|
if (failedContact != null &&
|
||||||
final failureStreak = _retryManager.recordPathFailure(failedContact);
|
onFinalDirectMessageFailureCallback != null) {
|
||||||
debugPrint(
|
|
||||||
' Path failure streak for ${failedContact.advName}: $failureStreak',
|
|
||||||
);
|
|
||||||
if (failureStreak >= 2 && onDirectPathFailedCallback != null) {
|
|
||||||
unawaited(
|
unawaited(
|
||||||
onDirectPathFailedCallback!(
|
onFinalDirectMessageFailureCallback!(
|
||||||
|
messageId: messageId,
|
||||||
contact: failedContact,
|
contact: failedContact,
|
||||||
failureStreak: failureStreak,
|
message: _messages[index],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
_persistMessages();
|
_persistMessages();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -1870,6 +2043,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
_clearAckHistoryForMessage(messageId);
|
_clearAckHistoryForMessage(messageId);
|
||||||
_retryManager.clearRetry(messageId);
|
_retryManager.clearRetry(messageId);
|
||||||
|
_messageRouteMetadata.remove(messageId);
|
||||||
|
|
||||||
_messages[index] = Message(
|
_messages[index] = Message(
|
||||||
id: message.id,
|
id: message.id,
|
||||||
|
|||||||
@@ -7,13 +7,23 @@ import '../models/contact.dart';
|
|||||||
import '../providers/contacts_provider.dart';
|
import '../providers/contacts_provider.dart';
|
||||||
import '../providers/app_provider.dart';
|
import '../providers/app_provider.dart';
|
||||||
import '../providers/connection_provider.dart';
|
import '../providers/connection_provider.dart';
|
||||||
|
import '../providers/messages_provider.dart';
|
||||||
|
import '../services/message_destination_preferences.dart';
|
||||||
|
import '../utils/contact_grouping.dart';
|
||||||
|
import '../utils/avatar_label_helper.dart';
|
||||||
|
import '../widgets/common/contact_avatar.dart';
|
||||||
import '../widgets/contacts/contact_tile.dart';
|
import '../widgets/contacts/contact_tile.dart';
|
||||||
import '../widgets/contacts/add_channel_dialog.dart';
|
import '../widgets/contacts/add_channel_dialog.dart';
|
||||||
|
|
||||||
class ContactsTab extends StatefulWidget {
|
class ContactsTab extends StatefulWidget {
|
||||||
final VoidCallback? onNavigateToMap;
|
final VoidCallback? onNavigateToMap;
|
||||||
|
final VoidCallback? onNavigateToMessages;
|
||||||
|
|
||||||
const ContactsTab({super.key, this.onNavigateToMap});
|
const ContactsTab({
|
||||||
|
super.key,
|
||||||
|
this.onNavigateToMap,
|
||||||
|
this.onNavigateToMessages,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ContactsTab> createState() => _ContactsTabState();
|
State<ContactsTab> createState() => _ContactsTabState();
|
||||||
@@ -22,6 +32,11 @@ class ContactsTab extends StatefulWidget {
|
|||||||
class _ContactsTabState extends State<ContactsTab> {
|
class _ContactsTabState extends State<ContactsTab> {
|
||||||
Position? _currentPosition;
|
Position? _currentPosition;
|
||||||
final Set<String> _resolvingAdvertKeys = <String>{};
|
final Set<String> _resolvingAdvertKeys = <String>{};
|
||||||
|
final Map<ContactSection, ContactSortMode> _sortModes = {
|
||||||
|
ContactSection.teamMembers: ContactSortMode.lastSeen,
|
||||||
|
ContactSection.repeaters: ContactSortMode.lastSeen,
|
||||||
|
ContactSection.rooms: ContactSortMode.lastSeen,
|
||||||
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -120,10 +135,20 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
return l10n.daysAgo(diff.inDays);
|
return l10n.daysAgo(diff.inDays);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Contact> _sortContactsByDistance(List<Contact> contacts) {
|
List<Contact> _sortContacts(List<Contact> contacts, ContactSection section) {
|
||||||
final sorted = List<Contact>.from(contacts);
|
final sorted = List<Contact>.from(contacts);
|
||||||
|
if (section == ContactSection.channels) {
|
||||||
|
sorted.sort(
|
||||||
|
(a, b) =>
|
||||||
|
a.displayName.toLowerCase().compareTo(b.displayName.toLowerCase()),
|
||||||
|
);
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
final sortMode = _sortModes[section] ?? ContactSortMode.lastSeen;
|
||||||
|
|
||||||
sorted.sort((a, b) {
|
sorted.sort((a, b) {
|
||||||
|
if (sortMode == ContactSortMode.distance) {
|
||||||
final distanceA = _distanceFromCurrentPosition(a);
|
final distanceA = _distanceFromCurrentPosition(a);
|
||||||
final distanceB = _distanceFromCurrentPosition(b);
|
final distanceB = _distanceFromCurrentPosition(b);
|
||||||
|
|
||||||
@@ -135,6 +160,7 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
} else if (distanceB != null) {
|
} else if (distanceB != null) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return b.lastSeenTime.compareTo(a.lastSeenTime);
|
return b.lastSeenTime.compareTo(a.lastSeenTime);
|
||||||
});
|
});
|
||||||
@@ -203,19 +229,31 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Consumer<ContactsProvider>(
|
body: Consumer<ContactsProvider>(
|
||||||
builder: (context, contactsProvider, child) {
|
builder: (context, contactsProvider, child) {
|
||||||
final chatContacts = _sortContactsByDistance(
|
final messagesProvider = context.watch<MessagesProvider>();
|
||||||
|
final chatContacts = _sortContacts(
|
||||||
contactsProvider.chatContacts,
|
contactsProvider.chatContacts,
|
||||||
|
ContactSection.teamMembers,
|
||||||
|
);
|
||||||
|
final repeaters = _sortContacts(
|
||||||
|
contactsProvider.repeaters,
|
||||||
|
ContactSection.repeaters,
|
||||||
|
);
|
||||||
|
final rooms = _sortContacts(
|
||||||
|
contactsProvider.rooms,
|
||||||
|
ContactSection.rooms,
|
||||||
|
);
|
||||||
|
final channels = _sortContacts(
|
||||||
|
contactsProvider.channels,
|
||||||
|
ContactSection.channels,
|
||||||
);
|
);
|
||||||
final repeaters = _sortContactsByDistance(contactsProvider.repeaters);
|
|
||||||
final rooms = _sortContactsByDistance(contactsProvider.rooms);
|
|
||||||
final channels = _sortContactsByDistance(contactsProvider.channels);
|
|
||||||
final pendingAdverts = contactsProvider.pendingAdverts;
|
final pendingAdverts = contactsProvider.pendingAdverts;
|
||||||
|
|
||||||
// Check if there are any displayable contacts (excluding channels)
|
// Check if there are any displayable contacts
|
||||||
final hasDisplayableContacts =
|
final hasDisplayableContacts =
|
||||||
chatContacts.isNotEmpty ||
|
chatContacts.isNotEmpty ||
|
||||||
repeaters.isNotEmpty ||
|
repeaters.isNotEmpty ||
|
||||||
rooms.isNotEmpty ||
|
rooms.isNotEmpty ||
|
||||||
|
channels.isNotEmpty ||
|
||||||
pendingAdverts.isNotEmpty;
|
pendingAdverts.isNotEmpty;
|
||||||
|
|
||||||
if (!hasDisplayableContacts) {
|
if (!hasDisplayableContacts) {
|
||||||
@@ -276,16 +314,12 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
title: l10n.teamMembers,
|
title: l10n.teamMembers,
|
||||||
count: chatContacts.length,
|
count: chatContacts.length,
|
||||||
icon: Icons.people,
|
icon: Icons.people,
|
||||||
),
|
trailing: _buildSortMenu(
|
||||||
...chatContacts.map(
|
context,
|
||||||
(contact) => ContactTile(
|
ContactSection.teamMembers,
|
||||||
contact: contact,
|
|
||||||
currentPosition: _currentPosition,
|
|
||||||
calculateDistance: _calculateDistanceInMeters,
|
|
||||||
formatDistance: _formatDistance,
|
|
||||||
onNavigateToMap: widget.onNavigateToMap,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
..._buildContactSectionItems(chatContacts),
|
||||||
const Divider(height: 32),
|
const Divider(height: 32),
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -295,16 +329,9 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
title: l10n.repeaters,
|
title: l10n.repeaters,
|
||||||
count: repeaters.length,
|
count: repeaters.length,
|
||||||
icon: Icons.router,
|
icon: Icons.router,
|
||||||
|
trailing: _buildSortMenu(context, ContactSection.repeaters),
|
||||||
),
|
),
|
||||||
...repeaters.map(
|
..._buildContactSectionItems(repeaters),
|
||||||
(contact) => ContactTile(
|
|
||||||
contact: contact,
|
|
||||||
currentPosition: _currentPosition,
|
|
||||||
calculateDistance: _calculateDistanceInMeters,
|
|
||||||
formatDistance: _formatDistance,
|
|
||||||
onNavigateToMap: widget.onNavigateToMap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 32),
|
const Divider(height: 32),
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -314,16 +341,9 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
title: l10n.rooms,
|
title: l10n.rooms,
|
||||||
count: rooms.length,
|
count: rooms.length,
|
||||||
icon: Icons.tag,
|
icon: Icons.tag,
|
||||||
|
trailing: _buildSortMenu(context, ContactSection.rooms),
|
||||||
),
|
),
|
||||||
...rooms.map(
|
..._buildContactSectionItems(rooms),
|
||||||
(contact) => ContactTile(
|
|
||||||
contact: contact,
|
|
||||||
currentPosition: _currentPosition,
|
|
||||||
calculateDistance: _calculateDistanceInMeters,
|
|
||||||
formatDistance: _formatDistance,
|
|
||||||
onNavigateToMap: widget.onNavigateToMap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 32),
|
const Divider(height: 32),
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -335,12 +355,11 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
),
|
),
|
||||||
if (channels.isNotEmpty) ...[
|
if (channels.isNotEmpty) ...[
|
||||||
...channels.map(
|
...channels.map(
|
||||||
(contact) => ContactTile(
|
(channel) => _ChannelActivityCard(
|
||||||
contact: contact,
|
channel: channel,
|
||||||
currentPosition: _currentPosition,
|
messagesProvider: messagesProvider,
|
||||||
calculateDistance: _calculateDistanceInMeters,
|
contactsProvider: contactsProvider,
|
||||||
formatDistance: _formatDistance,
|
onNavigateToMessages: widget.onNavigateToMessages,
|
||||||
onNavigateToMap: widget.onNavigateToMap,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -371,8 +390,97 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildContactSectionItems(List<Contact> contacts) {
|
||||||
|
final items = ContactGrouping.buildItemsFromSorted(contacts);
|
||||||
|
|
||||||
|
return items.map((item) {
|
||||||
|
if (item.isGroup) {
|
||||||
|
return _InferredContactGroupCard(
|
||||||
|
label: item.group!.label,
|
||||||
|
contacts: item.group!.contacts,
|
||||||
|
currentPosition: _currentPosition,
|
||||||
|
calculateDistance: _calculateDistanceInMeters,
|
||||||
|
formatDistance: _formatDistance,
|
||||||
|
onNavigateToMap: widget.onNavigateToMap,
|
||||||
|
onNavigateToMessages: widget.onNavigateToMessages,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ContactTile(
|
||||||
|
contact: item.contact!,
|
||||||
|
currentPosition: _currentPosition,
|
||||||
|
calculateDistance: _calculateDistanceInMeters,
|
||||||
|
formatDistance: _formatDistance,
|
||||||
|
onNavigateToMap: widget.onNavigateToMap,
|
||||||
|
onNavigateToMessages: widget.onNavigateToMessages,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSortMenu(BuildContext context, ContactSection section) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final selectedMode = _sortModes[section] ?? ContactSortMode.lastSeen;
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return PopupMenuButton<ContactSortMode>(
|
||||||
|
tooltip: 'Sort',
|
||||||
|
initialValue: selectedMode,
|
||||||
|
onSelected: (sortMode) {
|
||||||
|
setState(() {
|
||||||
|
_sortModes[section] = sortMode;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
PopupMenuItem<ContactSortMode>(
|
||||||
|
value: ContactSortMode.lastSeen,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.schedule,
|
||||||
|
size: 18,
|
||||||
|
color: selectedMode == ContactSortMode.lastSeen
|
||||||
|
? colorScheme.primary
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(l10n.lastSeen),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem<ContactSortMode>(
|
||||||
|
value: ContactSortMode.distance,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.near_me,
|
||||||
|
size: 18,
|
||||||
|
color: selectedMode == ContactSortMode.distance
|
||||||
|
? colorScheme.primary
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(l10n.distance),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
child: Icon(
|
||||||
|
Icons.more_horiz,
|
||||||
|
size: 18,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ContactSortMode { lastSeen, distance }
|
||||||
|
|
||||||
|
enum ContactSection { teamMembers, repeaters, rooms, channels }
|
||||||
|
|
||||||
class _PendingAdvertTile extends StatelessWidget {
|
class _PendingAdvertTile extends StatelessWidget {
|
||||||
final PendingAdvert advert;
|
final PendingAdvert advert;
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
@@ -417,11 +525,13 @@ class _SectionHeader extends StatelessWidget {
|
|||||||
final String title;
|
final String title;
|
||||||
final int count;
|
final int count;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
final Widget? trailing;
|
||||||
|
|
||||||
const _SectionHeader({
|
const _SectionHeader({
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.count,
|
required this.count,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
|
this.trailing,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -450,6 +560,415 @@ class _SectionHeader extends StatelessWidget {
|
|||||||
style: Theme.of(context).textTheme.labelSmall,
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (trailing != null) ...[const Spacer(), trailing!],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InferredContactGroupCard extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final List<Contact> contacts;
|
||||||
|
final Position? currentPosition;
|
||||||
|
final double Function(double, double, double, double) calculateDistance;
|
||||||
|
final String Function(double) formatDistance;
|
||||||
|
final VoidCallback? onNavigateToMap;
|
||||||
|
final VoidCallback? onNavigateToMessages;
|
||||||
|
|
||||||
|
const _InferredContactGroupCard({
|
||||||
|
required this.label,
|
||||||
|
required this.contacts,
|
||||||
|
required this.currentPosition,
|
||||||
|
required this.calculateDistance,
|
||||||
|
required this.formatDistance,
|
||||||
|
required this.onNavigateToMap,
|
||||||
|
required this.onNavigateToMessages,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerLow,
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
border: Border.all(
|
||||||
|
color: colorScheme.outlineVariant.withValues(alpha: 0.35),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Theme(
|
||||||
|
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
|
||||||
|
child: ExpansionTile(
|
||||||
|
tilePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||||
|
childrenPadding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
|
||||||
|
initiallyExpanded: false,
|
||||||
|
leading: Icon(
|
||||||
|
Icons.folder_copy_outlined,
|
||||||
|
size: 18,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w800),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.primaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
contacts.length.toString(),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
...contacts.map(
|
||||||
|
(contact) => ContactTile(
|
||||||
|
contact: contact,
|
||||||
|
currentPosition: currentPosition,
|
||||||
|
calculateDistance: calculateDistance,
|
||||||
|
formatDistance: formatDistance,
|
||||||
|
onNavigateToMap: onNavigateToMap,
|
||||||
|
onNavigateToMessages: onNavigateToMessages,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChannelActivityCard extends StatelessWidget {
|
||||||
|
final Contact channel;
|
||||||
|
final MessagesProvider messagesProvider;
|
||||||
|
final ContactsProvider contactsProvider;
|
||||||
|
final VoidCallback? onNavigateToMessages;
|
||||||
|
|
||||||
|
const _ChannelActivityCard({
|
||||||
|
required this.channel,
|
||||||
|
required this.messagesProvider,
|
||||||
|
required this.contactsProvider,
|
||||||
|
required this.onNavigateToMessages,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final channelIdx = channel.publicKey.length > 1 ? channel.publicKey[1] : 0;
|
||||||
|
final channelMessages = messagesProvider.getMessagesForChannel(channelIdx)
|
||||||
|
..sort((a, b) => b.sentAt.compareTo(a.sentAt));
|
||||||
|
final participantNames = <String>[];
|
||||||
|
for (final message in channelMessages) {
|
||||||
|
final senderName = message.senderName?.trim();
|
||||||
|
if (senderName == null || senderName.isEmpty) continue;
|
||||||
|
if (!participantNames.contains(senderName)) {
|
||||||
|
participantNames.add(senderName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
colorScheme.surfaceContainerLow,
|
||||||
|
colorScheme.tertiaryContainer.withValues(alpha: 0.45),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: colorScheme.outlineVariant.withValues(alpha: 0.35),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
onTap: () async {
|
||||||
|
await MessageDestinationPreferences.setDestination(
|
||||||
|
MessageDestinationPreferences.destinationTypeChannel,
|
||||||
|
recipientPublicKey: channel.publicKeyHex,
|
||||||
|
);
|
||||||
|
messagesProvider.navigateToDestination(
|
||||||
|
MessageDestinationPreferences.destinationTypeChannel,
|
||||||
|
recipientPublicKeyHex: channel.publicKeyHex,
|
||||||
|
);
|
||||||
|
onNavigateToMessages?.call();
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
ContactAvatar(contact: channel, radius: 20),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
channel.displayName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium
|
||||||
|
?.copyWith(
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 15,
|
||||||
|
height: 1.05,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (participantNames.isNotEmpty)
|
||||||
|
_ExpandableParticipantStack(
|
||||||
|
names: participantNames,
|
||||||
|
contactForName: _findParticipantContact,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
'No recent chatters',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall
|
||||||
|
?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: [
|
||||||
|
_MetricChip(
|
||||||
|
icon: Icons.forum_outlined,
|
||||||
|
label: '${channelMessages.length}',
|
||||||
|
helper: 'messages',
|
||||||
|
),
|
||||||
|
_MetricChip(
|
||||||
|
icon: Icons.group_outlined,
|
||||||
|
label: '${participantNames.length}',
|
||||||
|
helper: 'active',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Contact? _findParticipantContact(String name) {
|
||||||
|
for (final contact in contactsProvider.contacts) {
|
||||||
|
if (!contact.isChannel && contact.advName == name) {
|
||||||
|
return contact;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExpandableParticipantStack extends StatefulWidget {
|
||||||
|
final List<String> names;
|
||||||
|
final Contact? Function(String name) contactForName;
|
||||||
|
|
||||||
|
const _ExpandableParticipantStack({
|
||||||
|
required this.names,
|
||||||
|
required this.contactForName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ExpandableParticipantStack> createState() =>
|
||||||
|
_ExpandableParticipantStackState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExpandableParticipantStackState
|
||||||
|
extends State<_ExpandableParticipantStack> {
|
||||||
|
bool _expanded = false;
|
||||||
|
static const int _collapsedVisibleCount = 4;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final hasOverflow = widget.names.length > _collapsedVisibleCount;
|
||||||
|
final visibleNames = _expanded
|
||||||
|
? widget.names
|
||||||
|
: widget.names.take(_collapsedVisibleCount).toList();
|
||||||
|
final overflowCount = _expanded
|
||||||
|
? 0
|
||||||
|
: widget.names.length - visibleNames.length;
|
||||||
|
final spacing = _expanded ? 20.0 : 16.0;
|
||||||
|
const avatarSize = 24.0;
|
||||||
|
final itemCount = visibleNames.length + (overflowCount > 0 ? 1 : 0);
|
||||||
|
final width = itemCount == 0 ? 0.0 : avatarSize + (itemCount - 1) * spacing;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: hasOverflow
|
||||||
|
? () {
|
||||||
|
setState(() {
|
||||||
|
_expanded = !_expanded;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 180),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
width: width,
|
||||||
|
height: avatarSize,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < visibleNames.length; i++)
|
||||||
|
Positioned(
|
||||||
|
left: i * spacing,
|
||||||
|
top: 0,
|
||||||
|
child: _ParticipantAvatar(
|
||||||
|
name: visibleNames[i],
|
||||||
|
contact: widget.contactForName(visibleNames[i]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (overflowCount > 0)
|
||||||
|
Positioned(
|
||||||
|
left: visibleNames.length * spacing,
|
||||||
|
top: 0,
|
||||||
|
child: _OverflowAvatar(count: overflowCount),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OverflowAvatar extends StatelessWidget {
|
||||||
|
final int count;
|
||||||
|
|
||||||
|
const _OverflowAvatar({required this.count});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
return Container(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerHighest,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: colorScheme.surface, width: 2),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
'+$count',
|
||||||
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
fontSize: 9,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ParticipantAvatar extends StatelessWidget {
|
||||||
|
final String name;
|
||||||
|
final Contact? contact;
|
||||||
|
static const double _size = 24;
|
||||||
|
|
||||||
|
const _ParticipantAvatar({required this.name, required this.contact});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (contact != null) {
|
||||||
|
final surfaceColor = Theme.of(context).colorScheme.surface;
|
||||||
|
return SizedBox(
|
||||||
|
width: _size,
|
||||||
|
height: _size,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: surfaceColor, width: 2),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(2),
|
||||||
|
child: ClipOval(child: ContactAvatar(contact: contact!, radius: 8)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
return Container(
|
||||||
|
width: _size,
|
||||||
|
height: _size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.tertiaryContainer,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: colorScheme.surface, width: 2),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Text(
|
||||||
|
AvatarLabelHelper.buildLabel(name),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: colorScheme.onTertiaryContainer,
|
||||||
|
fontSize: 9,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MetricChip extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
final String helper;
|
||||||
|
|
||||||
|
const _MetricChip({
|
||||||
|
required this.icon,
|
||||||
|
required this.label,
|
||||||
|
required this.helper,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surface.withValues(alpha: 0.72),
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 14, color: colorScheme.onSurfaceVariant),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
helper,
|
||||||
|
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import '../l10n/app_localizations.dart';
|
|||||||
import '../widgets/permission_request_dialog.dart';
|
import '../widgets/permission_request_dialog.dart';
|
||||||
import '../widgets/connection_dialog.dart';
|
import '../widgets/connection_dialog.dart';
|
||||||
import '../utils/battery_display_helper.dart';
|
import '../utils/battery_display_helper.dart';
|
||||||
|
import '../services/developer_mode_service.dart';
|
||||||
|
import '../services/mesh_map_nodes_service.dart';
|
||||||
|
|
||||||
enum _HomeTab { messages, contacts, sensors, map }
|
enum _HomeTab { messages, contacts, sensors, map }
|
||||||
|
|
||||||
@@ -55,6 +57,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
bool _isMapFullscreen = false;
|
bool _isMapFullscreen = false;
|
||||||
bool _showRxTxIndicators = true;
|
bool _showRxTxIndicators = true;
|
||||||
|
bool _isDeveloperModeEnabled = false;
|
||||||
bool _isMapEnabled = true;
|
bool _isMapEnabled = true;
|
||||||
bool _isContactsEnabled = true;
|
bool _isContactsEnabled = true;
|
||||||
bool _isSensorsEnabled = false;
|
bool _isSensorsEnabled = false;
|
||||||
@@ -90,6 +93,8 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
// Initialize synchronously so first build always has a valid controller.
|
// Initialize synchronously so first build always has a valid controller.
|
||||||
_initTabController();
|
_initTabController();
|
||||||
_loadRxTxPreference();
|
_loadRxTxPreference();
|
||||||
|
_loadDeveloperModePreference();
|
||||||
|
MeshMapNodesService.syncInBackgroundIfStale();
|
||||||
|
|
||||||
// Show permission dialog after the first frame if needed
|
// Show permission dialog after the first frame if needed
|
||||||
if (widget.shouldShowPermissionDialog) {
|
if (widget.shouldShowPermissionDialog) {
|
||||||
@@ -192,7 +197,6 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
|
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case _HomeTab.messages:
|
case _HomeTab.messages:
|
||||||
context.read<MessagesProvider>().markAllAsRead();
|
|
||||||
break;
|
break;
|
||||||
case _HomeTab.contacts:
|
case _HomeTab.contacts:
|
||||||
context.read<ContactsProvider>().markAllAsViewed();
|
context.read<ContactsProvider>().markAllAsViewed();
|
||||||
@@ -217,6 +221,9 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
_lifecycleState = state;
|
_lifecycleState = state;
|
||||||
_syncFastLocationUiState();
|
_syncFastLocationUiState();
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
MeshMapNodesService.syncInBackgroundIfStale();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadRxTxPreference() async {
|
Future<void> _loadRxTxPreference() async {
|
||||||
@@ -228,6 +235,14 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadDeveloperModePreference() async {
|
||||||
|
final isEnabled = await DeveloperModeService.isEnabled();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isDeveloperModeEnabled = isEnabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
@@ -378,10 +393,14 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
required int count,
|
required int count,
|
||||||
required bool isActive,
|
required bool isActive,
|
||||||
required Color activeColor,
|
required Color activeColor,
|
||||||
|
bool compact = false,
|
||||||
}) {
|
}) {
|
||||||
final color = isActive ? activeColor : Colors.grey;
|
final color = isActive ? activeColor : Colors.grey;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: compact ? 7 : 8,
|
||||||
|
vertical: compact ? 4 : 5,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withValues(alpha: 0.12),
|
color: color.withValues(alpha: 0.12),
|
||||||
borderRadius: BorderRadius.circular(999),
|
borderRadius: BorderRadius.circular(999),
|
||||||
@@ -390,15 +409,15 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 7,
|
width: compact ? 6 : 7,
|
||||||
height: 7,
|
height: compact ? 6 : 7,
|
||||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
SizedBox(width: compact ? 5 : 6),
|
||||||
Text(
|
Text(
|
||||||
'$label:$count',
|
'$label:$count',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: compact ? 10 : 11,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: color,
|
color: color,
|
||||||
),
|
),
|
||||||
@@ -598,7 +617,11 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
),
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) {
|
||||||
|
final items = <PopupMenuEntry<void>>[];
|
||||||
|
|
||||||
|
if (_isDeveloperModeEnabled) {
|
||||||
|
items.add(
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -613,12 +636,17 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
navigator.push(
|
navigator.push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const SpectrumScanScreen(),
|
builder: (context) =>
|
||||||
|
const SpectrumScanScreen(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.add(
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -628,7 +656,6 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Capture context-dependent objects before async gap
|
|
||||||
final navigator = Navigator.of(context);
|
final navigator = Navigator.of(context);
|
||||||
Future.delayed(Duration.zero, () async {
|
Future.delayed(Duration.zero, () async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -642,12 +669,15 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// Reload preference when returning from settings
|
|
||||||
_loadRxTxPreference();
|
_loadRxTxPreference();
|
||||||
|
_loadDeveloperModePreference();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -666,6 +696,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
onNavigateToMap: _isMapEnabled
|
onNavigateToMap: _isMapEnabled
|
||||||
? () => _navigateToTab(_HomeTab.map)
|
? () => _navigateToTab(_HomeTab.map)
|
||||||
: null,
|
: null,
|
||||||
|
onNavigateToMessages: () => _navigateToTab(_HomeTab.messages),
|
||||||
);
|
);
|
||||||
case _HomeTab.sensors:
|
case _HomeTab.sensors:
|
||||||
return const SensorsTab();
|
return const SensorsTab();
|
||||||
@@ -1014,17 +1045,19 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
txActive: provider.txActivity,
|
txActive: provider.txActivity,
|
||||||
)
|
)
|
||||||
: Container(
|
: Container(
|
||||||
|
constraints: const BoxConstraints(minHeight: 48),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 10,
|
horizontal: 8,
|
||||||
vertical: 8,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surfaceContainerHigh
|
color: theme.colorScheme.surfaceContainerHigh
|
||||||
.withValues(alpha: 0.85),
|
.withValues(alpha: 0.85),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(22),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
_buildActivityBadge(
|
_buildActivityBadge(
|
||||||
@@ -1032,13 +1065,15 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
count: provider.rxPacketCount,
|
count: provider.rxPacketCount,
|
||||||
isActive: provider.rxActivity,
|
isActive: provider.rxActivity,
|
||||||
activeColor: Colors.green,
|
activeColor: Colors.green,
|
||||||
|
compact: true,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 4),
|
||||||
_buildActivityBadge(
|
_buildActivityBadge(
|
||||||
label: 'TX',
|
label: 'TX',
|
||||||
count: provider.txPacketCount,
|
count: provider.txPacketCount,
|
||||||
isActive: provider.txActivity,
|
isActive: provider.txActivity,
|
||||||
activeColor: Colors.blue,
|
activeColor: Colors.blue,
|
||||||
|
compact: true,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -89,9 +89,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
// Load saved message destination
|
// Load saved message destination
|
||||||
_loadSavedDestination();
|
_loadSavedDestination();
|
||||||
_loadVoiceBitrate();
|
_loadVoiceBitrate();
|
||||||
// Mark all messages as read when tab is opened
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.read<MessagesProvider>().markAllAsRead();
|
|
||||||
_checkForNavigationRequest();
|
_checkForNavigationRequest();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -128,11 +126,20 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
void _checkForNavigationRequest() {
|
void _checkForNavigationRequest() {
|
||||||
final messagesProvider = context.read<MessagesProvider>();
|
final messagesProvider = context.read<MessagesProvider>();
|
||||||
final targetMessageId = messagesProvider.targetMessageId;
|
final targetMessageId = messagesProvider.targetMessageId;
|
||||||
|
final targetDestinationType = messagesProvider.targetDestinationType;
|
||||||
|
|
||||||
if (targetMessageId != null) {
|
if (targetMessageId != null) {
|
||||||
_scrollToMessage(targetMessageId);
|
_scrollToMessage(targetMessageId);
|
||||||
messagesProvider.clearMessageNavigation();
|
messagesProvider.clearMessageNavigation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targetDestinationType != null) {
|
||||||
|
_applyPendingDestination(
|
||||||
|
type: targetDestinationType,
|
||||||
|
recipientPublicKeyHex: messagesProvider.targetRecipientPublicKeyHex,
|
||||||
|
);
|
||||||
|
messagesProvider.clearDestinationNavigation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scrollToMessage(String messageId) {
|
void _scrollToMessage(String messageId) {
|
||||||
@@ -258,6 +265,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
/// Show recipient selector bottom sheet
|
/// Show recipient selector bottom sheet
|
||||||
void _showRecipientSelector() {
|
void _showRecipientSelector() {
|
||||||
final contactsProvider = context.read<ContactsProvider>();
|
final contactsProvider = context.read<ContactsProvider>();
|
||||||
|
final messagesProvider = context.read<MessagesProvider>();
|
||||||
|
|
||||||
// Filter contacts by type
|
// Filter contacts by type
|
||||||
final contacts = contactsProvider.contacts
|
final contacts = contactsProvider.contacts
|
||||||
@@ -278,6 +286,13 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
contacts: contacts,
|
contacts: contacts,
|
||||||
rooms: rooms,
|
rooms: rooms,
|
||||||
channels: channels,
|
channels: channels,
|
||||||
|
unreadCount: messagesProvider.unreadCount,
|
||||||
|
unreadCountsByPublicKey: {
|
||||||
|
for (final contact in [...contacts, ...rooms, ...channels])
|
||||||
|
contact.publicKeyHex: messagesProvider.getUnreadCountForDestination(
|
||||||
|
contact,
|
||||||
|
),
|
||||||
|
},
|
||||||
currentDestinationType: _destinationType,
|
currentDestinationType: _destinationType,
|
||||||
currentRecipientPublicKey: _selectedRecipient?.publicKeyHex,
|
currentRecipientPublicKey: _selectedRecipient?.publicKeyHex,
|
||||||
onSelect: _onRecipientSelected,
|
onSelect: _onRecipientSelected,
|
||||||
@@ -292,6 +307,8 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
_selectedRecipient = recipient;
|
_selectedRecipient = recipient;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_markCurrentDestinationAsRead();
|
||||||
|
|
||||||
_enforceMessageByteLimit();
|
_enforceMessageByteLimit();
|
||||||
|
|
||||||
// Save to preferences
|
// Save to preferences
|
||||||
@@ -304,9 +321,73 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
if (!mounted) return;
|
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
|
/// Get icon for current destination type
|
||||||
IconData _getDestinationIcon() {
|
IconData _getDestinationIcon() {
|
||||||
if (_destinationType ==
|
if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
|
||||||
|
return Icons.all_inbox;
|
||||||
|
} else if (_destinationType ==
|
||||||
MessageDestinationPreferences.destinationTypeChannel) {
|
MessageDestinationPreferences.destinationTypeChannel) {
|
||||||
return Icons.public;
|
return Icons.public;
|
||||||
} else if (_destinationType ==
|
} else if (_destinationType ==
|
||||||
@@ -318,6 +399,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _getDestinationLabel() {
|
String _getDestinationLabel() {
|
||||||
|
if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
|
||||||
|
return AppLocalizations.of(context)!.showAll;
|
||||||
|
}
|
||||||
if (_destinationType ==
|
if (_destinationType ==
|
||||||
MessageDestinationPreferences.destinationTypeChannel &&
|
MessageDestinationPreferences.destinationTypeChannel &&
|
||||||
_selectedRecipient != null) {
|
_selectedRecipient != null) {
|
||||||
@@ -343,6 +427,12 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ToastLogger.error(context, 'Select a channel, contact, or room first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check destination type and send accordingly
|
// Check destination type and send accordingly
|
||||||
if (_destinationType ==
|
if (_destinationType ==
|
||||||
@@ -375,6 +465,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
|
|
||||||
_textController.clear();
|
_textController.clear();
|
||||||
_focusNode.unfocus();
|
_focusNode.unfocus();
|
||||||
|
_markCurrentDestinationAsRead();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1107,6 +1198,15 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.search),
|
||||||
|
title: const Text('Search messages'),
|
||||||
|
onTap: () async {
|
||||||
|
await _runAfterSheetDismissal(sheetContext, () async {
|
||||||
|
_showFilteredMessageSearch();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.add_location_alt),
|
leading: const Icon(Icons.add_location_alt),
|
||||||
title: Text(AppLocalizations.of(context)!.sendSarMarker),
|
title: Text(AppLocalizations.of(context)!.sendSarMarker),
|
||||||
@@ -1174,6 +1274,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) {
|
Widget _buildDestinationAvatar(BuildContext context) {
|
||||||
final recipient = _selectedRecipient;
|
final recipient = _selectedRecipient;
|
||||||
if (recipient != null) {
|
if (recipient != null) {
|
||||||
@@ -1432,6 +1642,13 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _markCurrentDestinationAsRead() {
|
||||||
|
context.read<MessagesProvider>().markDestinationAsRead(
|
||||||
|
destinationType: _destinationType,
|
||||||
|
contact: _selectedRecipient,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
List<Message> _getFilteredMessages(MessagesProvider messagesProvider) {
|
List<Message> _getFilteredMessages(MessagesProvider messagesProvider) {
|
||||||
// Get all recent messages
|
// Get all recent messages
|
||||||
final allMessages = messagesProvider.getRecentMessages(count: 100);
|
final allMessages = messagesProvider.getRecentMessages(count: 100);
|
||||||
@@ -1439,18 +1656,19 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
List<Message> filteredMessages;
|
List<Message> filteredMessages;
|
||||||
|
|
||||||
// If channel destination is selected, filter by selected channel.
|
// If channel destination is selected, filter by selected channel.
|
||||||
if (_destinationType ==
|
if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
|
||||||
|
filteredMessages = allMessages;
|
||||||
|
} else if (_destinationType ==
|
||||||
MessageDestinationPreferences.destinationTypeChannel) {
|
MessageDestinationPreferences.destinationTypeChannel) {
|
||||||
final selectedChannelIdx = _selectedRecipient?.publicKey[1] ?? 0;
|
final selectedChannelIdx = _selectedRecipient?.publicKey[1] ?? 0;
|
||||||
if (selectedChannelIdx == 0) {
|
|
||||||
// Public channel view keeps showing all messages (current app behavior).
|
|
||||||
filteredMessages = allMessages;
|
|
||||||
} else {
|
|
||||||
filteredMessages = allMessages
|
filteredMessages = allMessages
|
||||||
.where((message) => message.channelIdx == selectedChannelIdx)
|
.where(
|
||||||
|
(message) =>
|
||||||
|
message.isChannelMessage &&
|
||||||
|
(message.channelIdx ?? 0) == selectedChannelIdx,
|
||||||
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// If a contact or room is selected, filter by recipient/sender prefixes.
|
// If a contact or room is selected, filter by recipient/sender prefixes.
|
||||||
else if ((_destinationType ==
|
else if ((_destinationType ==
|
||||||
MessageDestinationPreferences.destinationTypeContact ||
|
MessageDestinationPreferences.destinationTypeContact ||
|
||||||
@@ -1458,6 +1676,10 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
MessageDestinationPreferences.destinationTypeRoom) &&
|
MessageDestinationPreferences.destinationTypeRoom) &&
|
||||||
_selectedRecipient != null) {
|
_selectedRecipient != null) {
|
||||||
filteredMessages = allMessages.where((message) {
|
filteredMessages = allMessages.where((message) {
|
||||||
|
if (!message.isContactMessage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Include messages sent TO this recipient
|
// Include messages sent TO this recipient
|
||||||
if (message.recipientPublicKey != null &&
|
if (message.recipientPublicKey != null &&
|
||||||
message.recipientPublicKey!.length >= 6 &&
|
message.recipientPublicKey!.length >= 6 &&
|
||||||
@@ -1538,6 +1760,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
onRefresh: _handleRefresh,
|
onRefresh: _handleRefresh,
|
||||||
onNavigateToMap: widget.onNavigateToMap,
|
onNavigateToMap: widget.onNavigateToMap,
|
||||||
onMessageTap: _handleMessageTap,
|
onMessageTap: _handleMessageTap,
|
||||||
|
onReplyToMessage: _replyToMessage,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import 'package:meshcore_client/meshcore_client.dart';
|
|||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
import '../providers/connection_provider.dart';
|
import '../providers/connection_provider.dart';
|
||||||
import '../providers/contacts_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';
|
import '../utils/log_rx_route_decoder.dart';
|
||||||
|
|
||||||
class PacketLogScreen extends StatefulWidget {
|
class PacketLogScreen extends StatefulWidget {
|
||||||
@@ -460,26 +462,6 @@ class _PacketLogCard extends StatelessWidget {
|
|||||||
final isRx = log.direction == PacketDirection.rx;
|
final isRx = log.direction == PacketDirection.rx;
|
||||||
final directionColor = isRx ? Colors.green : Colors.blue;
|
final directionColor = isRx ? Colors.green : Colors.blue;
|
||||||
final rxInfo = log.logRxDataInfo;
|
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(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
@@ -604,13 +586,9 @@ class _PacketLogCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (isRx && decodedRoute != null) ...[
|
if (isRx) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_RouteSection(
|
_DecodedRouteSection(log: log),
|
||||||
route: decodedRoute,
|
|
||||||
path: resolvedPath ?? const [],
|
|
||||||
originalSender: originalSender,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Container(
|
Container(
|
||||||
@@ -755,6 +733,97 @@ 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<List<dynamic>>(
|
||||||
|
future: Future.wait<dynamic>([
|
||||||
|
RouteHashPreferences.getHashSize(),
|
||||||
|
MeshMapNodesService.loadCachedNodes(
|
||||||
|
cacheTtl: MeshMapNodesService.traceCacheTtl,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final decodedRoute = LogRxRouteDecoder.decode(
|
||||||
|
log.rawData,
|
||||||
|
preferredHashSize: snapshot.data?.first as int?,
|
||||||
|
);
|
||||||
|
if (decodedRoute == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final cachedNodes = snapshot.hasData
|
||||||
|
? snapshot.data![1] as List<MeshMapNode>
|
||||||
|
: const <MeshMapNode>[];
|
||||||
|
final resolvedPath = decodedRoute.hopHashes
|
||||||
|
.map(
|
||||||
|
(hashHex) => _resolveHashWithFallback(
|
||||||
|
hashHex,
|
||||||
|
contacts: contacts,
|
||||||
|
cachedNodes: cachedNodes,
|
||||||
|
ownPublicKey: ownPublicKey,
|
||||||
|
ownName: ownName,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
final originalSender = resolvedPath.isEmpty ? null : resolvedPath.first;
|
||||||
|
|
||||||
|
return _RouteSection(
|
||||||
|
route: decodedRoute,
|
||||||
|
path: resolvedPath,
|
||||||
|
originalSender: originalSender,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ResolvedNodeHash _resolveHashWithFallback(
|
||||||
|
String hashHex, {
|
||||||
|
required List<Contact> contacts,
|
||||||
|
required List<MeshMapNode> cachedNodes,
|
||||||
|
required Uint8List? ownPublicKey,
|
||||||
|
required String? ownName,
|
||||||
|
}) {
|
||||||
|
final localResolved = LogRxRouteDecoder.resolveHash(
|
||||||
|
hashHex,
|
||||||
|
contacts: contacts,
|
||||||
|
ownPublicKey: ownPublicKey,
|
||||||
|
ownName: ownName,
|
||||||
|
);
|
||||||
|
if (localResolved.matchCount > 0 || localResolved.isOwnNode) {
|
||||||
|
return localResolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalizedHashHex = hashHex.toLowerCase();
|
||||||
|
final matches = cachedNodes
|
||||||
|
.where((node) => node.publicKey.startsWith(normalizedHashHex))
|
||||||
|
.toList();
|
||||||
|
if (matches.isEmpty) {
|
||||||
|
return localResolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
matches.sort((a, b) => b.updatedAtMs.compareTo(a.updatedAtMs));
|
||||||
|
return ResolvedNodeHash(
|
||||||
|
hashHex: normalizedHashHex,
|
||||||
|
label: matches.first.name,
|
||||||
|
isOwnNode: false,
|
||||||
|
isUniqueMatch: matches.length == 1,
|
||||||
|
matchCount: matches.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _RouteSection extends StatelessWidget {
|
class _RouteSection extends StatelessWidget {
|
||||||
final DecodedLogRxRoute route;
|
final DecodedLogRxRoute route;
|
||||||
final List<ResolvedNodeHash> path;
|
final List<ResolvedNodeHash> path;
|
||||||
@@ -802,7 +871,13 @@ class _RouteSection extends StatelessWidget {
|
|||||||
_FactCard(
|
_FactCard(
|
||||||
icon: Icons.hub,
|
icon: Icons.hub,
|
||||||
label: 'Hops',
|
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)
|
if (originalSender != null)
|
||||||
_FactCard(
|
_FactCard(
|
||||||
|
|||||||
@@ -371,6 +371,7 @@ class _SensorCard extends StatelessWidget {
|
|||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final telemetry = contact?.telemetry;
|
final telemetry = contact?.telemetry;
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
final metrics = contact == null || telemetry == null
|
final metrics = contact == null || telemetry == null
|
||||||
? const <_MetricCardData>[]
|
? const <_MetricCardData>[]
|
||||||
: _buildMetricCards(l10n, telemetry, contact!);
|
: _buildMetricCards(l10n, telemetry, contact!);
|
||||||
@@ -378,16 +379,23 @@ class _SensorCard extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surface,
|
|
||||||
borderRadius: BorderRadius.circular(28),
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
colorScheme.surfaceContainerLow,
|
||||||
|
colorScheme.surfaceContainerHighest.withValues(alpha: 0.9),
|
||||||
|
],
|
||||||
|
),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: theme.colorScheme.outline.withValues(alpha: 0.14),
|
color: colorScheme.outlineVariant.withValues(alpha: 0.35),
|
||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.04),
|
color: Colors.black.withValues(alpha: 0.045),
|
||||||
blurRadius: 18,
|
blurRadius: 12,
|
||||||
offset: const Offset(0, 8),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io' show Platform;
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
import 'package:flutter/foundation.dart'
|
||||||
|
show TargetPlatform, defaultTargetPlatform, kIsWeb;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_avif/flutter_avif.dart';
|
import 'package:flutter_avif/flutter_avif.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
@@ -15,12 +16,14 @@ import '../providers/app_provider.dart';
|
|||||||
import '../providers/connection_provider.dart';
|
import '../providers/connection_provider.dart';
|
||||||
import '../services/location_tracking_service.dart';
|
import '../services/location_tracking_service.dart';
|
||||||
import '../services/locale_preferences.dart';
|
import '../services/locale_preferences.dart';
|
||||||
|
import '../services/mesh_map_nodes_service.dart';
|
||||||
import '../services/update_checker_service.dart';
|
import '../services/update_checker_service.dart';
|
||||||
import '../services/voice_codec_service.dart';
|
import '../services/voice_codec_service.dart';
|
||||||
import '../services/voice_bitrate_preferences.dart';
|
import '../services/voice_bitrate_preferences.dart';
|
||||||
import '../services/image_preferences.dart';
|
import '../services/image_preferences.dart';
|
||||||
import '../services/route_hash_preferences.dart';
|
import '../services/route_hash_preferences.dart';
|
||||||
import '../services/image_codec_service.dart';
|
import '../services/image_codec_service.dart';
|
||||||
|
import '../services/developer_mode_service.dart';
|
||||||
import '../utils/sample_data_generator.dart';
|
import '../utils/sample_data_generator.dart';
|
||||||
import '../utils/image_message_parser.dart';
|
import '../utils/image_message_parser.dart';
|
||||||
import '../utils/voice_message_parser.dart';
|
import '../utils/voice_message_parser.dart';
|
||||||
@@ -70,6 +73,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
bool _fastLocationUpdatesEnabled = false;
|
bool _fastLocationUpdatesEnabled = false;
|
||||||
double _fastLocationMovementThresholdMeters = 10.0;
|
double _fastLocationMovementThresholdMeters = 10.0;
|
||||||
int _fastLocationActiveCadenceSeconds = 10;
|
int _fastLocationActiveCadenceSeconds = 10;
|
||||||
|
bool _isDeveloperModeEnabled = false;
|
||||||
|
DateTime? _onlineTraceCacheUpdatedAt;
|
||||||
|
bool _isClearingOnlineTraceCache = false;
|
||||||
|
int _versionTapCount = 0;
|
||||||
final ImagePicker _imagePicker = ImagePicker();
|
final ImagePicker _imagePicker = ImagePicker();
|
||||||
final LocationTrackingService _locationService = LocationTrackingService();
|
final LocationTrackingService _locationService = LocationTrackingService();
|
||||||
|
|
||||||
@@ -85,6 +92,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_loadRouteHashSizePreference();
|
_loadRouteHashSizePreference();
|
||||||
_loadImagePreferences();
|
_loadImagePreferences();
|
||||||
_loadFastLocationSettings();
|
_loadFastLocationSettings();
|
||||||
|
_loadDeveloperMode();
|
||||||
|
_loadOnlineTraceCacheStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -114,6 +123,56 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadDeveloperMode() async {
|
||||||
|
final isEnabled = await DeveloperModeService.isEnabled();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_isDeveloperModeEnabled = isEnabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadOnlineTraceCacheStatus() async {
|
||||||
|
final cachedAt = await MeshMapNodesService.cachedAt();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_onlineTraceCacheUpdatedAt = cachedAt;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
Future<void> _saveRxTxPreference(bool value) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setBool('show_rx_tx_indicators', value);
|
await prefs.setBool('show_rx_tx_indicators', value);
|
||||||
@@ -424,7 +483,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
/// Check for app updates and show notification or dialog
|
/// Check for app updates and show notification or dialog
|
||||||
Future<void> _checkForUpdates() async {
|
Future<void> _checkForUpdates() async {
|
||||||
// Only on Android
|
// Only on Android
|
||||||
if (!Platform.isAndroid) {
|
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
@@ -783,6 +842,66 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _clearOnlineTraceCache() async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Clear online trace database'),
|
||||||
|
content: const Text(
|
||||||
|
'This removes the cached online node database used as a trace fallback.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: Text(AppLocalizations.of(context)!.cancel),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
|
child: Text(AppLocalizations.of(context)!.clear),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed != true || !mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isClearingOnlineTraceCache = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
await MeshMapNodesService.clearCache();
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_onlineTraceCacheUpdatedAt = null;
|
||||||
|
_isClearingOnlineTraceCache = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Online trace database cleared'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _onlineTraceCacheSubtitle() {
|
||||||
|
final cachedAt = _onlineTraceCacheUpdatedAt;
|
||||||
|
if (cachedAt == null) {
|
||||||
|
return 'No cached online database. Refresh runs in background when internet is available.';
|
||||||
|
}
|
||||||
|
|
||||||
|
final expiresAt = cachedAt.add(MeshMapNodesService.traceCacheTtl);
|
||||||
|
return 'Last synced ${_formatDateTime(cachedAt)}. Cached for 24 hours until ${_formatDateTime(expiresAt)}.';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDateTime(DateTime value) {
|
||||||
|
final local = value.toLocal();
|
||||||
|
String two(int part) => part.toString().padLeft(2, '0');
|
||||||
|
return '${local.year}-${two(local.month)}-${two(local.day)} ${two(local.hour)}:${two(local.minute)}';
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _showRouteHashSizeDialog() async {
|
Future<void> _showRouteHashSizeDialog() async {
|
||||||
final selected = await showDialog<int>(
|
final selected = await showDialog<int>(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -929,6 +1048,45 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: _showRouteHashSizeDialog,
|
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(
|
ListTile(
|
||||||
leading: const Icon(Icons.delete_sweep, color: Colors.red),
|
leading: const Icon(Icons.delete_sweep, color: Colors.red),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
@@ -938,6 +1096,63 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
subtitle: const Text('Delete all stored message history'),
|
subtitle: const Text('Delete all stored message history'),
|
||||||
onTap: _clearMessages,
|
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('Tracing'),
|
||||||
|
_buildSettingsCard([
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.cloud_sync),
|
||||||
|
title: const Text('Online trace database'),
|
||||||
|
subtitle: Text(_onlineTraceCacheSubtitle()),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.delete_sweep,
|
||||||
|
color: _isClearingOnlineTraceCache ? null : Colors.red,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'Clear online trace database',
|
||||||
|
style: TextStyle(
|
||||||
|
color: _isClearingOnlineTraceCache ? null : Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Remove the 24-hour cached fallback used when local route matches are incomplete',
|
||||||
|
),
|
||||||
|
enabled: !_isClearingOnlineTraceCache,
|
||||||
|
trailing: _isClearingOnlineTraceCache
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onTap: _isClearingOnlineTraceCache
|
||||||
|
? null
|
||||||
|
: _clearOnlineTraceCache,
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
_buildSectionHeader('Voice'),
|
_buildSectionHeader('Voice'),
|
||||||
@@ -1106,8 +1321,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
if (!kIsWeb) ...[
|
||||||
_buildSectionHeader('Network Sharing'),
|
_buildSectionHeader('Network Sharing'),
|
||||||
const ConnectionModeSelector(),
|
const ConnectionModeSelector(),
|
||||||
|
],
|
||||||
|
|
||||||
_buildSectionHeader(AppLocalizations.of(context)!.permissionsSection),
|
_buildSectionHeader(AppLocalizations.of(context)!.permissionsSection),
|
||||||
_buildSettingsCard([
|
_buildSettingsCard([
|
||||||
@@ -1197,6 +1414,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
? '${_packageInfo!.version} (${_packageInfo!.buildNumber})'
|
? '${_packageInfo!.version} (${_packageInfo!.buildNumber})'
|
||||||
: 'Loading...',
|
: 'Loading...',
|
||||||
),
|
),
|
||||||
|
onTap: _handleVersionTap,
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.badge),
|
leading: const Icon(Icons.badge),
|
||||||
@@ -1212,7 +1430,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
onTap: () => _showAboutDialog(),
|
onTap: () => _showAboutDialog(),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
if (Platform.isAndroid)
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:io' show Platform;
|
import 'package:flutter/foundation.dart' show TargetPlatform, defaultTargetPlatform, debugPrint, kIsWeb;
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
|
|
||||||
/// Service for accessing build information from native platform code
|
/// Service for accessing build information from native platform code
|
||||||
/// Currently supports Android only - returns "unknown" for other platforms
|
/// Currently supports Android only - returns "unknown" for other platforms
|
||||||
@@ -25,7 +24,7 @@ class BuildInfoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only Android has the platform channel implementation
|
// Only Android has the platform channel implementation
|
||||||
if (!Platform.isAndroid) {
|
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
|
||||||
debugPrint('[BuildInfoService] Not on Android platform, returning "unknown"');
|
debugPrint('[BuildInfoService] Not on Android platform, returning "unknown"');
|
||||||
_cachedCommitHash = 'unknown';
|
_cachedCommitHash = 'unknown';
|
||||||
return _cachedCommitHash!;
|
return _cachedCommitHash!;
|
||||||
|
|||||||
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('fr'), // French
|
||||||
Locale('it'), // Italian
|
Locale('it'), // Italian
|
||||||
Locale('el'), // Greek
|
Locale('el'), // Greek
|
||||||
|
Locale('pt'), // Portuguese
|
||||||
|
Locale('tr'), // Turkish
|
||||||
|
Locale('pl'), // Polish
|
||||||
|
Locale('uk'), // Ukrainian
|
||||||
Locale('ru'), // Russian
|
Locale('ru'), // Russian
|
||||||
Locale('zh'), // Chinese
|
Locale('zh'), // Chinese
|
||||||
];
|
];
|
||||||
@@ -66,6 +70,14 @@ class LocalePreferences {
|
|||||||
return 'Italiano';
|
return 'Italiano';
|
||||||
case 'el':
|
case 'el':
|
||||||
return 'Greek';
|
return 'Greek';
|
||||||
|
case 'pt':
|
||||||
|
return 'Portuguese';
|
||||||
|
case 'tr':
|
||||||
|
return 'Turkish';
|
||||||
|
case 'pl':
|
||||||
|
return 'Polish';
|
||||||
|
case 'uk':
|
||||||
|
return 'Ukrainian';
|
||||||
case 'ru':
|
case 'ru':
|
||||||
return 'Русский';
|
return 'Русский';
|
||||||
case 'zh':
|
case 'zh':
|
||||||
@@ -94,6 +106,14 @@ class LocalePreferences {
|
|||||||
return 'Italiano';
|
return 'Italiano';
|
||||||
case 'el':
|
case 'el':
|
||||||
return 'Ελληνικά';
|
return 'Ελληνικά';
|
||||||
|
case 'pt':
|
||||||
|
return 'Português';
|
||||||
|
case 'tr':
|
||||||
|
return 'Türkçe';
|
||||||
|
case 'pl':
|
||||||
|
return 'Polski';
|
||||||
|
case 'uk':
|
||||||
|
return 'Українська';
|
||||||
case 'ru':
|
case 'ru':
|
||||||
return 'Русский';
|
return 'Русский';
|
||||||
case 'zh':
|
case 'zh':
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class MeshMapNode {
|
class MeshMapNode {
|
||||||
final int type;
|
final int type;
|
||||||
@@ -33,25 +35,35 @@ class MeshMapNode {
|
|||||||
class MeshMapNodesService {
|
class MeshMapNodesService {
|
||||||
static const String _nodesEndpoint =
|
static const String _nodesEndpoint =
|
||||||
'https://api.meshcore.nz/api/v1/map/nodes';
|
'https://api.meshcore.nz/api/v1/map/nodes';
|
||||||
static const Duration _cacheTtl = Duration(minutes: 2);
|
static const Duration _cacheTtl = Duration(hours: 24);
|
||||||
static const Duration traceCacheTtl = Duration(minutes: 10);
|
static const Duration traceCacheTtl = _cacheTtl;
|
||||||
static const Duration traceTimeout = Duration(seconds: 30);
|
static const Duration traceTimeout = Duration(seconds: 30);
|
||||||
|
static const String _cacheKey = 'mesh_map_nodes_cache_v1';
|
||||||
|
static const String _cacheTimestampKey = 'mesh_map_nodes_cache_timestamp_v1';
|
||||||
static List<MeshMapNode>? _cachedNodes;
|
static List<MeshMapNode>? _cachedNodes;
|
||||||
static DateTime? _cachedAt;
|
static DateTime? _cachedAt;
|
||||||
|
static Future<void>? _ongoingSync;
|
||||||
|
|
||||||
static Future<List<MeshMapNode>> fetchNodes({
|
static Future<List<MeshMapNode>> fetchNodes({
|
||||||
bool forceRefresh = false,
|
bool forceRefresh = false,
|
||||||
Duration cacheTtl = _cacheTtl,
|
Duration cacheTtl = _cacheTtl,
|
||||||
|
bool allowNetwork = true,
|
||||||
|
http.Client? client,
|
||||||
}) async {
|
}) async {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
final cached = await loadCachedNodes(cacheTtl: cacheTtl);
|
||||||
if (!forceRefresh &&
|
if (!forceRefresh &&
|
||||||
_cachedNodes != null &&
|
cached.isNotEmpty &&
|
||||||
_cachedAt != null &&
|
_cachedAt != null &&
|
||||||
now.difference(_cachedAt!) < cacheTtl) {
|
now.difference(_cachedAt!) < cacheTtl) {
|
||||||
return _cachedNodes!;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
final response = await http
|
if (!allowNetwork) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await (client ?? http.Client())
|
||||||
.get(Uri.parse(_nodesEndpoint))
|
.get(Uri.parse(_nodesEndpoint))
|
||||||
.timeout(traceTimeout);
|
.timeout(traceTimeout);
|
||||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||||
@@ -69,8 +81,131 @@ class MeshMapNodesService {
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
_cachedNodes = nodes;
|
await _storeCache(nodes, cachedAt: now);
|
||||||
_cachedAt = now;
|
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<List<MeshMapNode>> loadCachedNodes({
|
||||||
|
Duration cacheTtl = _cacheTtl,
|
||||||
|
}) async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (_cachedNodes != null &&
|
||||||
|
_cachedAt != null &&
|
||||||
|
now.difference(_cachedAt!) < cacheTtl) {
|
||||||
|
return _cachedNodes!;
|
||||||
|
}
|
||||||
|
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final cachedAtMs = prefs.getInt(_cacheTimestampKey);
|
||||||
|
final cachedJson = prefs.getString(_cacheKey);
|
||||||
|
if (cachedAtMs == null || cachedJson == null) {
|
||||||
|
_cachedNodes = null;
|
||||||
|
_cachedAt = null;
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final cachedAt = DateTime.fromMillisecondsSinceEpoch(cachedAtMs);
|
||||||
|
if (now.difference(cachedAt) >= cacheTtl) {
|
||||||
|
_cachedNodes = null;
|
||||||
|
_cachedAt = cachedAt;
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final decoded = jsonDecode(cachedJson) as List<dynamic>;
|
||||||
|
final nodes = decoded
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.map(MeshMapNode.fromJson)
|
||||||
|
.where(
|
||||||
|
(n) => n.publicKey.isNotEmpty && n.latitude != 0 && n.longitude != 0,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
_cachedNodes = nodes;
|
||||||
|
_cachedAt = cachedAt;
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> hasFreshCache({Duration cacheTtl = _cacheTtl}) async {
|
||||||
|
final nodes = await loadCachedNodes(cacheTtl: cacheTtl);
|
||||||
|
return nodes.isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> syncInBackgroundIfStale({
|
||||||
|
Duration cacheTtl = _cacheTtl,
|
||||||
|
http.Client? client,
|
||||||
|
}) async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (_cachedAt != null && now.difference(_cachedAt!) < cacheTtl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final cached = await loadCachedNodes(cacheTtl: cacheTtl);
|
||||||
|
if (cached.isNotEmpty && _cachedAt != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_ongoingSync != null) {
|
||||||
|
return _ongoingSync!;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ongoingSync = () async {
|
||||||
|
try {
|
||||||
|
await fetchNodes(
|
||||||
|
forceRefresh: true,
|
||||||
|
cacheTtl: cacheTtl,
|
||||||
|
allowNetwork: true,
|
||||||
|
client: client,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// Background refresh is best-effort only.
|
||||||
|
} finally {
|
||||||
|
_ongoingSync = null;
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
return _ongoingSync!;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> clearCache() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove(_cacheKey);
|
||||||
|
await prefs.remove(_cacheTimestampKey);
|
||||||
|
_cachedNodes = null;
|
||||||
|
_cachedAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<DateTime?> cachedAt() async {
|
||||||
|
if (_cachedAt != null) return _cachedAt;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final cachedAtMs = prefs.getInt(_cacheTimestampKey);
|
||||||
|
if (cachedAtMs == null) return null;
|
||||||
|
_cachedAt = DateTime.fromMillisecondsSinceEpoch(cachedAtMs);
|
||||||
|
return _cachedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _storeCache(
|
||||||
|
List<MeshMapNode> nodes, {
|
||||||
|
required DateTime cachedAt,
|
||||||
|
}) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(
|
||||||
|
_cacheKey,
|
||||||
|
jsonEncode(
|
||||||
|
nodes
|
||||||
|
.map(
|
||||||
|
(node) => {
|
||||||
|
'type': node.type,
|
||||||
|
'name': node.name,
|
||||||
|
'public_key': node.publicKey,
|
||||||
|
'latitude': node.latitude,
|
||||||
|
'longitude': node.longitude,
|
||||||
|
'updated_at': node.updatedAtMs,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await prefs.setInt(_cacheTimestampKey, cachedAt.millisecondsSinceEpoch);
|
||||||
|
_cachedNodes = nodes;
|
||||||
|
_cachedAt = cachedAt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class MessageDestinationPreferences {
|
|||||||
static const String _recipientPublicKeyKey = 'message_recipient_public_key';
|
static const String _recipientPublicKeyKey = 'message_recipient_public_key';
|
||||||
|
|
||||||
/// Destination types
|
/// Destination types
|
||||||
|
static const String destinationTypeAll = 'all';
|
||||||
static const String destinationTypeChannel = 'channel';
|
static const String destinationTypeChannel = 'channel';
|
||||||
static const String destinationTypeContact = 'contact';
|
static const String destinationTypeContact = 'contact';
|
||||||
static const String destinationTypeRoom = 'room';
|
static const String destinationTypeRoom = 'room';
|
||||||
@@ -24,14 +25,11 @@ class MessageDestinationPreferences {
|
|||||||
|
|
||||||
final publicKey = prefs.getString(_recipientPublicKeyKey);
|
final publicKey = prefs.getString(_recipientPublicKeyKey);
|
||||||
|
|
||||||
return {
|
return {'type': type, 'publicKey': ?publicKey};
|
||||||
'type': type,
|
|
||||||
'publicKey': ?publicKey,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save the selected destination
|
/// 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)
|
/// [recipientPublicKey] - hex string of recipient's public key (required for contact/room)
|
||||||
static Future<void> setDestination(
|
static Future<void> setDestination(
|
||||||
String type, {
|
String type, {
|
||||||
@@ -58,6 +56,8 @@ class MessageDestinationPreferences {
|
|||||||
/// Get display name for destination type
|
/// Get display name for destination type
|
||||||
static String getDestinationTypeName(String type) {
|
static String getDestinationTypeName(String type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case destinationTypeAll:
|
||||||
|
return 'All';
|
||||||
case destinationTypeChannel:
|
case destinationTypeChannel:
|
||||||
return 'Channel';
|
return 'Channel';
|
||||||
case destinationTypeContact:
|
case destinationTypeContact:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import '../models/message.dart';
|
|||||||
import '../models/message_contact_location.dart';
|
import '../models/message_contact_location.dart';
|
||||||
import '../models/message_reception_details.dart';
|
import '../models/message_reception_details.dart';
|
||||||
import '../models/message_transfer_details.dart';
|
import '../models/message_transfer_details.dart';
|
||||||
|
import '../models/message_route_metadata.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
/// Service for persisting messages to local storage
|
/// Service for persisting messages to local storage
|
||||||
@@ -16,6 +17,8 @@ class MessageStorageService {
|
|||||||
'stored_message_reception_details';
|
'stored_message_reception_details';
|
||||||
static const String _messageTransferDetailsKey =
|
static const String _messageTransferDetailsKey =
|
||||||
'stored_message_transfer_details';
|
'stored_message_transfer_details';
|
||||||
|
static const String _messageRouteMetadataKey =
|
||||||
|
'stored_message_route_metadata';
|
||||||
static const int _maxStoredMessages = 1000; // Store up to 1000 messages
|
static const int _maxStoredMessages = 1000; // Store up to 1000 messages
|
||||||
|
|
||||||
/// Save messages to persistent storage
|
/// Save messages to persistent storage
|
||||||
@@ -24,6 +27,7 @@ class MessageStorageService {
|
|||||||
Map<String, MessageContactLocation> messageContactLocations = const {},
|
Map<String, MessageContactLocation> messageContactLocations = const {},
|
||||||
Map<String, MessageReceptionDetails> messageReceptionDetails = const {},
|
Map<String, MessageReceptionDetails> messageReceptionDetails = const {},
|
||||||
Map<String, MessageTransferDetails> messageTransferDetails = const {},
|
Map<String, MessageTransferDetails> messageTransferDetails = const {},
|
||||||
|
Map<String, MessageRouteMetadata> messageRouteMetadata = const {},
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
@@ -44,6 +48,7 @@ class MessageStorageService {
|
|||||||
final locationJson = <String, dynamic>{};
|
final locationJson = <String, dynamic>{};
|
||||||
final receptionJson = <String, dynamic>{};
|
final receptionJson = <String, dynamic>{};
|
||||||
final transferJson = <String, dynamic>{};
|
final transferJson = <String, dynamic>{};
|
||||||
|
final routeMetadataJson = <String, dynamic>{};
|
||||||
for (final entry in messageContactLocations.entries) {
|
for (final entry in messageContactLocations.entries) {
|
||||||
if (retainedMessageIds.contains(entry.key)) {
|
if (retainedMessageIds.contains(entry.key)) {
|
||||||
locationJson[entry.key] = entry.value.toJson();
|
locationJson[entry.key] = entry.value.toJson();
|
||||||
@@ -59,6 +64,11 @@ class MessageStorageService {
|
|||||||
transferJson[entry.key] = entry.value.toJson();
|
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(
|
await prefs.setString(
|
||||||
_messageContactLocationsKey,
|
_messageContactLocationsKey,
|
||||||
jsonEncode(locationJson),
|
jsonEncode(locationJson),
|
||||||
@@ -71,6 +81,10 @@ class MessageStorageService {
|
|||||||
_messageTransferDetailsKey,
|
_messageTransferDetailsKey,
|
||||||
jsonEncode(transferJson),
|
jsonEncode(transferJson),
|
||||||
);
|
);
|
||||||
|
await prefs.setString(
|
||||||
|
_messageRouteMetadataKey,
|
||||||
|
jsonEncode(routeMetadataJson),
|
||||||
|
);
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'✅ [MessageStorage] Saved ${limitedList.length} messages to storage',
|
'✅ [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
|
/// Load messages from persistent storage
|
||||||
Future<List<Message>> loadMessages() async {
|
Future<List<Message>> loadMessages() async {
|
||||||
try {
|
try {
|
||||||
@@ -206,6 +246,7 @@ class MessageStorageService {
|
|||||||
await prefs.remove(_messageContactLocationsKey);
|
await prefs.remove(_messageContactLocationsKey);
|
||||||
await prefs.remove(_messageReceptionDetailsKey);
|
await prefs.remove(_messageReceptionDetailsKey);
|
||||||
await prefs.remove(_messageTransferDetailsKey);
|
await prefs.remove(_messageTransferDetailsKey);
|
||||||
|
await prefs.remove(_messageRouteMetadataKey);
|
||||||
debugPrint('✅ [MessageStorage] Cleared all stored messages');
|
debugPrint('✅ [MessageStorage] Cleared all stored messages');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [MessageStorage] Error clearing messages: $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,6 +61,9 @@ class PacketCaptureStorageService {
|
|||||||
File? _file;
|
File? _file;
|
||||||
|
|
||||||
Future<File> _resolveFile() async {
|
Future<File> _resolveFile() async {
|
||||||
|
if (kIsWeb) {
|
||||||
|
throw UnsupportedError('Packet capture file storage is not supported on web');
|
||||||
|
}
|
||||||
if (_file != null) return _file!;
|
if (_file != null) return _file!;
|
||||||
final dir = await getApplicationSupportDirectory();
|
final dir = await getApplicationSupportDirectory();
|
||||||
final file = File('${dir.path}/$_fileName');
|
final file = File('${dir.path}/$_fileName');
|
||||||
@@ -73,6 +76,7 @@ class PacketCaptureStorageService {
|
|||||||
|
|
||||||
Future<void> appendLogs(List<BlePacketLog> logs) async {
|
Future<void> appendLogs(List<BlePacketLog> logs) async {
|
||||||
if (logs.isEmpty) return;
|
if (logs.isEmpty) return;
|
||||||
|
if (kIsWeb) return;
|
||||||
try {
|
try {
|
||||||
final file = await _resolveFile();
|
final file = await _resolveFile();
|
||||||
final sink = file.openWrite(mode: FileMode.append);
|
final sink = file.openWrite(mode: FileMode.append);
|
||||||
@@ -98,6 +102,7 @@ class PacketCaptureStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<StoredPacketCapture>> loadRecent({int limit = 500}) async {
|
Future<List<StoredPacketCapture>> loadRecent({int limit = 500}) async {
|
||||||
|
if (kIsWeb) return const [];
|
||||||
try {
|
try {
|
||||||
final file = await _resolveFile();
|
final file = await _resolveFile();
|
||||||
if (!await file.exists()) return const [];
|
if (!await file.exists()) return const [];
|
||||||
@@ -116,6 +121,7 @@ class PacketCaptureStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<int> count() async {
|
Future<int> count() async {
|
||||||
|
if (kIsWeb) return 0;
|
||||||
try {
|
try {
|
||||||
final file = await _resolveFile();
|
final file = await _resolveFile();
|
||||||
if (!await file.exists()) return 0;
|
if (!await file.exists()) return 0;
|
||||||
@@ -127,6 +133,7 @@ class PacketCaptureStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clear() async {
|
Future<void> clear() async {
|
||||||
|
if (kIsWeb) return;
|
||||||
try {
|
try {
|
||||||
final file = await _resolveFile();
|
final file = await _resolveFile();
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
|
|||||||
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();
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
class AvatarLabelHelper {
|
class AvatarLabelHelper {
|
||||||
|
static final RegExp _alnumChunks = RegExp(r'[A-Za-z0-9]+');
|
||||||
|
|
||||||
static String buildLabel(String name) {
|
static String buildLabel(String name) {
|
||||||
final trimmed = name.trim();
|
final trimmed = name.trim();
|
||||||
if (trimmed.isEmpty) return '?';
|
if (trimmed.isEmpty) return '?';
|
||||||
@@ -11,18 +13,23 @@ class AvatarLabelHelper {
|
|||||||
return '#${_take(hashBody, 2)}'.toUpperCase();
|
return '#${_take(hashBody, 2)}'.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
final parts = trimmed
|
final parts = _alnumChunks
|
||||||
.split(RegExp(r'[\s_-]+'))
|
.allMatches(trimmed)
|
||||||
|
.map((match) => match.group(0)!)
|
||||||
.where((part) => part.isNotEmpty)
|
.where((part) => part.isNotEmpty)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
if (parts.isEmpty) {
|
||||||
|
return '?';
|
||||||
|
}
|
||||||
|
|
||||||
if (parts.length >= 2) {
|
if (parts.length >= 2) {
|
||||||
final first = _take(parts[0], 1);
|
final first = _take(parts[0], 1);
|
||||||
final second = _take(parts[1], 1);
|
final second = _take(parts[1], 1);
|
||||||
return '$first$second'.toUpperCase();
|
return '$first$second'.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
return _take(trimmed, 2).toUpperCase();
|
return _take(parts.first, 2).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
static String _take(String value, int count) {
|
static String _take(String value, int count) {
|
||||||
|
|||||||
116
lib/utils/contact_grouping.dart
Normal file
116
lib/utils/contact_grouping.dart
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import '../models/contact.dart';
|
||||||
|
|
||||||
|
class InferredContactGroup {
|
||||||
|
final String key;
|
||||||
|
final String label;
|
||||||
|
final List<Contact> contacts;
|
||||||
|
|
||||||
|
const InferredContactGroup({
|
||||||
|
required this.key,
|
||||||
|
required this.label,
|
||||||
|
required this.contacts,
|
||||||
|
});
|
||||||
|
|
||||||
|
DateTime get latestSeen => contacts.first.lastSeenTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContactListItem {
|
||||||
|
final Contact? contact;
|
||||||
|
final InferredContactGroup? group;
|
||||||
|
|
||||||
|
const ContactListItem._({this.contact, this.group});
|
||||||
|
|
||||||
|
const ContactListItem.contact(Contact contact) : this._(contact: contact);
|
||||||
|
|
||||||
|
const ContactListItem.group(InferredContactGroup group)
|
||||||
|
: this._(group: group);
|
||||||
|
|
||||||
|
bool get isGroup => group != null;
|
||||||
|
|
||||||
|
DateTime get latestSeen => group?.latestSeen ?? contact!.lastSeenTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContactGrouping {
|
||||||
|
static final RegExp _prefixedNamePattern = RegExp(
|
||||||
|
r'^([A-Za-z0-9]{2,})([-_/:])',
|
||||||
|
);
|
||||||
|
|
||||||
|
static List<Contact> sortByLastSeen(List<Contact> contacts) {
|
||||||
|
return List<Contact>.from(contacts)
|
||||||
|
..sort((a, b) => b.lastSeenTime.compareTo(a.lastSeenTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<ContactListItem> buildItems(
|
||||||
|
List<Contact> contacts, {
|
||||||
|
int minGroupSize = 4,
|
||||||
|
}) {
|
||||||
|
final sortedContacts = sortByLastSeen(contacts);
|
||||||
|
return buildItemsFromSorted(sortedContacts, minGroupSize: minGroupSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<ContactListItem> buildItemsFromSorted(
|
||||||
|
List<Contact> sortedContacts, {
|
||||||
|
int minGroupSize = 4,
|
||||||
|
}) {
|
||||||
|
final groupedContacts = <String, List<Contact>>{};
|
||||||
|
final groupLabels = <String, String>{};
|
||||||
|
|
||||||
|
for (final contact in sortedContacts) {
|
||||||
|
final prefix = _extractPrefix(contact.displayName);
|
||||||
|
if (prefix == null) continue;
|
||||||
|
groupedContacts.putIfAbsent(prefix.key, () => <Contact>[]).add(contact);
|
||||||
|
groupLabels.putIfAbsent(prefix.key, () => prefix.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
final eligibleGroups = <String, InferredContactGroup>{};
|
||||||
|
for (final entry in groupedContacts.entries) {
|
||||||
|
if (entry.value.length < minGroupSize) continue;
|
||||||
|
eligibleGroups[entry.key] = InferredContactGroup(
|
||||||
|
key: entry.key,
|
||||||
|
label: groupLabels[entry.key] ?? entry.key,
|
||||||
|
contacts: entry.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final emittedGroups = <String>{};
|
||||||
|
final items = <ContactListItem>[];
|
||||||
|
|
||||||
|
for (final contact in sortedContacts) {
|
||||||
|
final prefix = _extractPrefix(contact.displayName);
|
||||||
|
final group = prefix == null ? null : eligibleGroups[prefix.key];
|
||||||
|
if (group == null) {
|
||||||
|
items.add(ContactListItem.contact(contact));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (emittedGroups.add(group.key)) {
|
||||||
|
items.add(ContactListItem.group(group));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
static _GroupPrefix? _extractPrefix(String name) {
|
||||||
|
final trimmed = name.trim();
|
||||||
|
if (trimmed.isEmpty) return null;
|
||||||
|
|
||||||
|
final match = _prefixedNamePattern.firstMatch(trimmed);
|
||||||
|
if (match == null) return null;
|
||||||
|
|
||||||
|
final rawPrefix = match.group(1);
|
||||||
|
final separator = match.group(2);
|
||||||
|
if (rawPrefix == null || separator == null) return null;
|
||||||
|
|
||||||
|
return _GroupPrefix(
|
||||||
|
key: rawPrefix.toUpperCase(),
|
||||||
|
label: '$rawPrefix$separator',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GroupPrefix {
|
||||||
|
final String key;
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
const _GroupPrefix({required this.key, required this.label});
|
||||||
|
}
|
||||||
@@ -4,38 +4,51 @@ import '../models/contact.dart';
|
|||||||
|
|
||||||
class DecodedLogRxRoute {
|
class DecodedLogRxRoute {
|
||||||
final int payloadType;
|
final int payloadType;
|
||||||
final List<int> pathHashes;
|
final int pathDescriptor;
|
||||||
|
final List<int> pathBytes;
|
||||||
|
final int hashSize;
|
||||||
|
|
||||||
const DecodedLogRxRoute({
|
const DecodedLogRxRoute({
|
||||||
required this.payloadType,
|
required this.payloadType,
|
||||||
required this.pathHashes,
|
required this.pathDescriptor,
|
||||||
|
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 {
|
class ResolvedNodeHash {
|
||||||
final int hash;
|
final String hashHex;
|
||||||
final String label;
|
final String label;
|
||||||
final bool isOwnNode;
|
final bool isOwnNode;
|
||||||
final bool isUniqueMatch;
|
final bool isUniqueMatch;
|
||||||
final int matchCount;
|
final int matchCount;
|
||||||
|
|
||||||
const ResolvedNodeHash({
|
const ResolvedNodeHash({
|
||||||
required this.hash,
|
required this.hashHex,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.isOwnNode,
|
required this.isOwnNode,
|
||||||
required this.isUniqueMatch,
|
required this.isUniqueMatch,
|
||||||
required this.matchCount,
|
required this.matchCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
String get hexLabel => '0x${hash.toRadixString(16).padLeft(2, '0')}';
|
String get hexLabel => '0x${hashHex.toUpperCase()}';
|
||||||
}
|
}
|
||||||
|
|
||||||
class LogRxRouteDecoder {
|
class LogRxRouteDecoder {
|
||||||
const LogRxRouteDecoder._();
|
const LogRxRouteDecoder._();
|
||||||
|
|
||||||
static DecodedLogRxRoute? decode(Uint8List rawData) {
|
static DecodedLogRxRoute? decode(
|
||||||
|
Uint8List rawData, {
|
||||||
|
int? preferredHashSize,
|
||||||
|
}) {
|
||||||
if (rawData.length < 5 || rawData[0] != 0x88) return null;
|
if (rawData.length < 5 || rawData[0] != 0x88) return null;
|
||||||
|
|
||||||
final rawPacketData = rawData.sublist(3);
|
final rawPacketData = rawData.sublist(3);
|
||||||
@@ -52,30 +65,109 @@ class LogRxRouteDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (rawPacketData.length <= index) return null;
|
if (rawPacketData.length <= index) return null;
|
||||||
final pathLen = rawPacketData[index++];
|
final pathDescriptor = rawPacketData[index++];
|
||||||
if (rawPacketData.length < index + pathLen) return null;
|
final pathMode = (pathDescriptor & 0xFF) >> 6;
|
||||||
|
final pathByteLen = pathMode == 0
|
||||||
|
? pathDescriptor
|
||||||
|
: descriptorByteLength(pathDescriptor);
|
||||||
|
if (pathByteLen == null || rawPacketData.length < index + pathByteLen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final pathBytes = rawPacketData.sublist(index, index + pathByteLen);
|
||||||
|
final hashSize = pathMode == 0
|
||||||
|
? inferHashSize(pathBytes, preferredHashSize: preferredHashSize)
|
||||||
|
: (descriptorHashSize(pathDescriptor) ??
|
||||||
|
inferHashSize(pathBytes, preferredHashSize: preferredHashSize));
|
||||||
|
|
||||||
return DecodedLogRxRoute(
|
return DecodedLogRxRoute(
|
||||||
payloadType: payloadType,
|
payloadType: payloadType,
|
||||||
pathHashes: rawPacketData.sublist(index, index + pathLen),
|
pathDescriptor: pathDescriptor,
|
||||||
|
pathBytes: pathBytes,
|
||||||
|
hashSize: hashSize,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int? descriptorHashSize(int pathDescriptor) {
|
||||||
|
final normalized = pathDescriptor & 0xFF;
|
||||||
|
final mode = normalized >> 6;
|
||||||
|
if (mode == 3) return null;
|
||||||
|
return mode + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int? descriptorHopCount(int pathDescriptor) {
|
||||||
|
final hashSize = descriptorHashSize(pathDescriptor);
|
||||||
|
if (hashSize == null) return null;
|
||||||
|
return (pathDescriptor & 0xFF) & 0x3F;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int? descriptorByteLength(int pathDescriptor) {
|
||||||
|
final hashSize = descriptorHashSize(pathDescriptor);
|
||||||
|
final hopCount = descriptorHopCount(pathDescriptor);
|
||||||
|
if (hashSize == null || hopCount == null) return null;
|
||||||
|
final byteLen = hopCount * hashSize;
|
||||||
|
return byteLen <= 64 ? byteLen : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
static ResolvedNodeHash resolveHash(
|
||||||
int hash, {
|
String hashHex, {
|
||||||
required Iterable<Contact> contacts,
|
required Iterable<Contact> contacts,
|
||||||
Uint8List? ownPublicKey,
|
Uint8List? ownPublicKey,
|
||||||
String? ownName,
|
String? ownName,
|
||||||
}) {
|
}) {
|
||||||
final ownHash = ownPublicKey != null && ownPublicKey.isNotEmpty
|
final normalizedHashHex = hashHex.toLowerCase();
|
||||||
? ownPublicKey.first
|
final ownKeyHex = _bytesToHex(ownPublicKey);
|
||||||
: null;
|
if (ownKeyHex != null && ownKeyHex.startsWith(normalizedHashHex)) {
|
||||||
if (ownHash == hash) {
|
|
||||||
final ownLabel = (ownName != null && ownName.trim().isNotEmpty)
|
final ownLabel = (ownName != null && ownName.trim().isNotEmpty)
|
||||||
? '$ownName (you)'
|
? '$ownName (you)'
|
||||||
: 'You';
|
: 'You';
|
||||||
return ResolvedNodeHash(
|
return ResolvedNodeHash(
|
||||||
hash: hash,
|
hashHex: normalizedHashHex,
|
||||||
label: ownLabel,
|
label: ownLabel,
|
||||||
isOwnNode: true,
|
isOwnNode: true,
|
||||||
isUniqueMatch: true,
|
isUniqueMatch: true,
|
||||||
@@ -84,12 +176,12 @@ class LogRxRouteDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final matches = contacts.where((contact) {
|
final matches = contacts.where((contact) {
|
||||||
return contact.publicKey.isNotEmpty && contact.publicKey.first == hash;
|
return contact.publicKeyHex.toLowerCase().startsWith(normalizedHashHex);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
if (matches.isEmpty) {
|
if (matches.isEmpty) {
|
||||||
return ResolvedNodeHash(
|
return ResolvedNodeHash(
|
||||||
hash: hash,
|
hashHex: normalizedHashHex,
|
||||||
label: 'Unknown',
|
label: 'Unknown',
|
||||||
isOwnNode: false,
|
isOwnNode: false,
|
||||||
isUniqueMatch: false,
|
isUniqueMatch: false,
|
||||||
@@ -99,7 +191,7 @@ class LogRxRouteDecoder {
|
|||||||
|
|
||||||
if (matches.length == 1) {
|
if (matches.length == 1) {
|
||||||
return ResolvedNodeHash(
|
return ResolvedNodeHash(
|
||||||
hash: hash,
|
hashHex: normalizedHashHex,
|
||||||
label: matches.first.displayName,
|
label: matches.first.displayName,
|
||||||
isOwnNode: false,
|
isOwnNode: false,
|
||||||
isUniqueMatch: true,
|
isUniqueMatch: true,
|
||||||
@@ -119,11 +211,19 @@ class LogRxRouteDecoder {
|
|||||||
? '$candidateNames +$extraCount'
|
? '$candidateNames +$extraCount'
|
||||||
: candidateNames;
|
: candidateNames;
|
||||||
return ResolvedNodeHash(
|
return ResolvedNodeHash(
|
||||||
hash: hash,
|
hashHex: normalizedHashHex,
|
||||||
label: label,
|
label: label,
|
||||||
isOwnNode: false,
|
isOwnNode: false,
|
||||||
isUniqueMatch: false,
|
isUniqueMatch: false,
|
||||||
matchCount: matches.length,
|
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:flutter/widgets.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import '../models/message.dart';
|
import '../models/message.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
|
import '../providers/messages_provider.dart';
|
||||||
|
|
||||||
/// Extension for Message to provide localized delivery status
|
/// Extension for Message to provide localized delivery status
|
||||||
extension MessageLocalization on Message {
|
extension MessageLocalization on Message {
|
||||||
/// Get localized delivery status text
|
/// Get localized delivery status text
|
||||||
String getLocalizedDeliveryStatus(BuildContext context) {
|
String getLocalizedDeliveryStatus(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final routeMetadata = context
|
||||||
|
.read<MessagesProvider>()
|
||||||
|
.getMessageRouteMetadata(id);
|
||||||
|
|
||||||
// For channel messages, show echo count instead of delivery status
|
// For channel messages, show echo count instead of delivery status
|
||||||
if (isChannelMessage && deliveryStatus == MessageDeliveryStatus.sent) {
|
if (isChannelMessage && deliveryStatus == MessageDeliveryStatus.sent) {
|
||||||
@@ -26,29 +31,51 @@ extension MessageLocalization on Message {
|
|||||||
case MessageDeliveryStatus.sending:
|
case MessageDeliveryStatus.sending:
|
||||||
if (isContactMessage) {
|
if (isContactMessage) {
|
||||||
if (retryAttempt > 0) {
|
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;
|
return l10n.sending;
|
||||||
case MessageDeliveryStatus.sent:
|
case MessageDeliveryStatus.sent:
|
||||||
return l10n.sent;
|
return routeMetadata == null
|
||||||
|
? l10n.sent
|
||||||
|
: '${l10n.sent} • ${routeMetadata.modeLabel}';
|
||||||
case MessageDeliveryStatus.delivered:
|
case MessageDeliveryStatus.delivered:
|
||||||
if (retryAttempt > 0 && roundTripTimeMs != null) {
|
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) {
|
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) {
|
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:
|
case MessageDeliveryStatus.failed:
|
||||||
if (retryAttempt > 0) {
|
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:
|
case MessageDeliveryStatus.received:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class ContactAvatar extends StatelessWidget {
|
|||||||
final foregroundColor = _getForegroundColor(backgroundColor);
|
final foregroundColor = _getForegroundColor(backgroundColor);
|
||||||
final emoji = contact.roleEmoji;
|
final emoji = contact.roleEmoji;
|
||||||
|
|
||||||
if (emoji != null && emoji.isNotEmpty) {
|
if (_showsLeadingEmoji && emoji != null && emoji.isNotEmpty) {
|
||||||
return _buildAvatarFrame(
|
return _buildAvatarFrame(
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
child: Text(emoji, style: TextStyle(fontSize: radius * 1.05)),
|
child: Text(emoji, style: TextStyle(fontSize: radius * 1.05)),
|
||||||
@@ -86,8 +86,16 @@ class ContactAvatar extends StatelessWidget {
|
|||||||
bool get _usesSquareShape =>
|
bool get _usesSquareShape =>
|
||||||
contact.type == ContactType.channel || contact.type == ContactType.room;
|
contact.type == ContactType.channel || contact.type == ContactType.room;
|
||||||
|
|
||||||
|
bool get _showsLeadingEmoji {
|
||||||
|
final emoji = contact.roleEmoji;
|
||||||
|
if (emoji == null || emoji.isEmpty) return false;
|
||||||
|
|
||||||
|
final effectiveName = (displayName ?? contact.displayName).trimLeft();
|
||||||
|
return effectiveName.startsWith(emoji);
|
||||||
|
}
|
||||||
|
|
||||||
Color _getBackgroundColor(BuildContext context) {
|
Color _getBackgroundColor(BuildContext context) {
|
||||||
if (_shouldUseLabelFallback || (contact.roleEmoji?.isNotEmpty ?? false)) {
|
if (_shouldUseLabelFallback || _showsLeadingEmoji) {
|
||||||
return TrailColorService.getTrailColor(contact);
|
return TrailColorService.getTrailColor(contact);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -25,6 +26,10 @@ class _ConnectionModeSelectorState extends State<ConnectionModeSelector> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadLocalIPs() async {
|
Future<void> _loadLocalIPs() async {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final Set<String> ipsSet = {};
|
final Set<String> ipsSet = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../models/contact.dart';
|
import '../../models/contact.dart';
|
||||||
|
import '../../providers/app_provider.dart';
|
||||||
import '../../services/route_hash_preferences.dart';
|
import '../../services/route_hash_preferences.dart';
|
||||||
|
|
||||||
class ContactRouteDialogResult {
|
class ContactRouteDialogResult {
|
||||||
@@ -57,6 +59,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
|
|||||||
int _selectedHashSize = RouteHashPreferences.defaultHashSize;
|
int _selectedHashSize = RouteHashPreferences.defaultHashSize;
|
||||||
ParsedContactRoute? _parsedRoute;
|
ParsedContactRoute? _parsedRoute;
|
||||||
String? _errorText;
|
String? _errorText;
|
||||||
|
bool _showRoutingInfo = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -133,6 +136,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final appProvider = context.watch<AppProvider>();
|
||||||
final routeCandidates =
|
final routeCandidates =
|
||||||
widget.availableContacts
|
widget.availableContacts
|
||||||
.where((contact) => contact.isRepeater || contact.isRoom)
|
.where((contact) => contact.isRepeater || contact.isRoom)
|
||||||
@@ -184,6 +188,19 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 16),
|
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(
|
Text(
|
||||||
'Pick hops from contacts',
|
'Pick hops from contacts',
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
@@ -203,10 +220,6 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
|
|||||||
dense: true,
|
dense: true,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(candidate.displayName),
|
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(
|
trailing: TextButton(
|
||||||
onPressed: () => _appendHop(candidate),
|
onPressed: () => _appendHop(candidate),
|
||||||
child: Text(
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -403,15 +403,6 @@ class _ImageMessageBubbleState extends State<ImageMessageBubble> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sender.routeSupportsLegacyRawTransport) {
|
|
||||||
_clearRequestState();
|
|
||||||
await _showBlockingAlert(
|
|
||||||
'Cannot fetch image',
|
|
||||||
'Sender route uses 3-byte hashes. Raw media fetch is not supported in this client yet.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sender.routeHopCount >= 2) {
|
if (sender.routeHopCount >= 2) {
|
||||||
_showToast(
|
_showToast(
|
||||||
'Image fetch over ${sender.routeHopCount} hops may take a while.',
|
'Image fetch over ${sender.routeHopCount} hops may take a while.',
|
||||||
@@ -462,7 +453,7 @@ class _ImageMessageBubbleState extends State<ImageMessageBubble> {
|
|||||||
);
|
);
|
||||||
await conn.sendRawVoicePacket(
|
await conn.sendRawVoicePacket(
|
||||||
contactPath: sender.outPath,
|
contactPath: sender.outPath,
|
||||||
contactPathLen: sender.routeSignedPathLen,
|
contactPathLen: sender.routeEncodedPathLen,
|
||||||
payload: payload,
|
payload: payload,
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import '../../providers/connection_provider.dart';
|
|||||||
import '../../providers/drawing_provider.dart';
|
import '../../providers/drawing_provider.dart';
|
||||||
import '../../providers/voice_provider.dart';
|
import '../../providers/voice_provider.dart';
|
||||||
import '../../providers/image_provider.dart' as ip;
|
import '../../providers/image_provider.dart' as ip;
|
||||||
|
import '../../providers/app_provider.dart';
|
||||||
import '../drawing_minimap_preview.dart';
|
import '../drawing_minimap_preview.dart';
|
||||||
import '../../models/ble_packet_log.dart';
|
import '../../models/ble_packet_log.dart';
|
||||||
import '../../services/sar_template_service.dart';
|
import '../../services/sar_template_service.dart';
|
||||||
@@ -26,6 +27,7 @@ import '../../utils/tictactoe_message_parser.dart';
|
|||||||
import '../../utils/location_formats.dart';
|
import '../../utils/location_formats.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
import '../../utils/message_extensions.dart';
|
import '../../utils/message_extensions.dart';
|
||||||
|
import '../../utils/log_rx_route_decoder.dart';
|
||||||
import '../../models/message_transfer_details.dart';
|
import '../../models/message_transfer_details.dart';
|
||||||
import 'voice_message_bubble.dart';
|
import 'voice_message_bubble.dart';
|
||||||
import 'image_message_bubble.dart';
|
import 'image_message_bubble.dart';
|
||||||
@@ -44,6 +46,7 @@ import 'system_message_bubble.dart';
|
|||||||
class MessageBubble extends StatefulWidget {
|
class MessageBubble extends StatefulWidget {
|
||||||
final Message message;
|
final Message message;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
final VoidCallback? onReply;
|
||||||
final bool isHighlighted;
|
final bool isHighlighted;
|
||||||
final VoidCallback? onNavigateToMap;
|
final VoidCallback? onNavigateToMap;
|
||||||
|
|
||||||
@@ -54,6 +57,7 @@ class MessageBubble extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.message,
|
required this.message,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.onReply,
|
||||||
this.isHighlighted = false,
|
this.isHighlighted = false,
|
||||||
this.onNavigateToMap,
|
this.onNavigateToMap,
|
||||||
this.isCompact = false,
|
this.isCompact = false,
|
||||||
@@ -209,6 +213,15 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
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
|
// Copy text option
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.copy),
|
leading: const Icon(Icons.copy),
|
||||||
@@ -469,6 +482,9 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
final retryCause = _retryCauseLabel(widget.message);
|
final retryCause = _retryCauseLabel(widget.message);
|
||||||
final retryResult = _retryResultLabel(widget.message);
|
final retryResult = _retryResultLabel(widget.message);
|
||||||
final retryMode = _retryModeLabel(widget.message);
|
final retryMode = _retryModeLabel(widget.message);
|
||||||
|
final routeMetadata = context
|
||||||
|
.read<MessagesProvider>()
|
||||||
|
.getMessageRouteMetadata(widget.message.id);
|
||||||
|
|
||||||
final rawLines = <String>[
|
final rawLines = <String>[
|
||||||
'Message ID: ${widget.message.id}',
|
'Message ID: ${widget.message.id}',
|
||||||
@@ -790,7 +806,7 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
_detailRow(
|
_detailRow(
|
||||||
context,
|
context,
|
||||||
label: l10n.retryAttempt,
|
label: l10n.retryAttempt,
|
||||||
value: '${widget.message.retryAttempt}/3',
|
value: '${widget.message.retryAttempt}/4',
|
||||||
),
|
),
|
||||||
if (widget.message.lastRetryAt != null)
|
if (widget.message.lastRetryAt != null)
|
||||||
_detailRow(
|
_detailRow(
|
||||||
@@ -809,6 +825,20 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
label: l10n.floodFallback,
|
label: l10n.floodFallback,
|
||||||
value: l10n.yes,
|
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)
|
if (retryResult != null)
|
||||||
_detailRow(
|
_detailRow(
|
||||||
context,
|
context,
|
||||||
@@ -885,7 +915,9 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
_detailRow(
|
_detailRow(
|
||||||
context,
|
context,
|
||||||
label: l10n.envelope,
|
label: l10n.envelope,
|
||||||
value: envelope != null ? 'VE3 compact' : l10n.unknown,
|
value: envelope != null
|
||||||
|
? 'VE3 compact'
|
||||||
|
: l10n.unknown,
|
||||||
),
|
),
|
||||||
if (voiceSession != null)
|
if (voiceSession != null)
|
||||||
_detailRow(
|
_detailRow(
|
||||||
@@ -1238,12 +1270,10 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
|
|
||||||
// Logged frame format:
|
// Logged frame format:
|
||||||
// [0]=response code 0x88, [1]=snrRaw, [2]=rssi, [3]=packet header, [4]=pathLen
|
// [0]=response code 0x88, [1]=snrRaw, [2]=rssi, [3]=packet header, [4]=pathLen
|
||||||
final raw = log.rawData;
|
final decoded = LogRxRouteDecoder.decode(log.rawData);
|
||||||
final payloadType = (raw[3] >> 2) & 0x0F;
|
if (decoded == null) continue;
|
||||||
final pathLen = raw[4];
|
if (decoded.payloadType != expectedPayloadType) continue;
|
||||||
if (payloadType != expectedPayloadType) continue;
|
if (decoded.hopCount != message.pathLen) continue;
|
||||||
if (pathLen != message.pathLen) continue;
|
|
||||||
if (raw.length < 5 + pathLen) continue;
|
|
||||||
|
|
||||||
final deltaMs =
|
final deltaMs =
|
||||||
(log.timestamp.difference(message.receivedAt).inMilliseconds).abs();
|
(log.timestamp.difference(message.receivedAt).inMilliseconds).abs();
|
||||||
@@ -1259,11 +1289,9 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
|
|
||||||
List<int>? _extractPathBytesFromLog(BlePacketLog? log) {
|
List<int>? _extractPathBytesFromLog(BlePacketLog? log) {
|
||||||
if (log == null) return null;
|
if (log == null) return null;
|
||||||
final raw = log.rawData;
|
final decoded = LogRxRouteDecoder.decode(log.rawData);
|
||||||
if (raw.length < 6) return null;
|
if (decoded == null || decoded.pathBytes.isEmpty) return null;
|
||||||
final pathLen = raw[4];
|
return decoded.pathBytes;
|
||||||
if (pathLen <= 0 || raw.length < 5 + pathLen) return null;
|
|
||||||
return raw.sublist(5, 5 + pathLen);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showDeleteConfirmation(BuildContext context) {
|
void _showDeleteConfirmation(BuildContext context) {
|
||||||
@@ -1545,8 +1573,15 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final routeMetadata = context
|
||||||
|
.read<MessagesProvider>()
|
||||||
|
.getMessageRouteMetadata(message.id);
|
||||||
|
if (routeMetadata != null) {
|
||||||
|
return routeMetadata.modeLabel;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.usedFloodFallback) {
|
if (message.usedFloodFallback) {
|
||||||
return 'Flood fallback';
|
return 'Flood route';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.retryAttempt > 0 || message.expectedAckTag != null) {
|
if (message.retryAttempt > 0 || message.expectedAckTag != null) {
|
||||||
@@ -1561,9 +1596,17 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final routeMetadata = context
|
||||||
|
.read<MessagesProvider>()
|
||||||
|
.getMessageRouteMetadata(message.id);
|
||||||
|
final routeLabel = routeMetadata?.modeLabel.toLowerCase();
|
||||||
|
|
||||||
if (message.deliveryStatus == MessageDeliveryStatus.delivered) {
|
if (message.deliveryStatus == MessageDeliveryStatus.delivered) {
|
||||||
|
if (routeLabel != null) {
|
||||||
|
return 'Delivered via $routeLabel';
|
||||||
|
}
|
||||||
if (message.usedFloodFallback) {
|
if (message.usedFloodFallback) {
|
||||||
return 'Delivered after flood fallback';
|
return 'Delivered after flood route';
|
||||||
}
|
}
|
||||||
if (message.retryAttempt > 0) {
|
if (message.retryAttempt > 0) {
|
||||||
return 'Delivered after retry';
|
return 'Delivered after retry';
|
||||||
@@ -1574,8 +1617,11 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.deliveryStatus == MessageDeliveryStatus.sending) {
|
if (message.deliveryStatus == MessageDeliveryStatus.sending) {
|
||||||
|
if (routeLabel != null) {
|
||||||
|
return '$routeLabel in progress';
|
||||||
|
}
|
||||||
if (message.usedFloodFallback) {
|
if (message.usedFloodFallback) {
|
||||||
return 'Flood fallback in progress';
|
return 'Flood route in progress';
|
||||||
}
|
}
|
||||||
if (message.retryAttempt > 0) {
|
if (message.retryAttempt > 0) {
|
||||||
return 'Retry in progress';
|
return 'Retry in progress';
|
||||||
@@ -1586,8 +1632,11 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.deliveryStatus == MessageDeliveryStatus.failed) {
|
if (message.deliveryStatus == MessageDeliveryStatus.failed) {
|
||||||
|
if (routeLabel != null) {
|
||||||
|
return 'Failed via $routeLabel';
|
||||||
|
}
|
||||||
if (message.usedFloodFallback) {
|
if (message.usedFloodFallback) {
|
||||||
return 'Failed after flood fallback';
|
return 'Failed after flood route';
|
||||||
}
|
}
|
||||||
if (message.retryAttempt > 0) {
|
if (message.retryAttempt > 0) {
|
||||||
return 'Failed after retry attempts';
|
return 'Failed after retry attempts';
|
||||||
@@ -1598,6 +1647,36 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
return null;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Display system messages with minimal styling
|
// Display system messages with minimal styling
|
||||||
@@ -1615,6 +1694,7 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
}
|
}
|
||||||
final isSarMarker = message.isSarMarker;
|
final isSarMarker = message.isSarMarker;
|
||||||
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
final messageFontScale = context.watch<AppProvider>().messageFontScale;
|
||||||
|
|
||||||
// Determine if this is own message
|
// Determine if this is own message
|
||||||
final connectionProvider = context.read<ConnectionProvider>();
|
final connectionProvider = context.read<ConnectionProvider>();
|
||||||
@@ -1728,6 +1808,9 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
final shouldFloatBubble = widget.isCompact;
|
final shouldFloatBubble = widget.isCompact;
|
||||||
|
final baseBodyStyle = Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyMedium?.copyWith(fontSize: 14 * messageFontScale);
|
||||||
final bubble = ConstrainedBox(
|
final bubble = ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: shouldFloatBubble
|
maxWidth: shouldFloatBubble
|
||||||
@@ -2146,10 +2229,7 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (drawing == null) {
|
if (drawing == null) {
|
||||||
return Text(
|
return Text(message.text, style: baseBodyStyle);
|
||||||
message.text,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final String drawingTypeLabel;
|
final String drawingTypeLabel;
|
||||||
@@ -2235,10 +2315,7 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
)
|
)
|
||||||
// Regular message content
|
// Regular message content
|
||||||
else if (!message.isDrawing || widget.isCompact)
|
else if (!message.isDrawing || widget.isCompact)
|
||||||
Text(
|
Text(message.text, style: baseBodyStyle),
|
||||||
message.text,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
|
|
||||||
if (!widget.isCompact &&
|
if (!widget.isCompact &&
|
||||||
!isSarMarker &&
|
!isSarMarker &&
|
||||||
@@ -2458,6 +2535,12 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
|
if (message.isContactMessage)
|
||||||
|
_buildDirectMessageStatusIndicator(
|
||||||
|
context,
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
Icon(
|
Icon(
|
||||||
getDeliveryStatusIcon(message.deliveryStatus),
|
getDeliveryStatusIcon(message.deliveryStatus),
|
||||||
size: 12,
|
size: 12,
|
||||||
@@ -2470,7 +2553,9 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text(
|
child: Text(
|
||||||
message.getLocalizedDeliveryStatus(context),
|
message.getLocalizedDeliveryStatus(
|
||||||
|
context,
|
||||||
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
@@ -2485,6 +2570,7 @@ class _MessageBubbleState extends State<MessageBubble> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
// Show retry button for failed messages
|
// Show retry button for failed messages
|
||||||
if (message.deliveryStatus ==
|
if (message.deliveryStatus ==
|
||||||
MessageDeliveryStatus.failed) ...[
|
MessageDeliveryStatus.failed) ...[
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../models/message.dart';
|
import '../../models/message.dart';
|
||||||
|
import '../../models/path_selection.dart';
|
||||||
import '../../models/message_reception_details.dart';
|
import '../../models/message_reception_details.dart';
|
||||||
|
import '../../providers/messages_provider.dart';
|
||||||
|
|
||||||
IconData getDeliveryStatusIcon(MessageDeliveryStatus status) {
|
IconData getDeliveryStatusIcon(MessageDeliveryStatus status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -185,6 +188,9 @@ Widget buildSentDirectSignalStatus(
|
|||||||
required int roundTripTimeMs,
|
required int roundTripTimeMs,
|
||||||
required Duration txEstimate,
|
required Duration txEstimate,
|
||||||
}) {
|
}) {
|
||||||
|
final routeMetadata = context
|
||||||
|
.read<MessagesProvider>()
|
||||||
|
.getMessageRouteMetadata(message.id);
|
||||||
final estimatedTransmitMs = sanitizeEstimatedTransmitMs(
|
final estimatedTransmitMs = sanitizeEstimatedTransmitMs(
|
||||||
estimatedTransmitMs: txEstimate > Duration.zero
|
estimatedTransmitMs: txEstimate > Duration.zero
|
||||||
? txEstimate.inMilliseconds
|
? txEstimate.inMilliseconds
|
||||||
@@ -230,7 +236,7 @@ Widget buildSentDirectSignalStatus(
|
|||||||
_techChip(
|
_techChip(
|
||||||
context,
|
context,
|
||||||
icon: Icons.refresh,
|
icon: Icons.refresh,
|
||||||
label: 'retry ${message.retryAttempt}/3',
|
label: 'retry ${message.retryAttempt}/4',
|
||||||
color: Colors.redAccent,
|
color: Colors.redAccent,
|
||||||
),
|
),
|
||||||
if (message.suggestedTimeoutMs != null)
|
if (message.suggestedTimeoutMs != null)
|
||||||
@@ -244,7 +250,7 @@ Widget buildSentDirectSignalStatus(
|
|||||||
_techChip(
|
_techChip(
|
||||||
context,
|
context,
|
||||||
icon: Icons.waves,
|
icon: Icons.waves,
|
||||||
label: 'flood fallback',
|
label: 'flood route',
|
||||||
color: Colors.teal,
|
color: Colors.teal,
|
||||||
)
|
)
|
||||||
else if (message.expectedAckTag != null)
|
else if (message.expectedAckTag != null)
|
||||||
@@ -254,6 +260,17 @@ Widget buildSentDirectSignalStatus(
|
|||||||
label: 'direct ACK',
|
label: 'direct ACK',
|
||||||
color: Colors.indigo,
|
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,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// ignore_for_file: use_null_aware_elements
|
// ignore_for_file: use_null_aware_elements
|
||||||
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart' as flutter_map;
|
import 'package:flutter_map/flutter_map.dart' as flutter_map;
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
@@ -10,6 +11,8 @@ import '../../models/message.dart';
|
|||||||
import '../../providers/connection_provider.dart';
|
import '../../providers/connection_provider.dart';
|
||||||
import '../../providers/contacts_provider.dart';
|
import '../../providers/contacts_provider.dart';
|
||||||
import '../../services/mesh_map_nodes_service.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 {
|
class MessageTraceSheet extends StatefulWidget {
|
||||||
final Message message;
|
final Message message;
|
||||||
@@ -32,6 +35,7 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
Future<_TraceResult> _loadTrace() async {
|
Future<_TraceResult> _loadTrace() async {
|
||||||
final connectionProvider = context.read<ConnectionProvider>();
|
final connectionProvider = context.read<ConnectionProvider>();
|
||||||
final contactsProvider = context.read<ContactsProvider>();
|
final contactsProvider = context.read<ContactsProvider>();
|
||||||
|
final preferredHashSize = await RouteHashPreferences.getHashSize();
|
||||||
final packetPath = _extractPathFromPacketLogs(
|
final packetPath = _extractPathFromPacketLogs(
|
||||||
logs: connectionProvider.bleService.packetLogs,
|
logs: connectionProvider.bleService.packetLogs,
|
||||||
message: widget.message,
|
message: widget.message,
|
||||||
@@ -46,19 +50,30 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
var trace = _buildTraceResult(
|
var trace = _buildTraceResult(
|
||||||
nodes: localNodes,
|
nodes: localNodes,
|
||||||
packetPath: packetPath,
|
packetPath: packetPath,
|
||||||
|
preferredHashSize: preferredHashSize,
|
||||||
senderPrefix: senderPrefix,
|
senderPrefix: senderPrefix,
|
||||||
recipientPrefix: recipientPrefix,
|
recipientPrefix: recipientPrefix,
|
||||||
);
|
);
|
||||||
if (_isCompleteTrace(trace, expectedRelayCount: math.max(0, widget.message.pathLen))) {
|
if (_isCompleteTrace(
|
||||||
|
trace,
|
||||||
|
expectedRelayCount: math.max(0, widget.message.pathLen),
|
||||||
|
)) {
|
||||||
return trace;
|
return trace;
|
||||||
}
|
}
|
||||||
|
|
||||||
final remoteNodes = await MeshMapNodesService.fetchNodes(
|
unawaited(
|
||||||
|
MeshMapNodesService.syncInBackgroundIfStale(
|
||||||
|
cacheTtl: MeshMapNodesService.traceCacheTtl,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final remoteNodes = await MeshMapNodesService.loadCachedNodes(
|
||||||
cacheTtl: MeshMapNodesService.traceCacheTtl,
|
cacheTtl: MeshMapNodesService.traceCacheTtl,
|
||||||
);
|
);
|
||||||
trace = _buildTraceResult(
|
trace = _buildTraceResult(
|
||||||
nodes: _mergeNodes(localNodes, remoteNodes),
|
nodes: _mergeNodes(localNodes, remoteNodes),
|
||||||
packetPath: packetPath,
|
packetPath: packetPath,
|
||||||
|
preferredHashSize: preferredHashSize,
|
||||||
senderPrefix: senderPrefix,
|
senderPrefix: senderPrefix,
|
||||||
recipientPrefix: recipientPrefix,
|
recipientPrefix: recipientPrefix,
|
||||||
);
|
);
|
||||||
@@ -336,9 +351,7 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
|
|
||||||
if (trace.mode == TraceMode.packetPath) {
|
if (trace.mode == TraceMode.packetPath) {
|
||||||
final entries = trace.matchedPathNodes.asMap().entries.map((entry) {
|
final entries = trace.matchedPathNodes.asMap().entries.map((entry) {
|
||||||
final hashHex = trace.pathHashes[entry.key]
|
final hashHex = trace.pathHashes[entry.key].toUpperCase();
|
||||||
.toRadixString(16)
|
|
||||||
.padLeft(2, '0');
|
|
||||||
return _RouteDisplayEntry(
|
return _RouteDisplayEntry(
|
||||||
node: entry.value,
|
node: entry.value,
|
||||||
label: entry.value?.name ?? 'Unknown',
|
label: entry.value?.name ?? 'Unknown',
|
||||||
@@ -428,6 +441,7 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
_TraceResult _buildTraceResult({
|
_TraceResult _buildTraceResult({
|
||||||
required List<MeshMapNode> nodes,
|
required List<MeshMapNode> nodes,
|
||||||
required List<int>? packetPath,
|
required List<int>? packetPath,
|
||||||
|
required int preferredHashSize,
|
||||||
required String? senderPrefix,
|
required String? senderPrefix,
|
||||||
required String? recipientPrefix,
|
required String? recipientPrefix,
|
||||||
}) {
|
}) {
|
||||||
@@ -435,9 +449,17 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
final recipientNode = _bestNodeForPrefix(nodes, recipientPrefix);
|
final recipientNode = _bestNodeForPrefix(nodes, recipientPrefix);
|
||||||
|
|
||||||
if (packetPath != null && packetPath.isNotEmpty) {
|
if (packetPath != null && packetPath.isNotEmpty) {
|
||||||
|
final hashSize = LogRxRouteDecoder.inferHashSize(
|
||||||
|
packetPath,
|
||||||
|
preferredHashSize: preferredHashSize,
|
||||||
|
);
|
||||||
|
final hopHashes = LogRxRouteDecoder.splitHopHashes(
|
||||||
|
packetPath,
|
||||||
|
hashSize: hashSize,
|
||||||
|
);
|
||||||
final matched = _matchNodesFromPathHashes(
|
final matched = _matchNodesFromPathHashes(
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
pathHashes: packetPath,
|
pathHashes: hopHashes,
|
||||||
senderPrefix: senderPrefix,
|
senderPrefix: senderPrefix,
|
||||||
recipientPrefix: recipientPrefix,
|
recipientPrefix: recipientPrefix,
|
||||||
);
|
);
|
||||||
@@ -445,7 +467,7 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
mode: TraceMode.packetPath,
|
mode: TraceMode.packetPath,
|
||||||
sender: senderNode,
|
sender: senderNode,
|
||||||
recipient: recipientNode,
|
recipient: recipientNode,
|
||||||
pathHashes: packetPath,
|
pathHashes: hopHashes,
|
||||||
matchedPathNodes: matched,
|
matchedPathNodes: matched,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -481,7 +503,9 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
trace.matchedPathNodes.every((node) => node != null);
|
trace.matchedPathNodes.every((node) => node != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
final concreteCount = trace.matchedPathNodes.whereType<MeshMapNode>().length;
|
final concreteCount = trace.matchedPathNodes
|
||||||
|
.whereType<MeshMapNode>()
|
||||||
|
.length;
|
||||||
return concreteCount >= expectedRelayCount + 2;
|
return concreteCount >= expectedRelayCount + 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,13 +523,10 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
for (final log in logs) {
|
for (final log in logs) {
|
||||||
if (log.responseCode != 0x88) continue; // pushLogRxData
|
if (log.responseCode != 0x88) continue; // pushLogRxData
|
||||||
if (log.rawData.length < 6) continue;
|
if (log.rawData.length < 6) continue;
|
||||||
final raw = log.rawData;
|
final decoded = LogRxRouteDecoder.decode(log.rawData);
|
||||||
final header = raw[3];
|
if (decoded == null) continue;
|
||||||
final payloadType = (header >> 2) & 0x0F;
|
if (decoded.payloadType != expectedPayloadType) continue;
|
||||||
final pathLen = raw[4];
|
if (decoded.hopCount != message.pathLen) continue;
|
||||||
if (payloadType != expectedPayloadType) continue;
|
|
||||||
if (pathLen != message.pathLen) continue;
|
|
||||||
if (raw.length < 5 + pathLen) continue;
|
|
||||||
|
|
||||||
final deltaMs =
|
final deltaMs =
|
||||||
(log.timestamp.difference(message.receivedAt).inMilliseconds).abs();
|
(log.timestamp.difference(message.receivedAt).inMilliseconds).abs();
|
||||||
@@ -516,20 +537,22 @@ class _MessageTraceSheetState extends State<MessageTraceSheet> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (bestLog == null || bestDeltaMs > 30000) return null;
|
if (bestLog == null || bestDeltaMs > 30000) return null;
|
||||||
final raw = bestLog.rawData;
|
final decoded = LogRxRouteDecoder.decode(bestLog.rawData);
|
||||||
final pathLen = raw[4];
|
if (decoded == null || decoded.pathBytes.isEmpty) {
|
||||||
return raw.sublist(5, 5 + pathLen);
|
return null;
|
||||||
|
}
|
||||||
|
return decoded.pathBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<MeshMapNode?> _matchNodesFromPathHashes({
|
List<MeshMapNode?> _matchNodesFromPathHashes({
|
||||||
required List<MeshMapNode> nodes,
|
required List<MeshMapNode> nodes,
|
||||||
required List<int> pathHashes,
|
required List<String> pathHashes,
|
||||||
required String? senderPrefix,
|
required String? senderPrefix,
|
||||||
required String? recipientPrefix,
|
required String? recipientPrefix,
|
||||||
}) {
|
}) {
|
||||||
final result = <MeshMapNode?>[];
|
final result = <MeshMapNode?>[];
|
||||||
for (var i = 0; i < pathHashes.length; i++) {
|
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
|
final candidates = nodes
|
||||||
.where((n) => n.publicKey.startsWith(hashHex))
|
.where((n) => n.publicKey.startsWith(hashHex))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -618,7 +641,7 @@ class _TraceResult {
|
|||||||
final TraceMode mode;
|
final TraceMode mode;
|
||||||
final MeshMapNode? sender;
|
final MeshMapNode? sender;
|
||||||
final MeshMapNode? recipient;
|
final MeshMapNode? recipient;
|
||||||
final List<int> pathHashes;
|
final List<String> pathHashes;
|
||||||
final List<MeshMapNode?> matchedPathNodes;
|
final List<MeshMapNode?> matchedPathNodes;
|
||||||
|
|
||||||
const _TraceResult({
|
const _TraceResult({
|
||||||
@@ -645,7 +668,10 @@ class _RouteDisplayEntry {
|
|||||||
return _RouteDisplayEntry(
|
return _RouteDisplayEntry(
|
||||||
node: node,
|
node: node,
|
||||||
label: node.name,
|
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/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
|
import '../../providers/app_provider.dart';
|
||||||
|
|
||||||
class MessagesComposer extends StatelessWidget {
|
class MessagesComposer extends StatelessWidget {
|
||||||
final TextEditingController textController;
|
final TextEditingController textController;
|
||||||
@@ -253,6 +255,10 @@ class _MessageInput extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final messageFontScale = context.watch<AppProvider>().messageFontScale;
|
||||||
|
const baseFontSize = 15.0;
|
||||||
|
final resolvedFontSize = baseFontSize * messageFontScale;
|
||||||
|
|
||||||
return AnimatedContainer(
|
return AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 180),
|
duration: const Duration(milliseconds: 180),
|
||||||
constraints: const BoxConstraints(minHeight: 46, maxHeight: 132),
|
constraints: const BoxConstraints(minHeight: 46, maxHeight: 132),
|
||||||
@@ -286,12 +292,12 @@ class _MessageInput extends StatelessWidget {
|
|||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
inputFormatters: [messageByteLimiter],
|
inputFormatters: [messageByteLimiter],
|
||||||
style: const TextStyle(fontSize: 15),
|
style: TextStyle(fontSize: resolvedFontSize),
|
||||||
textAlignVertical: TextAlignVertical.center,
|
textAlignVertical: TextAlignVertical.center,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: AppLocalizations.of(context)!.typeYourMessage,
|
hintText: AppLocalizations.of(context)!.typeYourMessage,
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: resolvedFontSize,
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.onSurfaceVariant.withValues(alpha: 0.9),
|
).colorScheme.onSurfaceVariant.withValues(alpha: 0.9),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class MessagesContent extends StatelessWidget {
|
|||||||
final Future<void> Function() onRefresh;
|
final Future<void> Function() onRefresh;
|
||||||
final VoidCallback? onNavigateToMap;
|
final VoidCallback? onNavigateToMap;
|
||||||
final ValueChanged<Message>? onMessageTap;
|
final ValueChanged<Message>? onMessageTap;
|
||||||
|
final ValueChanged<Message>? onReplyToMessage;
|
||||||
|
|
||||||
const MessagesContent({
|
const MessagesContent({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -24,6 +25,7 @@ class MessagesContent extends StatelessWidget {
|
|||||||
required this.onRefresh,
|
required this.onRefresh,
|
||||||
this.onNavigateToMap,
|
this.onNavigateToMap,
|
||||||
this.onMessageTap,
|
this.onMessageTap,
|
||||||
|
this.onReplyToMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -86,6 +88,9 @@ class MessagesContent extends StatelessWidget {
|
|||||||
onTap: onMessageTap == null
|
onTap: onMessageTap == null
|
||||||
? null
|
? null
|
||||||
: () => onMessageTap!(message),
|
: () => onMessageTap!(message),
|
||||||
|
onReply: onReplyToMessage == null
|
||||||
|
? null
|
||||||
|
: () => onReplyToMessage!(message),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ class RecipientSelectorSheet extends StatefulWidget {
|
|||||||
final List<Contact> contacts;
|
final List<Contact> contacts;
|
||||||
final List<Contact> rooms;
|
final List<Contact> rooms;
|
||||||
final List<Contact> channels;
|
final List<Contact> channels;
|
||||||
|
final int unreadCount;
|
||||||
|
final Map<String, int> unreadCountsByPublicKey;
|
||||||
final String? currentDestinationType;
|
final String? currentDestinationType;
|
||||||
final String? currentRecipientPublicKey;
|
final String? currentRecipientPublicKey;
|
||||||
final Function(String type, Contact? recipient) onSelect;
|
final Function(String type, Contact? recipient) onSelect;
|
||||||
@@ -17,6 +19,8 @@ class RecipientSelectorSheet extends StatefulWidget {
|
|||||||
required this.contacts,
|
required this.contacts,
|
||||||
required this.rooms,
|
required this.rooms,
|
||||||
required this.channels,
|
required this.channels,
|
||||||
|
required this.unreadCount,
|
||||||
|
required this.unreadCountsByPublicKey,
|
||||||
this.currentDestinationType,
|
this.currentDestinationType,
|
||||||
this.currentRecipientPublicKey,
|
this.currentRecipientPublicKey,
|
||||||
required this.onSelect,
|
required this.onSelect,
|
||||||
@@ -47,7 +51,7 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
|||||||
|
|
||||||
bool _isSelected(String type, Contact? contact) {
|
bool _isSelected(String type, Contact? contact) {
|
||||||
if (widget.currentDestinationType != type) return false;
|
if (widget.currentDestinationType != type) return false;
|
||||||
if (contact == null) return false;
|
if (contact == null) return widget.currentRecipientPublicKey == null;
|
||||||
return contact.publicKeyHex == widget.currentRecipientPublicKey;
|
return contact.publicKeyHex == widget.currentRecipientPublicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +142,19 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
children: [
|
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
|
// Channels section
|
||||||
if (widget.channels.isNotEmpty) ...[
|
if (widget.channels.isNotEmpty) ...[
|
||||||
Padding(
|
Padding(
|
||||||
@@ -174,6 +191,10 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
|||||||
subtitle: channel.isPublicChannel
|
subtitle: channel.isPublicChannel
|
||||||
? l10n.broadcastToAllNearby
|
? l10n.broadcastToAllNearby
|
||||||
: '${l10n.channel} ${channel.publicKey[1]}', // Show slot number
|
: '${l10n.channel} ${channel.publicKey[1]}', // Show slot number
|
||||||
|
unreadCount:
|
||||||
|
widget.unreadCountsByPublicKey[channel
|
||||||
|
.publicKeyHex] ??
|
||||||
|
0,
|
||||||
isSelected: _isSelected('channel', channel),
|
isSelected: _isSelected('channel', channel),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
widget.onSelect('channel', channel);
|
widget.onSelect('channel', channel);
|
||||||
@@ -219,6 +240,10 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
|||||||
contact: contact,
|
contact: contact,
|
||||||
title: contact.displayName,
|
title: contact.displayName,
|
||||||
subtitle: contact.publicKeyShort,
|
subtitle: contact.publicKeyShort,
|
||||||
|
unreadCount:
|
||||||
|
widget.unreadCountsByPublicKey[contact
|
||||||
|
.publicKeyHex] ??
|
||||||
|
0,
|
||||||
isSelected: _isSelected('contact', contact),
|
isSelected: _isSelected('contact', contact),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
widget.onSelect('contact', contact);
|
widget.onSelect('contact', contact);
|
||||||
@@ -264,6 +289,9 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
|||||||
contact: room,
|
contact: room,
|
||||||
title: room.displayName,
|
title: room.displayName,
|
||||||
subtitle: room.publicKeyShort,
|
subtitle: room.publicKeyShort,
|
||||||
|
unreadCount:
|
||||||
|
widget.unreadCountsByPublicKey[room.publicKeyHex] ??
|
||||||
|
0,
|
||||||
isSelected: _isSelected('room', room),
|
isSelected: _isSelected('room', room),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
widget.onSelect('room', room);
|
widget.onSelect('room', room);
|
||||||
@@ -314,6 +342,7 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
|||||||
required Contact contact,
|
required Contact contact,
|
||||||
required String title,
|
required String title,
|
||||||
required String subtitle,
|
required String subtitle,
|
||||||
|
required int unreadCount,
|
||||||
required bool isSelected,
|
required bool isSelected,
|
||||||
required VoidCallback onTap,
|
required VoidCallback onTap,
|
||||||
}) {
|
}) {
|
||||||
@@ -339,13 +368,95 @@ class _RecipientSelectorSheetState extends State<RecipientSelectorSheet> {
|
|||||||
).textTheme.bodySmall?.color?.withValues(alpha: 0.6),
|
).textTheme.bodySmall?.color?.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
trailing: isSelected
|
trailing: _buildTrailing(
|
||||||
? Icon(
|
context,
|
||||||
Icons.check_circle,
|
unreadCount: unreadCount,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
isSelected: isSelected,
|
||||||
)
|
),
|
||||||
: null,
|
|
||||||
onTap: onTap,
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,15 +366,6 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sender.routeSupportsLegacyRawTransport) {
|
|
||||||
_clearRequestState();
|
|
||||||
await _showBlockingAlert(
|
|
||||||
'Cannot fetch voice',
|
|
||||||
'Sender route uses 3-byte hashes. Raw media fetch is not supported in this client yet.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sender.routeHopCount >= 2) {
|
if (sender.routeHopCount >= 2) {
|
||||||
_showToast(
|
_showToast(
|
||||||
'Voice fetch over ${sender.routeHopCount} hops may take a while.',
|
'Voice fetch over ${sender.routeHopCount} hops may take a while.',
|
||||||
@@ -415,10 +406,7 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
|||||||
missingIndices: missing,
|
missingIndices: missing,
|
||||||
requesterKey6: requesterKey6,
|
requesterKey6: requesterKey6,
|
||||||
)
|
)
|
||||||
: VoiceFetchRequest(
|
: VoiceFetchRequest(sessionId: sessionId, requesterKey6: requesterKey6);
|
||||||
sessionId: sessionId,
|
|
||||||
requesterKey6: requesterKey6,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
@@ -426,7 +414,7 @@ class _VoiceMessageBubbleState extends State<VoiceMessageBubble> {
|
|||||||
);
|
);
|
||||||
await connectionProvider.sendRawVoicePacket(
|
await connectionProvider.sendRawVoicePacket(
|
||||||
contactPath: sender.outPath,
|
contactPath: sender.outPath,
|
||||||
contactPathLen: sender.routeSignedPathLen,
|
contactPathLen: sender.routeEncodedPathLen,
|
||||||
payload: request.encodeBinary(),
|
payload: request.encodeBinary(),
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|||||||
@@ -796,7 +796,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: main
|
ref: main
|
||||||
resolved-ref: cea66b5251135c7f9b84f15c0878d4c8af6e88e9
|
resolved-ref: bd3744ee21376b81be5f852cd0c1a82c0df40460
|
||||||
url: "https://github.com/dz0ny/meshcore_client.git"
|
url: "https://github.com/dz0ny/meshcore_client.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
@@ -1265,10 +1265,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_android
|
name: sqflite_android
|
||||||
sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88
|
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2+2"
|
version: "2.4.2+3"
|
||||||
sqflite_common:
|
sqflite_common:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -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
|
# 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
|
# 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.
|
# 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:
|
environment:
|
||||||
sdk: ^3.9.2
|
sdk: ^3.9.2
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ void main() {
|
|||||||
expect(contact.routeHashSize, 3);
|
expect(contact.routeHashSize, 3);
|
||||||
expect(contact.routeHopCount, 2);
|
expect(contact.routeHopCount, 2);
|
||||||
expect(contact.routeCanonicalText, 'AABBCC,DDEEFF');
|
expect(contact.routeCanonicalText, 'AABBCC,DDEEFF');
|
||||||
expect(contact.routeSupportsLegacyRawTransport, isFalse);
|
expect(contact.routeSupportsLegacyRawTransport, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('treats -1 as unknown route', () {
|
test('treats -1 as unknown route', () {
|
||||||
|
|||||||
@@ -47,6 +47,63 @@ void main() {
|
|||||||
expect(ok, isFalse);
|
expect(ok, isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('returns false when requester has no learned path', () async {
|
||||||
|
final ok = await serveCachedSessionFragments<_Fragment>(
|
||||||
|
providerLabel: 'TestProvider',
|
||||||
|
sessionId: 'deadbeef',
|
||||||
|
requester: _buildContact(outPathLen: -1),
|
||||||
|
fragments: [
|
||||||
|
_Fragment(0, Uint8List.fromList([1])),
|
||||||
|
],
|
||||||
|
maxDirectPayloadHops: 3,
|
||||||
|
indexOf: (f) => f.index,
|
||||||
|
encodeBinary: (f) => f.payload,
|
||||||
|
sendRawPacket:
|
||||||
|
({
|
||||||
|
required contactPath,
|
||||||
|
required contactPathLen,
|
||||||
|
required payload,
|
||||||
|
}) async {},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ok, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false when requester path payload is empty', () async {
|
||||||
|
final requester = Contact(
|
||||||
|
publicKey: Uint8List.fromList(List<int>.generate(32, (i) => i)),
|
||||||
|
type: ContactType.chat,
|
||||||
|
flags: 0,
|
||||||
|
outPathLen: 1,
|
||||||
|
outPath: Uint8List(0),
|
||||||
|
advName: 'Requester',
|
||||||
|
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
advLat: 0,
|
||||||
|
advLon: 0,
|
||||||
|
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
final ok = await serveCachedSessionFragments<_Fragment>(
|
||||||
|
providerLabel: 'TestProvider',
|
||||||
|
sessionId: 'deadbeef',
|
||||||
|
requester: requester,
|
||||||
|
fragments: [
|
||||||
|
_Fragment(0, Uint8List.fromList([1])),
|
||||||
|
],
|
||||||
|
maxDirectPayloadHops: 3,
|
||||||
|
indexOf: (f) => f.index,
|
||||||
|
encodeBinary: (f) => f.payload,
|
||||||
|
sendRawPacket:
|
||||||
|
({
|
||||||
|
required contactPath,
|
||||||
|
required contactPathLen,
|
||||||
|
required payload,
|
||||||
|
}) async {},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ok, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
test('sends only requested indices', () async {
|
test('sends only requested indices', () async {
|
||||||
final sent = <Uint8List>[];
|
final sent = <Uint8List>[];
|
||||||
final ok = await serveCachedSessionFragments<_Fragment>(
|
final ok = await serveCachedSessionFragments<_Fragment>(
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import 'package:fake_async/fake_async.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:meshcore_sar_app/models/contact.dart';
|
import 'package:meshcore_sar_app/models/contact.dart';
|
||||||
import 'package:meshcore_sar_app/models/message.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:meshcore_sar_app/providers/messages_provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
Contact _buildContact() {
|
Contact _buildContact() {
|
||||||
return Contact(
|
return Contact(
|
||||||
@@ -39,6 +42,10 @@ Message _buildDirectMessage(String id) {
|
|||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
});
|
||||||
|
|
||||||
group('MessagesProvider retransmission', () {
|
group('MessagesProvider retransmission', () {
|
||||||
test('direct messages become sent before delivery ACK arrives', () {
|
test('direct messages become sent before delivery ACK arrives', () {
|
||||||
final provider = MessagesProvider();
|
final provider = MessagesProvider();
|
||||||
@@ -171,7 +178,11 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(retryCalls, 0);
|
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();
|
async.flushMicrotasks();
|
||||||
|
|
||||||
expect(retryCalls, 1);
|
expect(retryCalls, 1);
|
||||||
@@ -237,68 +248,89 @@ void main() {
|
|||||||
expect(provider.messages.single.retryAttempt, 0);
|
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 provider = MessagesProvider();
|
||||||
final contact = _buildContact();
|
final fallbackCalls = <String>[];
|
||||||
final resetRequests = <(String, int)>[];
|
provider.onFinalRouterFallbackCallback =
|
||||||
provider.onDirectPathFailedCallback =
|
({required messageId, required contact, required message}) async {
|
||||||
({required contact, required failureStreak}) async {
|
fallbackCalls.add(messageId);
|
||||||
resetRequests.add((contact.advName, failureStreak));
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
provider.addSentMessage(
|
provider.addSentMessage(
|
||||||
_buildDirectMessage(
|
_buildDirectMessage(
|
||||||
'm5',
|
'm5',
|
||||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
).copyWith(retryAttempt: MessageRetryManager.maxRetryAttempts),
|
||||||
contact: contact,
|
contact: _buildContact(),
|
||||||
);
|
);
|
||||||
provider.markMessageFailed('m5');
|
|
||||||
|
|
||||||
|
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();
|
||||||
provider.addSentMessage(
|
provider.addSentMessage(
|
||||||
_buildDirectMessage(
|
_buildDirectMessage(
|
||||||
'm6',
|
'm6',
|
||||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
).copyWith(retryAttempt: MessageRetryManager.maxRetryAttempts),
|
||||||
contact: contact,
|
contact: _buildContact(),
|
||||||
|
);
|
||||||
|
provider.updateMessageRouteSelection(
|
||||||
|
'm6',
|
||||||
|
PathSelection.flood(),
|
||||||
|
routerFallbackAttempted: true,
|
||||||
);
|
);
|
||||||
provider.markMessageFailed('m6');
|
|
||||||
|
|
||||||
|
provider.markMessageFailed('m6');
|
||||||
await Future<void>.delayed(Duration.zero);
|
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 {
|
test(
|
||||||
|
'final permanent failure callback runs after router fallback failure',
|
||||||
|
() async {
|
||||||
final provider = MessagesProvider();
|
final provider = MessagesProvider();
|
||||||
final contact = _buildContact();
|
final failedMessageIds = <String>[];
|
||||||
final resetRequests = <int>[];
|
provider.onFinalDirectMessageFailureCallback =
|
||||||
provider.onDirectPathFailedCallback =
|
({required messageId, required contact, required message}) async {
|
||||||
({required contact, required failureStreak}) async {
|
failedMessageIds.add(messageId);
|
||||||
resetRequests.add(failureStreak);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
provider.addSentMessage(
|
provider.addSentMessage(
|
||||||
_buildDirectMessage(
|
_buildDirectMessage(
|
||||||
'm7',
|
'm7',
|
||||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
).copyWith(retryAttempt: MessageRetryManager.maxRetryAttempts),
|
||||||
contact: contact,
|
contact: _buildContact(),
|
||||||
);
|
);
|
||||||
|
provider.updateMessageRouteSelection(
|
||||||
|
'm7',
|
||||||
|
PathSelection.flood(),
|
||||||
|
routerFallbackAttempted: true,
|
||||||
|
);
|
||||||
|
|
||||||
provider.markMessageFailed('m7');
|
provider.markMessageFailed('m7');
|
||||||
|
|
||||||
provider.addSentMessage(_buildDirectMessage('m8'), contact: contact);
|
|
||||||
provider.markMessageSent('m8', 123, 10);
|
|
||||||
provider.markMessageDelivered(123, 150);
|
|
||||||
|
|
||||||
provider.addSentMessage(
|
|
||||||
_buildDirectMessage(
|
|
||||||
'm9',
|
|
||||||
).copyWith(retryAttempt: 3, usedFloodFallback: true),
|
|
||||||
contact: contact,
|
|
||||||
);
|
|
||||||
provider.markMessageFailed('m9');
|
|
||||||
|
|
||||||
await Future<void>.delayed(Duration.zero);
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
|
||||||
expect(resetRequests, isEmpty);
|
expect(failedMessageIds, ['m7']);
|
||||||
});
|
expect(
|
||||||
|
provider.messages.single.deliveryStatus,
|
||||||
|
MessageDeliveryStatus.failed,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter_test/flutter_test.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/message.dart';
|
||||||
import 'package:meshcore_sar_app/models/message_contact_location.dart';
|
import 'package:meshcore_sar_app/models/message_contact_location.dart';
|
||||||
import 'package:meshcore_sar_app/providers/messages_provider.dart';
|
import 'package:meshcore_sar_app/providers/messages_provider.dart';
|
||||||
@@ -11,6 +12,30 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
Contact _buildContact({
|
||||||
|
required List<int> prefix,
|
||||||
|
required ContactType type,
|
||||||
|
String name = 'Test Contact',
|
||||||
|
}) {
|
||||||
|
final key = Uint8List.fromList([
|
||||||
|
...prefix,
|
||||||
|
...List<int>.generate(26, (index) => index),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return Contact(
|
||||||
|
publicKey: key,
|
||||||
|
type: type,
|
||||||
|
flags: 0,
|
||||||
|
outPathLen: 0,
|
||||||
|
outPath: Uint8List(64),
|
||||||
|
advName: name,
|
||||||
|
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
advLat: 0,
|
||||||
|
advLon: 0,
|
||||||
|
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
group('MessagesProvider voice detection', () {
|
group('MessagesProvider voice detection', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
@@ -190,5 +215,60 @@ void main() {
|
|||||||
expect(restoredVoice?.downloaders.single.requesterKey6, '112233445566');
|
expect(restoredVoice?.downloaders.single.requesterKey6, '112233445566');
|
||||||
expect(restoredImage?.downloaders.single.requesterName, equals('Bob'));
|
expect(restoredImage?.downloaders.single.requesterName, equals('Bob'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('marks only the selected contact destination as read', () {
|
||||||
|
final provider = MessagesProvider();
|
||||||
|
final alice = _buildContact(
|
||||||
|
prefix: [0x10, 0x11, 0x12, 0x13, 0x14, 0x15],
|
||||||
|
type: ContactType.chat,
|
||||||
|
name: 'Alice',
|
||||||
|
);
|
||||||
|
final bob = _buildContact(
|
||||||
|
prefix: [0x20, 0x21, 0x22, 0x23, 0x24, 0x25],
|
||||||
|
type: ContactType.chat,
|
||||||
|
name: 'Bob',
|
||||||
|
);
|
||||||
|
|
||||||
|
provider.addMessage(
|
||||||
|
Message(
|
||||||
|
id: 'alice-incoming',
|
||||||
|
messageType: MessageType.contact,
|
||||||
|
pathLen: 1,
|
||||||
|
textType: MessageTextType.plain,
|
||||||
|
senderTimestamp: 1700000100,
|
||||||
|
text: 'Alice unread',
|
||||||
|
receivedAt: DateTime.now(),
|
||||||
|
senderPublicKeyPrefix: alice.publicKey.sublist(0, 6),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
provider.addMessage(
|
||||||
|
Message(
|
||||||
|
id: 'bob-incoming',
|
||||||
|
messageType: MessageType.contact,
|
||||||
|
pathLen: 1,
|
||||||
|
textType: MessageTextType.plain,
|
||||||
|
senderTimestamp: 1700000101,
|
||||||
|
text: 'Bob unread',
|
||||||
|
receivedAt: DateTime.now(),
|
||||||
|
senderPublicKeyPrefix: bob.publicKey.sublist(0, 6),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
provider.markDestinationAsRead(
|
||||||
|
destinationType: 'contact',
|
||||||
|
contact: alice,
|
||||||
|
);
|
||||||
|
|
||||||
|
final aliceMessage = provider.messages.firstWhere(
|
||||||
|
(message) => message.id == 'alice-incoming',
|
||||||
|
);
|
||||||
|
final bobMessage = provider.messages.firstWhere(
|
||||||
|
(message) => message.id == 'bob-incoming',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(aliceMessage.isRead, isTrue);
|
||||||
|
expect(bobMessage.isRead, isFalse);
|
||||||
|
expect(provider.unreadCount, equals(1));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
97
test/services/mesh_map_nodes_service_test.dart
Normal file
97
test/services/mesh_map_nodes_service_test.dart
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:http/testing.dart';
|
||||||
|
import 'package:meshcore_sar_app/services/mesh_map_nodes_service.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
await MeshMapNodesService.clearCache();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fetchNodes persists nodes and cached lookup works offline', () async {
|
||||||
|
final client = MockClient((request) async {
|
||||||
|
expect(
|
||||||
|
request.url.toString(),
|
||||||
|
'https://api.meshcore.nz/api/v1/map/nodes',
|
||||||
|
);
|
||||||
|
return http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'nodes': [
|
||||||
|
{
|
||||||
|
'type': 1,
|
||||||
|
'name': 'Alpha',
|
||||||
|
'public_key': 'aa11bb22',
|
||||||
|
'latitude': 46.05,
|
||||||
|
'longitude': 14.50,
|
||||||
|
'updated_at': 123456,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final nodes = await MeshMapNodesService.fetchNodes(client: client);
|
||||||
|
final cached = await MeshMapNodesService.fetchNodes(allowNetwork: false);
|
||||||
|
|
||||||
|
expect(nodes, hasLength(1));
|
||||||
|
expect(cached, hasLength(1));
|
||||||
|
expect(cached.first.name, 'Alpha');
|
||||||
|
expect(await MeshMapNodesService.hasFreshCache(), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadCachedNodes ignores stale persisted cache', () async {
|
||||||
|
SharedPreferences.setMockInitialValues({
|
||||||
|
'mesh_map_nodes_cache_v1': jsonEncode([
|
||||||
|
{
|
||||||
|
'type': 1,
|
||||||
|
'name': 'Old node',
|
||||||
|
'public_key': 'bb22cc33',
|
||||||
|
'latitude': 46.05,
|
||||||
|
'longitude': 14.50,
|
||||||
|
'updated_at': 123456,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
'mesh_map_nodes_cache_timestamp_v1': DateTime.now()
|
||||||
|
.subtract(const Duration(hours: 25))
|
||||||
|
.millisecondsSinceEpoch,
|
||||||
|
});
|
||||||
|
|
||||||
|
final nodes = await MeshMapNodesService.loadCachedNodes();
|
||||||
|
|
||||||
|
expect(nodes, isEmpty);
|
||||||
|
expect(await MeshMapNodesService.hasFreshCache(), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearCache removes persisted online trace database', () async {
|
||||||
|
final client = MockClient(
|
||||||
|
(_) async => http.Response(
|
||||||
|
jsonEncode({
|
||||||
|
'nodes': [
|
||||||
|
{
|
||||||
|
'type': 1,
|
||||||
|
'name': 'Alpha',
|
||||||
|
'public_key': 'aa11bb22',
|
||||||
|
'latitude': 46.05,
|
||||||
|
'longitude': 14.50,
|
||||||
|
'updated_at': 123456,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await MeshMapNodesService.fetchNodes(client: client);
|
||||||
|
await MeshMapNodesService.clearCache();
|
||||||
|
|
||||||
|
expect(await MeshMapNodesService.loadCachedNodes(), isEmpty);
|
||||||
|
expect(await MeshMapNodesService.cachedAt(), isNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -22,5 +22,13 @@ void main() {
|
|||||||
test('keeps non-hash labels at two characters', () {
|
test('keeps non-hash labels at two characters', () {
|
||||||
expect(AvatarLabelHelper.buildLabel('abc'), 'AB');
|
expect(AvatarLabelHelper.buildLabel('abc'), 'AB');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('strips emoji when building fallback initials', () {
|
||||||
|
expect(AvatarLabelHelper.buildLabel('Charlie 🙂 Delta'), 'CD');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('strips leading emoji when building fallback initials', () {
|
||||||
|
expect(AvatarLabelHelper.buildLabel('🙂 Charlie'), 'CH');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
120
test/utils/contact_grouping_test.dart
Normal file
120
test/utils/contact_grouping_test.dart
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:meshcore_sar_app/models/contact.dart';
|
||||||
|
import 'package:meshcore_sar_app/utils/contact_grouping.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
Contact buildContact({
|
||||||
|
required int seed,
|
||||||
|
required String name,
|
||||||
|
required DateTime lastSeen,
|
||||||
|
}) {
|
||||||
|
return Contact(
|
||||||
|
publicKey: Uint8List.fromList(
|
||||||
|
List<int>.generate(32, (index) => (seed + index) % 255),
|
||||||
|
),
|
||||||
|
type: ContactType.chat,
|
||||||
|
flags: 0,
|
||||||
|
outPathLen: 0,
|
||||||
|
outPath: Uint8List(64),
|
||||||
|
advName: name,
|
||||||
|
lastAdvert: lastSeen.millisecondsSinceEpoch ~/ 1000,
|
||||||
|
advLat: 0,
|
||||||
|
advLon: 0,
|
||||||
|
lastMod: lastSeen.millisecondsSinceEpoch ~/ 1000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
group('ContactGrouping.buildItems', () {
|
||||||
|
test(
|
||||||
|
'groups prefixed contacts only when at least four share the prefix',
|
||||||
|
() {
|
||||||
|
final now = DateTime(2026, 3, 10, 12);
|
||||||
|
final items = ContactGrouping.buildItems([
|
||||||
|
buildContact(seed: 1, name: 'SI-1', lastSeen: now),
|
||||||
|
buildContact(
|
||||||
|
seed: 2,
|
||||||
|
name: 'SI-2',
|
||||||
|
lastSeen: now.subtract(const Duration(minutes: 1)),
|
||||||
|
),
|
||||||
|
buildContact(
|
||||||
|
seed: 3,
|
||||||
|
name: 'SI-3',
|
||||||
|
lastSeen: now.subtract(const Duration(minutes: 2)),
|
||||||
|
),
|
||||||
|
buildContact(
|
||||||
|
seed: 4,
|
||||||
|
name: 'SI-4',
|
||||||
|
lastSeen: now.subtract(const Duration(minutes: 3)),
|
||||||
|
),
|
||||||
|
buildContact(
|
||||||
|
seed: 5,
|
||||||
|
name: 'OTHER',
|
||||||
|
lastSeen: now.subtract(const Duration(minutes: 4)),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(items, hasLength(2));
|
||||||
|
expect(items.first.isGroup, isTrue);
|
||||||
|
expect(items.first.group!.label, 'SI-');
|
||||||
|
expect(
|
||||||
|
items.first.group!.contacts.map((contact) => contact.displayName),
|
||||||
|
['SI-1', 'SI-2', 'SI-3', 'SI-4'],
|
||||||
|
);
|
||||||
|
expect(items.last.contact!.displayName, 'OTHER');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('does not group when only three contacts share a prefix', () {
|
||||||
|
final now = DateTime(2026, 3, 10, 12);
|
||||||
|
final items = ContactGrouping.buildItems([
|
||||||
|
buildContact(seed: 1, name: 'SI-1', lastSeen: now),
|
||||||
|
buildContact(
|
||||||
|
seed: 2,
|
||||||
|
name: 'SI-2',
|
||||||
|
lastSeen: now.subtract(const Duration(minutes: 1)),
|
||||||
|
),
|
||||||
|
buildContact(
|
||||||
|
seed: 3,
|
||||||
|
name: 'SI-3',
|
||||||
|
lastSeen: now.subtract(const Duration(minutes: 2)),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(items, hasLength(3));
|
||||||
|
expect(items.every((item) => !item.isGroup), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('orders groups and ungrouped contacts by latest last seen', () {
|
||||||
|
final now = DateTime(2026, 3, 10, 12);
|
||||||
|
final items = ContactGrouping.buildItems([
|
||||||
|
buildContact(
|
||||||
|
seed: 1,
|
||||||
|
name: 'Lone',
|
||||||
|
lastSeen: now.subtract(const Duration(minutes: 1)),
|
||||||
|
),
|
||||||
|
buildContact(seed: 2, name: 'SI-1', lastSeen: now),
|
||||||
|
buildContact(
|
||||||
|
seed: 3,
|
||||||
|
name: 'SI-2',
|
||||||
|
lastSeen: now.subtract(const Duration(minutes: 2)),
|
||||||
|
),
|
||||||
|
buildContact(
|
||||||
|
seed: 4,
|
||||||
|
name: 'SI-3',
|
||||||
|
lastSeen: now.subtract(const Duration(minutes: 3)),
|
||||||
|
),
|
||||||
|
buildContact(
|
||||||
|
seed: 5,
|
||||||
|
name: 'SI-4',
|
||||||
|
lastSeen: now.subtract(const Duration(minutes: 4)),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(items, hasLength(2));
|
||||||
|
expect(items.first.isGroup, isTrue);
|
||||||
|
expect(items.last.contact!.displayName, 'Lone');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -24,15 +24,64 @@ void main() {
|
|||||||
|
|
||||||
expect(decoded, isNotNull);
|
expect(decoded, isNotNull);
|
||||||
expect(decoded!.payloadType, 0x01);
|
expect(decoded!.payloadType, 0x01);
|
||||||
expect(decoded.pathHashes, [0xc2, 0xba, 0x5f, 0xde]);
|
expect(decoded.pathDescriptor, 0x04);
|
||||||
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('parses encoded descriptor with 2-byte hashes', () {
|
||||||
|
final packet = Uint8List.fromList([
|
||||||
|
0x88,
|
||||||
|
0x37,
|
||||||
|
0xae,
|
||||||
|
0x05,
|
||||||
|
0x42,
|
||||||
|
0xc2,
|
||||||
|
0xba,
|
||||||
|
0x5f,
|
||||||
|
0xde,
|
||||||
|
0x5c,
|
||||||
|
]);
|
||||||
|
|
||||||
|
final decoded = LogRxRouteDecoder.decode(packet);
|
||||||
|
|
||||||
|
expect(decoded, isNotNull);
|
||||||
|
expect(decoded!.pathDescriptor, 0x42);
|
||||||
|
expect(decoded.pathBytes, [0xc2, 0xba, 0x5f, 0xde]);
|
||||||
|
expect(decoded.hashSize, 2);
|
||||||
|
expect(decoded.hopCount, 2);
|
||||||
|
expect(decoded.hopHashes, ['c2ba', '5fde']);
|
||||||
|
});
|
||||||
|
|
||||||
|
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', () {
|
group('LogRxRouteDecoder.resolveHash', () {
|
||||||
test('prefers own node when hash matches device key', () {
|
test('prefers own node when hash matches device key', () {
|
||||||
final resolved = LogRxRouteDecoder.resolveHash(
|
final resolved = LogRxRouteDecoder.resolveHash(
|
||||||
0xc2,
|
'c201',
|
||||||
contacts: const [],
|
contacts: const [],
|
||||||
ownPublicKey: Uint8List.fromList([0xc2, 0x01, 0x02]),
|
ownPublicKey: Uint8List.fromList([0xc2, 0x01, 0x02]),
|
||||||
ownName: 'Base',
|
ownName: 'Base',
|
||||||
@@ -44,8 +93,10 @@ void main() {
|
|||||||
|
|
||||||
test('resolves unique contact by first public key byte', () {
|
test('resolves unique contact by first public key byte', () {
|
||||||
final resolved = LogRxRouteDecoder.resolveHash(
|
final resolved = LogRxRouteDecoder.resolveHash(
|
||||||
0xc2,
|
'c211',
|
||||||
contacts: [_contact(name: 'Alpha', keyPrefix: 0xc2)],
|
contacts: [
|
||||||
|
_contact(name: 'Alpha', keyPrefix: [0xc2, 0x11]),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(resolved.isUniqueMatch, isTrue);
|
expect(resolved.isUniqueMatch, isTrue);
|
||||||
@@ -54,10 +105,10 @@ void main() {
|
|||||||
|
|
||||||
test('marks ambiguous matches without pretending certainty', () {
|
test('marks ambiguous matches without pretending certainty', () {
|
||||||
final resolved = LogRxRouteDecoder.resolveHash(
|
final resolved = LogRxRouteDecoder.resolveHash(
|
||||||
0xc2,
|
'c2',
|
||||||
contacts: [
|
contacts: [
|
||||||
_contact(name: 'Alpha', keyPrefix: 0xc2),
|
_contact(name: 'Alpha', keyPrefix: [0xc2, 0x11]),
|
||||||
_contact(name: 'Bravo', keyPrefix: 0xc2),
|
_contact(name: 'Bravo', keyPrefix: [0xc2, 0x22]),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -67,10 +118,10 @@ void main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Contact _contact({required String name, required int keyPrefix}) {
|
Contact _contact({required String name, required List<int> keyPrefix}) {
|
||||||
return Contact(
|
return Contact(
|
||||||
publicKey: Uint8List.fromList([
|
publicKey: Uint8List.fromList([
|
||||||
keyPrefix,
|
...keyPrefix,
|
||||||
0x11,
|
0x11,
|
||||||
0x22,
|
0x22,
|
||||||
0x33,
|
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: (selectedContact, destinationType) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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