diff --git a/.gitignore b/.gitignore index 779856c..88295e7 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ migrate_working_dir/ pubspec.lock /build/ /coverage/ +# fvm project files +.fvm/ +.fvmrc # Symbolication related app.*.symbols diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index a8934f1..b432277 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -104,6 +104,22 @@ class RepeaterBatterySnapshot { }); } +class MeshCoreRadioStateSnapshot { + final int freqHz; + final int bwHz; + final int sf; + final int cr; + final int txPowerDbm; + + const MeshCoreRadioStateSnapshot({ + required this.freqHz, + required this.bwHz, + required this.sf, + required this.cr, + required this.txPowerDbm, + }); +} + class MeshCoreConnector extends ChangeNotifier { // Message windowing to limit memory usage static const int _messageWindowSize = 200; @@ -169,6 +185,7 @@ class MeshCoreConnector extends ChangeNotifier { int? _currentSf; int? _currentCr; bool? _clientRepeat; + MeshCoreRadioStateSnapshot? _rememberedNonRepeatRadioState; int? _firmwareVerCode; int _pathHashByteWidth = 1; CompanionRadioStats? _latestRadioStats; @@ -196,6 +213,7 @@ class MeshCoreConnector extends ChangeNotifier { static const int _contactMsgBackoffFallbackMs = 5000; static const int _contactMsgBackoffMinMs = 500; static const int _contactMsgBackoffMaxMs = 15000; + int _pollingInterval = 30; bool _batteryRequested = false; bool _awaitingSelfInfo = false; bool _hasReceivedDeviceInfo = false; @@ -326,8 +344,14 @@ class MeshCoreConnector extends ChangeNotifier { List get allContacts => List.unmodifiable([ ..._contacts, - ..._discoveredContacts.where((c) => !c.isActive), + ..._discoveredContacts.where( + (c) => !c.isActive && c.publicKeyHex != selfPublicKeyHex, + ), ]); + + List get allContactsUnfiltered => + List.unmodifiable([..._contacts, ..._discoveredContacts]); + List get discoveredContacts { return List.unmodifiable(_discoveredContacts); } @@ -362,6 +386,8 @@ class MeshCoreConnector extends ChangeNotifier { int? get currentBwHz => _currentBwHz; int? get currentSf => _currentSf; int? get currentCr => _currentCr; + MeshCoreRadioStateSnapshot? get rememberedNonRepeatRadioState => + _rememberedNonRepeatRadioState; bool? get autoAddUsers => _autoAddUsers; bool? get autoAddRepeaters => _autoAddRepeaters; bool? get autoAddRoomServers => _autoAddRoomServers; @@ -373,6 +399,10 @@ class MeshCoreConnector extends ChangeNotifier { int get advertLocationPolicy => _advertLocPolicy; int get multiAcks => _multiAcks; bool? get clientRepeat => _clientRepeat; + void rememberNonRepeatRadioState(MeshCoreRadioStateSnapshot snapshot) { + _rememberedNonRepeatRadioState = snapshot; + } + int? get firmwareVerCode => _firmwareVerCode; Map? get currentCustomVars => _currentCustomVars; int? get batteryMillivolts => _batteryMillivolts; @@ -2271,6 +2301,7 @@ class MeshCoreConnector extends ChangeNotifier { _selfLatitude = null; _selfLongitude = null; _clientRepeat = null; + _rememberedNonRepeatRadioState = null; _firmwareVerCode = null; _batteryMillivolts = null; _repeaterBatterySnapshots.clear(); @@ -2368,9 +2399,18 @@ class MeshCoreConnector extends ChangeNotifier { _batteryPollTimer = null; } + void setPollingInterval(int i) { + _pollingInterval = i.clamp(1, 60); + if (isConnected) { + _startRadioStatsPolling(); + } + } + void _startRadioStatsPolling() { _radioStatsPollTimer?.cancel(); - _radioStatsPollTimer = Timer.periodic(const Duration(seconds: 1), (_) { + _radioStatsPollTimer = Timer.periodic(Duration(seconds: _pollingInterval), ( + _, + ) { if (!isConnected) { _stopRadioStatsPolling(); return; @@ -2495,6 +2535,18 @@ class MeshCoreConnector extends ChangeNotifier { }); } + Contact getFromDiscovered(Contact contact) { + final tmp = _discoveredContacts.firstWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + orElse: () => contact, + ); + return contact.copyWith( + rawPacket: tmp.rawPacket, + latitude: tmp.latitude, + longitude: tmp.longitude, + ); + } + Future getContacts({int? since, bool preserveExisting = false}) async { if (!isConnected) return; @@ -3875,7 +3927,9 @@ class MeshCoreConnector extends ChangeNotifier { if (mlTimeout != null) { if (pathLength < 0) { // Flood: trust ML, only enforce firmware formula as floor - return mlTimeout.clamp(physicsMin, mlTimeout); + if (mlTimeout < physicsMin) { + return physicsMin; + } } return mlTimeout.clamp(physicsMin, physicsMax); } @@ -3885,8 +3939,17 @@ class MeshCoreConnector extends ChangeNotifier { } void _handleContact(Uint8List frame, {bool isContact = true}) { - final contact = Contact.fromFrame(frame); - if (contact != null) { + final contactTmp = Contact.fromFrame(frame); + if (contactTmp != null) { + if (listEquals(contactTmp.publicKey, _selfPublicKey)) { + appLogger.info( + 'Ignoring contact with self public key: ${contactTmp.name}', + tag: 'Connector', + ); + removeContact(contactTmp); + return; + } + final contact = getFromDiscovered(contactTmp); _handleDiscovery(contact, frame, noNotify: true, addActive: true); if (contact.type == advTypeRepeater) { diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index b42e3e5..396d78b 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -202,15 +202,15 @@ const int cmdGetChannel = 31; const int cmdSetChannel = 32; const int cmdSendTracePath = 36; const int cmdSetOtherParams = 38; -const int cmdSendAnonReq = 57; const int cmdSendTelemetryReq = 39; const int cmdGetCustomVar = 40; const int cmdSetCustomVar = 41; const int cmdSendBinaryReq = 50; +const int cmdGetStats = 56; +const int cmdSendAnonReq = 57; const int cmdSetAutoAddConfig = 58; const int cmdGetAutoAddConfig = 59; const int cmdSetPathHashMode = 61; -const int cmdGetStats = 56; // Text message types const int txtTypePlain = 0; diff --git a/lib/helpers/gif_helper.dart b/lib/helpers/gif_helper.dart index 8dd187b..5b68e90 100644 --- a/lib/helpers/gif_helper.dart +++ b/lib/helpers/gif_helper.dart @@ -30,7 +30,7 @@ class GifHelper { ).firstMatch(trimmed); return pageMatch?.group(1); } - + /// Encode a GIF in a format that parseGif() can parse. static String encodeGif(String gifId) { return 'g:$gifId'; diff --git a/lib/helpers/link_handler.dart b/lib/helpers/link_handler.dart index b931ca1..c2eae29 100644 --- a/lib/helpers/link_handler.dart +++ b/lib/helpers/link_handler.dart @@ -3,6 +3,7 @@ import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:url_launcher/url_launcher.dart'; import '../l10n/l10n.dart'; import '../utils/platform_info.dart'; +import '../helpers/snack_bar_builder.dart'; class LinkHandler { static TextStyle defaultLinkStyle(BuildContext context, TextStyle base) { @@ -93,21 +94,19 @@ class LinkHandler { final uri = Uri.parse(url); if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_couldNotOpenLink(url)), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_couldNotOpenLink(url)), + backgroundColor: Colors.red, ); } } } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_invalidLink), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_invalidLink), + backgroundColor: Colors.red, ); } } diff --git a/lib/helpers/reaction_helper.dart b/lib/helpers/reaction_helper.dart index 169b1a1..36118ca 100644 --- a/lib/helpers/reaction_helper.dart +++ b/lib/helpers/reaction_helper.dart @@ -109,7 +109,7 @@ class ReactionHelper { return ReactionInfo(targetHash: match.group(1)!, emoji: emoji); } - + /// Encode a reaction message that parseReaction() can parse. static String encodeReaction(String hash, String emojiIndex) { return 'r:$hash:$emojiIndex'; diff --git a/lib/helpers/snack_bar_builder.dart b/lib/helpers/snack_bar_builder.dart new file mode 100644 index 0000000..d7409b6 --- /dev/null +++ b/lib/helpers/snack_bar_builder.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +// showDismissibleSnackBar shows a [SnackBar] with tap to dismiss +// all other properties are default and optional +void showDismissibleSnackBar( + BuildContext context, { + Key? key, + required Widget content, + Color? backgroundColor, + double? elevation, + EdgeInsetsGeometry? margin, + EdgeInsetsGeometry? padding, + double? width, + ShapeBorder? shape, + HitTestBehavior? hitTestBehavior, + SnackBarBehavior? behavior, + SnackBarAction? action, + double? actionOverflowThreshold, + bool? showCloseIcon, + Color? closeIconColor, + Duration? duration, + bool? persist, + Animation? animation, + void Function()? onVisible, + DismissDirection? dismissDirection, + Clip? clipBehavior, +}) { + final messenger = ScaffoldMessenger.of(context); + messenger.showSnackBar( + SnackBar( + key: key, + content: GestureDetector( + onTap: () => messenger.hideCurrentSnackBar(), + child: content, + ), + backgroundColor: backgroundColor, + elevation: elevation, + margin: margin, + padding: padding, + width: width, + shape: shape, + hitTestBehavior: hitTestBehavior, + behavior: behavior, + action: action, + actionOverflowThreshold: actionOverflowThreshold, + showCloseIcon: showCloseIcon, + closeIconColor: closeIconColor, + duration: duration ?? const Duration(seconds: 4), + persist: persist, + animation: animation, + onVisible: onVisible, + dismissDirection: dismissDirection ?? DismissDirection.down, + clipBehavior: clipBehavior ?? Clip.hardEdge, + ), + ); +} diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 13e9de7..718bfd9 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -2042,10 +2042,6 @@ } } }, - "scanner_linuxPairingPinTitle": "PIN за съвпадение чрез Bluetooth", - "scanner_linuxPairingPinPrompt": "Въведете PIN кода за {deviceName} (оставете празно, ако няма такъв).", - "scanner_linuxPairingHidePin": "Скриване на PIN кода", - "scanner_linuxPairingShowPin": "Покажи PIN", "@translation_translateTo": { "placeholders": { "language": { @@ -2059,5 +2055,19 @@ "translation_composerEnabledHint": "Съобщенията ще бъдат преведени, преди да бъдат изпратени.", "translation_translateTo": "Превеждане на {language}", "translation_translationOptions": "Опции за превод", - "translation_systemLanguage": "Език на системата" + "translation_systemLanguage": "Език на системата", + "scanner_linuxPairingPinTitle": "PIN за съвпадение чрез Bluetooth", + "scanner_linuxPairingPinPrompt": "Въведете PIN кода за {deviceName} (оставете празно, ако няма такъв).", + "scanner_linuxPairingHidePin": "Скриване на PIN кода", + "scanner_linuxPairingShowPin": "Покажи PIN", + "repeater_cliQuickClockSync": "Синхронизация на часовника", + "repeater_cliQuickDiscovery": "Открий Съседи", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Автоматично изпращайте съобщение \"синхронизиране на часовника\" след успешно влизане.", + "repeater_clockSyncAfterLogin": "Синхронизиране на часовника след влизане" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 62badce..54683d2 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -2070,10 +2070,6 @@ } } }, - "scanner_linuxPairingPinPrompt": "Geben Sie den PIN-Code für {deviceName} ein (lassen Sie das Feld leer, falls kein PIN-Code vorhanden ist).", - "scanner_linuxPairingShowPin": "PIN anzeigen", - "scanner_linuxPairingPinTitle": "PIN für die Bluetooth-Verbindung", - "scanner_linuxPairingHidePin": "PIN verbergen", "@translation_translateTo": { "placeholders": { "language": { @@ -2087,5 +2083,19 @@ "translation_composerDisabledHint": "Nachrichten in der ursprünglichen, getippten Sprache senden.", "translation_translateTo": "Übersetzen Sie auf {language}", "translation_translationOptions": "Übersetzungsmöglichkeiten", - "translation_systemLanguage": "Sprache des Systems" + "translation_systemLanguage": "Sprache des Systems", + "scanner_linuxPairingShowPin": "PIN anzeigen", + "scanner_linuxPairingHidePin": "PIN ausblenden", + "scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN", + "scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine).", + "repeater_cliQuickClockSync": "Uhr Synchronisieren", + "repeater_cliQuickDiscovery": "Entdecke Nachbarn", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLogin": "Uhrzeit-Synchronisation nach dem Anmelden", + "repeater_clockSyncAfterLoginSubtitle": "Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0617553..8ad6bf3 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -303,8 +303,12 @@ "path_routeWeight": "{weight}/{max}", "@path_routeWeight": { "placeholders": { - "weight": { "type": "String" }, - "max": { "type": "String" } + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } } }, "appSettings_battery": "Battery", @@ -603,6 +607,15 @@ "channels_enterHashtag": "Enter hashtag", "channels_hashtagHint": "e.g. #team", "chat_noMessages": "No messages yet", + "chat_sendMessage": "Send message", + "chat_sendMessageTo": "Send message to {name}", + "@chat_sendMessageTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "chat_sendMessageToStart": "Send a message to get started", "chat_originalMessageNotFound": "Original message not found", "chat_replyingTo": "Replying to {name}", @@ -1025,8 +1038,8 @@ "login_enterPassword": "Enter password", "login_savePassword": "Save password", "login_savePasswordSubtitle": "Password will be stored securely on this device", - "login_repeaterDescription": "Enter the repeater password to access settings and status.", - "login_roomDescription": "Enter the room password to access settings and status.", + "login_repeaterDescription": "Enter the repeater password for guest or admin access.", + "login_roomDescription": "Enter the room password for guest or admin access.", "login_routing": "Routing", "login_routingMode": "Routing mode", "login_autoUseSavedPath": "Auto (use saved path)", @@ -1092,7 +1105,10 @@ "path_setPath": "Set Path", "repeater_management": "Repeater Management", "room_management": "Room Server Management", + "repeater_guest": "Repeater Information", + "room_guest": "Room Server Information", "repeater_managementTools": "Management Tools", + "repeater_guestTools": "Guest Tools", "repeater_status": "Status", "repeater_statusSubtitle": "View repeater status, stats, and neighbors", "repeater_telemetry": "Telemetry", @@ -1103,6 +1119,14 @@ "repeater_neighborsSubtitle": "View zero hop neighbors.", "repeater_settings": "Settings", "repeater_settingsSubtitle": "Configure repeater parameters", + "repeater_clockSyncAfterLogin": "Clock sync after login", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "repeater_clockSyncAfterLoginSubtitle": "Automatically send \"clock sync\" after a successful login", + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, "repeater_statusTitle": "Repeater Status", "repeater_routingMode": "Routing mode", "repeater_autoUseSavedPath": "Auto (use saved path)", @@ -1333,6 +1357,8 @@ "repeater_cliQuickVersion": "Version", "repeater_cliQuickAdvertise": "Advertise", "repeater_cliQuickClock": "Clock", + "repeater_cliQuickClockSync": "Clock Sync", + "repeater_cliQuickDiscovery": "Discover Neighbors", "repeater_cliHelpAdvert": "Sends an advertisement packet", "repeater_cliHelpReboot": "Reboots the device. (note, you'll prob get 'Timeout' which is normal)", "repeater_cliHelpClock": "Displays current time per device's clock.", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 4d465bb..6f95d81 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -2087,5 +2087,15 @@ "translation_translateBeforeSending": "Traducir antes de enviar", "translation_translateTo": "Traducir a {language}", "translation_translationOptions": "Opciones de traducción", - "translation_systemLanguage": "Idioma del sistema" + "translation_systemLanguage": "Idioma del sistema", + "repeater_cliQuickDiscovery": "Descubrir Vecinos", + "repeater_cliQuickClockSync": "Sincronización del reloj", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Enviar automáticamente la función de \"sincronización de reloj\" después de un inicio de sesión exitoso.", + "repeater_clockSyncAfterLogin": "Sincronización del reloj después de iniciar sesión" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 16e1d3d..4b0497b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -2042,10 +2042,6 @@ } } }, - "scanner_linuxPairingPinTitle": "Code PIN pour la connexion Bluetooth", - "scanner_linuxPairingHidePin": "Masquer le code PIN", - "scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).", - "scanner_linuxPairingShowPin": "Afficher le code PIN", "@translation_translateTo": { "placeholders": { "language": { @@ -2059,5 +2055,19 @@ "translation_messageTranslation": "Traduction du message", "translation_translateTo": "Traduire en {language}", "translation_translationOptions": "Options de traduction", - "translation_systemLanguage": "Langue du système" + "translation_systemLanguage": "Langue du système", + "scanner_linuxPairingPinTitle": "Code PIN pour la connexion Bluetooth", + "scanner_linuxPairingHidePin": "Masquer le code PIN", + "scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).", + "scanner_linuxPairingShowPin": "Afficher le code PIN", + "repeater_cliQuickClockSync": "Synchronisation de l'horloge", + "repeater_cliQuickDiscovery": "Découvrir les voisins", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Envoyer automatiquement une notification \"synchronisation de l'heure\" après une connexion réussie.", + "repeater_clockSyncAfterLogin": "Synchronisation de l'horloge après la connexion" } diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index cf42e1b..3553b18 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -2081,7 +2081,7 @@ } }, "scanner_linuxPairingShowPin": "Megjelenítse a PIN-kódot", - "scanner_linuxPairingPinPrompt": "Adja meg a PIN kódot a {deviceName} számára (hagyja üresen, ha nincs).", + "scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs).", "scanner_linuxPairingHidePin": "Rejtse el a PIN-kódot", "scanner_linuxPairingPinTitle": "Bluetooth párosítási PIN", "@translation_translateTo": { @@ -2097,5 +2097,15 @@ "translation_composerDisabledHint": "Küldj üzeneteket az eredeti, nyomtatott nyelven.", "translation_translateTo": "Fordítás {language}-ra", "translation_translationOptions": "Fordítási lehetőségek", - "translation_systemLanguage": "Rendszer nyelvé" + "translation_systemLanguage": "Rendszer nyelvé", + "repeater_cliQuickClockSync": "Óra szinkronizálás", + "repeater_cliQuickDiscovery": "Fedezd fel a szomszédokat", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Automatikusan küldje el a \"óra szinkronizálás\" üzenetet a sikeres bejelentkezés után.", + "repeater_clockSyncAfterLogin": "Óra szinkronizálás bejelentkezés után" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index b9676bb..d0e195e 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -2042,10 +2042,6 @@ } } }, - "scanner_linuxPairingPinPrompt": "Inserire il codice PIN per {deviceName} (lasciare vuoto se non presente).", - "scanner_linuxPairingShowPin": "Mostra PIN", - "scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth", - "scanner_linuxPairingHidePin": "Nascondi il PIN", "@translation_translateTo": { "placeholders": { "language": { @@ -2059,5 +2055,19 @@ "translation_composerEnabledHint": "I messaggi verranno tradotti prima di essere inviati.", "translation_translateTo": "Tradurre in {language}", "translation_translationOptions": "Opzioni di traduzione", - "translation_systemLanguage": "Lingua del sistema" + "translation_systemLanguage": "Lingua del sistema", + "scanner_linuxPairingPinPrompt": "Inserire il codice PIN per {deviceName} (lasciare vuoto se non presente).", + "scanner_linuxPairingShowPin": "Mostra PIN", + "scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth", + "scanner_linuxPairingHidePin": "Nascondi il PIN", + "repeater_cliQuickClockSync": "Sincronizzazione dell'orologio", + "repeater_cliQuickDiscovery": "Scopri i Vicini", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Invia automaticamente il comando \"sincronizzazione dell'orologio\" dopo un login riuscito.", + "repeater_clockSyncAfterLogin": "Sincronizzazione dell'orologio dopo il login" } diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 6a9c975..6f85116 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -2080,10 +2080,6 @@ } } }, - "scanner_linuxPairingShowPin": "PINを表示する", - "scanner_linuxPairingHidePin": "PINを非表示にする", - "scanner_linuxPairingPinPrompt": "{deviceName} の PIN を入力してください(該当しない場合は空白で入力)。", - "scanner_linuxPairingPinTitle": "Bluetooth 接続のためのPIN", "@translation_translateTo": { "placeholders": { "language": { @@ -2097,5 +2093,19 @@ "translation_composerDisabledHint": "元のタイプされた言語でメッセージを送信してください。", "translation_translateTo": "{language} への翻訳", "translation_translationOptions": "翻訳の選択肢", - "translation_systemLanguage": "システム言語" + "translation_systemLanguage": "システム言語", + "scanner_linuxPairingShowPin": "PINを表示", + "scanner_linuxPairingHidePin": "PINを非表示", + "scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN", + "scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。", + "repeater_cliQuickClockSync": "クロック同期", + "repeater_cliQuickDiscovery": "近隣を発見する", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLogin": "ログイン後、時計の時刻を同期する", + "repeater_clockSyncAfterLoginSubtitle": "ログインが成功した場合、自動的に「時刻同期」を送信する。" } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 2050e3b..bd73847 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -2097,5 +2097,15 @@ "translation_composerDisabledHint": "원래 작성된 언어로 메시지를 보내세요.", "translation_translateTo": "{language} 번역", "translation_translationOptions": "번역 옵션", - "translation_systemLanguage": "시스템 언어" + "translation_systemLanguage": "시스템 언어", + "repeater_cliQuickClockSync": "시계 동기화", + "repeater_cliQuickDiscovery": "이웃 발견하기", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLogin": "로그인 후 시계 동기화", + "repeater_clockSyncAfterLoginSubtitle": "성공적인 로그인 후, 자동으로 \"시간 동기화\"를 전송합니다." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e2bd2f3..2c1342d 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2296,6 +2296,18 @@ abstract class AppLocalizations { /// **'No messages yet'** String get chat_noMessages; + /// No description provided for @chat_sendMessage. + /// + /// In en, this message translates to: + /// **'Send message'** + String get chat_sendMessage; + + /// No description provided for @chat_sendMessageTo. + /// + /// In en, this message translates to: + /// **'Send a message to {contactName}'** + String chat_sendMessageTo(String contactName); + /// No description provided for @chat_sendMessageToStart. /// /// In en, this message translates to: @@ -2326,12 +2338,6 @@ abstract class AppLocalizations { /// **'Location'** String get chat_location; - /// No description provided for @chat_sendMessageTo. - /// - /// In en, this message translates to: - /// **'Send a message to {contactName}'** - String chat_sendMessageTo(String contactName); - /// No description provided for @chat_typeMessage. /// /// In en, this message translates to: @@ -3432,13 +3438,13 @@ abstract class AppLocalizations { /// No description provided for @login_repeaterDescription. /// /// In en, this message translates to: - /// **'Enter the repeater password to access settings and status.'** + /// **'Enter the repeater password for guest or admin access.'** String get login_repeaterDescription; /// No description provided for @login_roomDescription. /// /// In en, this message translates to: - /// **'Enter the room password to access settings and status.'** + /// **'Enter the room password for guest or admin access.'** String get login_roomDescription; /// No description provided for @login_routing. @@ -3603,12 +3609,30 @@ abstract class AppLocalizations { /// **'Room Server Management'** String get room_management; + /// No description provided for @repeater_guest. + /// + /// In en, this message translates to: + /// **'Repeater Information'** + String get repeater_guest; + + /// No description provided for @room_guest. + /// + /// In en, this message translates to: + /// **'Room Server Information'** + String get room_guest; + /// No description provided for @repeater_managementTools. /// /// In en, this message translates to: /// **'Management Tools'** String get repeater_managementTools; + /// No description provided for @repeater_guestTools. + /// + /// In en, this message translates to: + /// **'Guest Tools'** + String get repeater_guestTools; + /// No description provided for @repeater_status. /// /// In en, this message translates to: @@ -3669,6 +3693,18 @@ abstract class AppLocalizations { /// **'Configure repeater parameters'** String get repeater_settingsSubtitle; + /// Repeater setting: auto sync device clock after successful login + /// + /// In en, this message translates to: + /// **'Clock sync after login'** + String get repeater_clockSyncAfterLogin; + + /// Repeater setting subtitle: describes the clock sync after login behavior + /// + /// In en, this message translates to: + /// **'Automatically send \"clock sync\" after a successful login'** + String get repeater_clockSyncAfterLoginSubtitle; + /// No description provided for @repeater_statusTitle. /// /// In en, this message translates to: @@ -4322,6 +4358,18 @@ abstract class AppLocalizations { /// **'Clock'** String get repeater_cliQuickClock; + /// No description provided for @repeater_cliQuickClockSync. + /// + /// In en, this message translates to: + /// **'Clock Sync'** + String get repeater_cliQuickClockSync; + + /// No description provided for @repeater_cliQuickDiscovery. + /// + /// In en, this message translates to: + /// **'Discover Neighbors'** + String get repeater_cliQuickDiscovery; + /// No description provided for @repeater_cliHelpAdvert. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 283860e..ffb1728 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1239,6 +1239,14 @@ class AppLocalizationsBg extends AppLocalizations { @override String get chat_noMessages => 'Няма съобщения.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Изпрати съобщение на $contactName'; + } + @override String get chat_sendMessageToStart => 'Изпрати съобщение, за да започнеш.'; @@ -1258,11 +1266,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get chat_location => 'Местоположение'; - @override - String chat_sendMessageTo(String contactName) { - return 'Изпрати съобщение на $contactName'; - } - @override String get chat_typeMessage => 'Въведете съобщение...'; @@ -2016,9 +2019,18 @@ class AppLocalizationsBg extends AppLocalizations { @override String get room_management => 'Управление на сървъра за стая'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Инструменти за управление'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Статус'; @@ -2053,6 +2065,14 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_settingsSubtitle => 'Конфигурирайте параметрите на репитера'; + @override + String get repeater_clockSyncAfterLogin => + 'Синхронизиране на часовника след влизане'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Автоматично изпращайте съобщение \"синхронизиране на часовника\" след успешно влизане.'; + @override String get repeater_statusTitle => 'Статус на повтарянето'; @@ -2429,6 +2449,12 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_cliQuickClock => 'Часовник'; + @override + String get repeater_cliQuickClockSync => 'Синхронизация на часовника'; + + @override + String get repeater_cliQuickDiscovery => 'Открий Съседи'; + @override String get repeater_cliHelpAdvert => 'Изпраща рекламен пакет'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index e29ae9e..f58e94a 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1238,6 +1238,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_noMessages => 'Noch keine Nachrichten.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Sende eine Nachricht an $contactName'; + } + @override String get chat_sendMessageToStart => 'Eine Nachricht senden, um anzufangen.'; @@ -1257,11 +1265,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_location => 'Ort'; - @override - String chat_sendMessageTo(String contactName) { - return 'Sende eine Nachricht an $contactName'; - } - @override String get chat_typeMessage => 'Eine Nachricht eingeben...'; @@ -2014,9 +2017,18 @@ class AppLocalizationsDe extends AppLocalizations { @override String get room_management => 'Raum-Server-Verwaltung'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Verwaltungs-Tools'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; @@ -2049,6 +2061,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Repeater-parameter konfigurieren'; + @override + String get repeater_clockSyncAfterLogin => + 'Uhrzeit-Synchronisation nach dem Anmelden'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden.'; + @override String get repeater_statusTitle => 'Repeaterstatus'; @@ -2429,6 +2449,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliQuickClock => 'Uhr'; + @override + String get repeater_cliQuickClockSync => 'Uhr Synchronisieren'; + + @override + String get repeater_cliQuickDiscovery => 'Entdecke Nachbarn'; + @override String get repeater_cliHelpAdvert => 'Sendet eine Ankündigung'; @@ -3652,14 +3678,14 @@ class AppLocalizationsDe extends AppLocalizations { String get scanner_linuxPairingShowPin => 'PIN anzeigen'; @override - String get scanner_linuxPairingHidePin => 'PIN verbergen'; + String get scanner_linuxPairingHidePin => 'PIN ausblenden'; @override - String get scanner_linuxPairingPinTitle => 'PIN für die Bluetooth-Verbindung'; + String get scanner_linuxPairingPinTitle => 'Bluetooth-Paarungs-PIN'; @override String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Geben Sie den PIN-Code für $deviceName ein (lassen Sie das Feld leer, falls kein PIN-Code vorhanden ist).'; + return 'Geben Sie die PIN für $deviceName ein (leer lassen, falls keine).'; } @override diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 877e11d..a2a88b0 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1213,6 +1213,14 @@ class AppLocalizationsEn extends AppLocalizations { @override String get chat_noMessages => 'No messages yet'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Send a message to $contactName'; + } + @override String get chat_sendMessageToStart => 'Send a message to get started'; @@ -1232,11 +1240,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get chat_location => 'Location'; - @override - String chat_sendMessageTo(String contactName) { - return 'Send a message to $contactName'; - } - @override String get chat_typeMessage => 'Type a message...'; @@ -1868,11 +1871,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get login_repeaterDescription => - 'Enter the repeater password to access settings and status.'; + 'Enter the repeater password for guest or admin access.'; @override String get login_roomDescription => - 'Enter the room password to access settings and status.'; + 'Enter the room password for guest or admin access.'; @override String get login_routing => 'Routing'; @@ -1976,9 +1979,18 @@ class AppLocalizationsEn extends AppLocalizations { @override String get room_management => 'Room Server Management'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Management Tools'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; @@ -2011,6 +2023,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Configure repeater parameters'; + @override + String get repeater_clockSyncAfterLogin => 'Clock sync after login'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Automatically send \"clock sync\" after a successful login'; + @override String get repeater_statusTitle => 'Repeater Status'; @@ -2379,6 +2398,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get repeater_cliQuickClock => 'Clock'; + @override + String get repeater_cliQuickClockSync => 'Clock Sync'; + + @override + String get repeater_cliQuickDiscovery => 'Discover Neighbors'; + @override String get repeater_cliHelpAdvert => 'Sends an advertisement packet'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index c963902..bdcf34c 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1238,6 +1238,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_noMessages => 'Aún no hay mensajes'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Enviar un mensaje a $contactName'; + } + @override String get chat_sendMessageToStart => 'Enviar un mensaje para comenzar'; @@ -1257,11 +1265,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_location => 'Ubicación'; - @override - String chat_sendMessageTo(String contactName) { - return 'Enviar un mensaje a $contactName'; - } - @override String get chat_typeMessage => 'Escribe un mensaje...'; @@ -2012,9 +2015,18 @@ class AppLocalizationsEs extends AppLocalizations { @override String get room_management => 'Administración del Servidor de Habitación'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Herramientas de Gestión'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Estado'; @@ -2047,6 +2059,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Configurar parámetros del repetidor'; + @override + String get repeater_clockSyncAfterLogin => + 'Sincronización del reloj después de iniciar sesión'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Enviar automáticamente la función de \"sincronización de reloj\" después de un inicio de sesión exitoso.'; + @override String get repeater_statusTitle => 'Estado del Repetidor'; @@ -2423,6 +2443,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliQuickClock => 'Reloj'; + @override + String get repeater_cliQuickClockSync => 'Sincronización del reloj'; + + @override + String get repeater_cliQuickDiscovery => 'Descubrir Vecinos'; + @override String get repeater_cliHelpAdvert => 'Envía un paquete de publicidad'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index eea88f5..6689efb 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1243,6 +1243,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String get chat_noMessages => 'Aucun message pour le moment.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Envoyer un message à $contactName'; + } + @override String get chat_sendMessageToStart => 'Envoyer un message pour commencer'; @@ -1262,11 +1270,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get chat_location => 'Emplacement'; - @override - String chat_sendMessageTo(String contactName) { - return 'Envoyer un message à $contactName'; - } - @override String get chat_typeMessage => 'Saisir un message...'; @@ -2023,9 +2026,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String get room_management => 'Administrattion Room Server'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Outils de Gestion'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'État'; @@ -2059,6 +2071,14 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_settingsSubtitle => 'Configurer les paramètres du répéteur'; + @override + String get repeater_clockSyncAfterLogin => + 'Synchronisation de l\'horloge après la connexion'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Envoyer automatiquement une notification \"synchronisation de l\'heure\" après une connexion réussie.'; + @override String get repeater_statusTitle => 'État du répéteur'; @@ -2442,6 +2462,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliQuickClock => 'Horloge'; + @override + String get repeater_cliQuickClockSync => 'Synchronisation de l\'horloge'; + + @override + String get repeater_cliQuickDiscovery => 'Découvrir les voisins'; + @override String get repeater_cliHelpAdvert => 'Envoie un paquet d\'annonce'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 5e36e94..a274768 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -1246,6 +1246,14 @@ class AppLocalizationsHu extends AppLocalizations { @override String get chat_noMessages => 'Még nincs üzenet.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Küldj üzenetet $contactName-nek'; + } + @override String get chat_sendMessageToStart => 'Küldj egy üzenetet, hogy elindulj!'; @@ -1265,11 +1273,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get chat_location => 'Helyszín'; - @override - String chat_sendMessageTo(String contactName) { - return 'Küldj üzenetet $contactName-nek'; - } - @override String get chat_typeMessage => 'Írjon üzenetet...'; @@ -2027,9 +2030,18 @@ class AppLocalizationsHu extends AppLocalizations { @override String get room_management => 'Szoba-szerver kezelés'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Menedzsmentes eszközök'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Állapot'; @@ -2063,6 +2075,14 @@ class AppLocalizationsHu extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Állítsa be a repeater paramétereket'; + @override + String get repeater_clockSyncAfterLogin => + 'Óra szinkronizálás bejelentkezés után'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Automatikusan küldje el a \"óra szinkronizálás\" üzenetet a sikeres bejelentkezés után.'; + @override String get repeater_statusTitle => 'Adatkapcsolódás állapot'; @@ -2437,6 +2457,12 @@ class AppLocalizationsHu extends AppLocalizations { @override String get repeater_cliQuickClock => 'óra'; + @override + String get repeater_cliQuickClockSync => 'Óra szinkronizálás'; + + @override + String get repeater_cliQuickDiscovery => 'Fedezd fel a szomszédokat'; + @override String get repeater_cliHelpAdvert => 'Elküldi egy hirdetési csomagot'; @@ -3668,7 +3694,7 @@ class AppLocalizationsHu extends AppLocalizations { @override String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Adja meg a PIN kódot a $deviceName számára (hagyja üresen, ha nincs).'; + return 'Adja meg a(z) $deviceName PIN-kódját (hagyja üresen, ha nincs).'; } @override diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index bb9e0d2..2dae65c 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1239,6 +1239,14 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_noMessages => 'Nessun messaggio ancora'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Invia un messaggio a $contactName'; + } + @override String get chat_sendMessageToStart => 'Invia un messaggio per iniziare'; @@ -1258,11 +1266,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_location => 'Posizione'; - @override - String chat_sendMessageTo(String contactName) { - return 'Invia un messaggio a $contactName'; - } - @override String get chat_typeMessage => 'Digita un messaggio...'; @@ -2013,9 +2016,18 @@ class AppLocalizationsIt extends AppLocalizations { @override String get room_management => 'Gestione del Server di Camera'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Strumenti di Gestione'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Stato'; @@ -2050,6 +2062,14 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_settingsSubtitle => 'Configura i parametri del ripetitore'; + @override + String get repeater_clockSyncAfterLogin => + 'Sincronizzazione dell\'orologio dopo il login'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Invia automaticamente il comando \"sincronizzazione dell\'orologio\" dopo un login riuscito.'; + @override String get repeater_statusTitle => 'Stato del Ripetitore'; @@ -2426,6 +2446,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliQuickClock => 'Orologio'; + @override + String get repeater_cliQuickClockSync => 'Sincronizzazione dell\'orologio'; + + @override + String get repeater_cliQuickDiscovery => 'Scopri i Vicini'; + @override String get repeater_cliHelpAdvert => 'Invia un pacchetto pubblicitario'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 5151ab8..bf47adc 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1179,6 +1179,14 @@ class AppLocalizationsJa extends AppLocalizations { @override String get chat_noMessages => 'まだメッセージは届いていません'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return '$contactName へのメッセージを送信する'; + } + @override String get chat_sendMessageToStart => '開始するためにメッセージを送信してください'; @@ -1198,11 +1206,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get chat_location => '場所'; - @override - String chat_sendMessageTo(String contactName) { - return '$contactName へのメッセージを送信する'; - } - @override String get chat_typeMessage => 'メッセージを入力してください…'; @@ -1929,9 +1932,18 @@ class AppLocalizationsJa extends AppLocalizations { @override String get room_management => 'ルームサーバーの管理'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => '管理ツール'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'ステータス'; @@ -1962,6 +1974,13 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_settingsSubtitle => 'リピーターのパラメータを設定する'; + @override + String get repeater_clockSyncAfterLogin => 'ログイン後、時計の時刻を同期する'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'ログインが成功した場合、自動的に「時刻同期」を送信する。'; + @override String get repeater_statusTitle => '再送ステータス'; @@ -2322,6 +2341,12 @@ class AppLocalizationsJa extends AppLocalizations { @override String get repeater_cliQuickClock => '時計'; + @override + String get repeater_cliQuickClockSync => 'クロック同期'; + + @override + String get repeater_cliQuickDiscovery => '近隣を発見する'; + @override String get repeater_cliHelpAdvert => '広告用資料を送る'; @@ -3468,17 +3493,17 @@ class AppLocalizationsJa extends AppLocalizations { String get translation_enterUrlFirst => 'まず、モデルのURLを入力してください。'; @override - String get scanner_linuxPairingShowPin => 'PINを表示する'; + String get scanner_linuxPairingShowPin => 'PINを表示'; @override - String get scanner_linuxPairingHidePin => 'PINを非表示にする'; + String get scanner_linuxPairingHidePin => 'PINを非表示'; @override - String get scanner_linuxPairingPinTitle => 'Bluetooth 接続のためのPIN'; + String get scanner_linuxPairingPinTitle => 'Bluetooth ペアリング PIN'; @override String scanner_linuxPairingPinPrompt(String deviceName) { - return '$deviceName の PIN を入力してください(該当しない場合は空白で入力)。'; + return '$deviceNameのPINを入力してください(なしの場合は空欄のまま)。'; } @override diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index be64545..ef66cc4 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1174,6 +1174,14 @@ class AppLocalizationsKo extends AppLocalizations { @override String get chat_noMessages => '아직 메시지가 없습니다.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return '$contactName에게 메시지를 보내'; + } + @override String get chat_sendMessageToStart => '시작하려면 메시지를 보내세요.'; @@ -1193,11 +1201,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get chat_location => '위치'; - @override - String chat_sendMessageTo(String contactName) { - return '$contactName에게 메시지를 보내'; - } - @override String get chat_typeMessage => '메시지를 입력하세요...'; @@ -1926,9 +1929,18 @@ class AppLocalizationsKo extends AppLocalizations { @override String get room_management => '방 서버 관리'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => '관리 도구'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => '상태'; @@ -1959,6 +1971,13 @@ class AppLocalizationsKo extends AppLocalizations { @override String get repeater_settingsSubtitle => '리피터 파라미터 설정'; + @override + String get repeater_clockSyncAfterLogin => '로그인 후 시계 동기화'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + '성공적인 로그인 후, 자동으로 \"시간 동기화\"를 전송합니다.'; + @override String get repeater_statusTitle => '반복 장치 상태'; @@ -2319,6 +2338,12 @@ class AppLocalizationsKo extends AppLocalizations { @override String get repeater_cliQuickClock => '시계'; + @override + String get repeater_cliQuickClockSync => '시계 동기화'; + + @override + String get repeater_cliQuickDiscovery => '이웃 발견하기'; + @override String get repeater_cliHelpAdvert => '광고 패킷을 발송'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 86809df..0779ffd 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1227,6 +1227,14 @@ class AppLocalizationsNl extends AppLocalizations { @override String get chat_noMessages => 'Nog geen berichten.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Verstuur een bericht naar $contactName'; + } + @override String get chat_sendMessageToStart => 'Een bericht sturen om te beginnen'; @@ -1246,11 +1254,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get chat_location => 'Locatie'; - @override - String chat_sendMessageTo(String contactName) { - return 'Verstuur een bericht naar $contactName'; - } - @override String get chat_typeMessage => 'Type een bericht...'; @@ -2000,9 +2003,18 @@ class AppLocalizationsNl extends AppLocalizations { @override String get room_management => 'Beheer Server Kamer'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Beheerfuncties'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; @@ -2035,6 +2047,14 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Configureer repeaterparameters'; + @override + String get repeater_clockSyncAfterLogin => + 'Na het inloggen, klok synchroniseren'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Automatisch een \"klok synchroniseren\" bericht versturen na een succesvolle inlog.'; + @override String get repeater_statusTitle => 'Status repeater'; @@ -2409,6 +2429,12 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_cliQuickClock => 'Tijd opvragen'; + @override + String get repeater_cliQuickClockSync => 'Kloksynchronisatie'; + + @override + String get repeater_cliQuickDiscovery => 'Ontdek Buren'; + @override String get repeater_cliHelpAdvert => 'Advertentie uitzenden'; @@ -3626,14 +3652,14 @@ class AppLocalizationsNl extends AppLocalizations { String get scanner_linuxPairingShowPin => 'Toon PIN'; @override - String get scanner_linuxPairingHidePin => 'Verberg PIN'; + String get scanner_linuxPairingHidePin => 'PIN verbergen'; @override - String get scanner_linuxPairingPinTitle => 'PIN voor Bluetooth-koppeling'; + String get scanner_linuxPairingPinTitle => 'Bluetooth‑koppelings‑PIN'; @override String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Voer het pincode-in voor $deviceName in (laat dit leeg als er geen is).'; + return 'Voer PIN in voor $deviceName (laat leeg als er geen is).'; } @override diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 8952815..cfffdf0 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1247,6 +1247,14 @@ class AppLocalizationsPl extends AppLocalizations { @override String get chat_noMessages => 'Brak jeszcze wiadomości'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Wyślij wiadomość do $contactName'; + } + @override String get chat_sendMessageToStart => 'Wyślij wiadomość, aby rozpocząć.'; @@ -1267,11 +1275,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get chat_location => 'Lokalizacja'; - @override - String chat_sendMessageTo(String contactName) { - return 'Wyślij wiadomość do $contactName'; - } - @override String get chat_typeMessage => 'Wpisz wiadomość...'; @@ -2028,9 +2031,18 @@ class AppLocalizationsPl extends AppLocalizations { @override String get room_management => 'Zarządzanie Serwerem Pokoju'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Narzędzia Zarządzania'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; @@ -2063,6 +2075,14 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Skonfiguruj parametry przekaźnika'; + @override + String get repeater_clockSyncAfterLogin => + 'Synchronizacja zegara po zalogowaniu'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Automatycznie wysyłaj powiadomienie \"synchronizacja zegara\" po pomyślnym zalogowaniu.'; + @override String get repeater_statusTitle => 'Status przekaźnika'; @@ -2435,6 +2455,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_cliQuickClock => 'Godzina'; + @override + String get repeater_cliQuickClockSync => 'Synchronizacja zegara'; + + @override + String get repeater_cliQuickDiscovery => 'Odkryj Sąsiadów'; + @override String get repeater_cliHelpAdvert => 'Wysyła pakiet rozgłoszeniowy'; @@ -3654,18 +3680,17 @@ class AppLocalizationsPl extends AppLocalizations { String get translation_enterUrlFirst => 'Najpierw wprowadź adres URL modelu.'; @override - String get scanner_linuxPairingShowPin => 'Wyświetl kod PIN'; + String get scanner_linuxPairingShowPin => 'Pokaż PIN'; @override - String get scanner_linuxPairingHidePin => 'Ukryj kod PIN'; + String get scanner_linuxPairingHidePin => 'Ukryj PIN'; @override - String get scanner_linuxPairingPinTitle => - 'PIN do sparowania przez Bluetooth'; + String get scanner_linuxPairingPinTitle => 'Kod PIN parowania Bluetooth'; @override String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Wprowadź kod PIN dla $deviceName (pust, jeśli nie jest wymagany).'; + return 'Wprowadź kod PIN dla $deviceName (pozostaw puste, jeśli brak).'; } @override diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 43dc27a..2abc403 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1238,6 +1238,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_noMessages => 'Ainda não existem mensagens.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Enviar uma mensagem para $contactName'; + } + @override String get chat_sendMessageToStart => 'Enviar uma mensagem para começar'; @@ -1257,11 +1265,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_location => 'Localização'; - @override - String chat_sendMessageTo(String contactName) { - return 'Enviar uma mensagem para $contactName'; - } - @override String get chat_typeMessage => 'Digite uma mensagem...'; @@ -2012,9 +2015,18 @@ class AppLocalizationsPt extends AppLocalizations { @override String get room_management => 'Gerenciamento de Servidor de Sala'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Ferramentas de Gerenciamento'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; @@ -2047,6 +2059,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Configurar parâmetros do repetidor'; + @override + String get repeater_clockSyncAfterLogin => + 'Sincronização do relógio após o login'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Enviar automaticamente a sincronização do \"relógio\" após um login bem-sucedido.'; + @override String get repeater_statusTitle => 'Status do Repetidor'; @@ -2423,6 +2443,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliQuickClock => 'Relógio'; + @override + String get repeater_cliQuickClockSync => 'Sincronização do Relógio'; + + @override + String get repeater_cliQuickDiscovery => 'Descobrir Vizinhos'; + @override String get repeater_cliHelpAdvert => 'Envia um pacote de anúncios'; @@ -3640,14 +3666,14 @@ class AppLocalizationsPt extends AppLocalizations { String get scanner_linuxPairingShowPin => 'Mostrar PIN'; @override - String get scanner_linuxPairingHidePin => 'Esconder o PIN'; + String get scanner_linuxPairingHidePin => 'Ocultar PIN'; @override - String get scanner_linuxPairingPinTitle => 'PIN de pareamento Bluetooth'; + String get scanner_linuxPairingPinTitle => 'PIN de emparelhamento Bluetooth'; @override String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Insira o código PIN para $deviceName (deixe em branco se não houver).'; + return 'Insira o PIN para $deviceName (deixe em branco se não houver).'; } @override diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 703d80d..8002011 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1238,6 +1238,14 @@ class AppLocalizationsRu extends AppLocalizations { @override String get chat_noMessages => 'Сообщений пока нет'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Отправить сообщение $contactName'; + } + @override String get chat_sendMessageToStart => 'Отправьте сообщение, чтобы начать'; @@ -1257,11 +1265,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get chat_location => 'Местоположение'; - @override - String chat_sendMessageTo(String contactName) { - return 'Отправить сообщение $contactName'; - } - @override String get chat_typeMessage => 'Напишите сообщение...'; @@ -2016,9 +2019,18 @@ class AppLocalizationsRu extends AppLocalizations { @override String get room_management => 'Управление сервером комнат'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Инструменты управления'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Статус'; @@ -2051,6 +2063,14 @@ class AppLocalizationsRu extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Настройка параметров репитера'; + @override + String get repeater_clockSyncAfterLogin => + 'Синхронизация часов после входа в систему'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации.'; + @override String get repeater_statusTitle => 'Статус репитера'; @@ -2427,6 +2447,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get repeater_cliQuickClock => 'Время'; + @override + String get repeater_cliQuickClockSync => 'Синхронизация часов'; + + @override + String get repeater_cliQuickDiscovery => 'Обнаружить Соседей'; + @override String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования'; @@ -3651,18 +3677,17 @@ class AppLocalizationsRu extends AppLocalizations { String get translation_enterUrlFirst => 'Сначала введите URL модели.'; @override - String get scanner_linuxPairingShowPin => 'Показать PIN-код'; + String get scanner_linuxPairingShowPin => 'Показать PIN'; @override - String get scanner_linuxPairingHidePin => 'Скрыть PIN-код'; + String get scanner_linuxPairingHidePin => 'Скрыть PIN'; @override - String get scanner_linuxPairingPinTitle => - 'PIN для сопряжения устройств по Bluetooth'; + String get scanner_linuxPairingPinTitle => 'PIN‑код сопряжения Bluetooth'; @override String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Введите PIN-код для $deviceName (оставьте поле пустым, если PIN-код отсутствует).'; + return 'Введите PIN‑код для $deviceName (оставьте пустым, если нет).'; } @override diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 980657d..aceaa69 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1226,6 +1226,14 @@ class AppLocalizationsSk extends AppLocalizations { @override String get chat_noMessages => 'Zatiaľ žiadne správy.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Pošli správu $contactName'; + } + @override String get chat_sendMessageToStart => 'Pošlite správu na začiatok'; @@ -1245,11 +1253,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get chat_location => 'Lokalita'; - @override - String chat_sendMessageTo(String contactName) { - return 'Pošli správu $contactName'; - } - @override String get chat_typeMessage => 'Napište správu...'; @@ -2001,9 +2004,18 @@ class AppLocalizationsSk extends AppLocalizations { @override String get room_management => 'Správa servera miestnosti'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Nástroje na správu'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; @@ -2036,6 +2048,14 @@ class AppLocalizationsSk extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Konfigurujte parametre opakovača'; + @override + String get repeater_clockSyncAfterLogin => + 'Synchronizácia hodiniek po prihlávení'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Automaticky posielajte notifikáciu \"synchronizácia času\" po úspešnom prihládení.'; + @override String get repeater_statusTitle => 'Status opakého zboru'; @@ -2406,6 +2426,12 @@ class AppLocalizationsSk extends AppLocalizations { @override String get repeater_cliQuickClock => 'Hodiny'; + @override + String get repeater_cliQuickClockSync => 'Synchronizácia hodin'; + + @override + String get repeater_cliQuickDiscovery => 'Objaviť susedov'; + @override String get repeater_cliHelpAdvert => 'Odosiela reklamnú balíček.'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index ad2a278..7f1d320 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1224,6 +1224,14 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_noMessages => 'Še ni sporočil.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Pošlji sporočilo $contactName'; + } + @override String get chat_sendMessageToStart => 'Pošlji sporočilo za začetek.'; @@ -1244,11 +1252,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_location => 'Lokacija'; - @override - String chat_sendMessageTo(String contactName) { - return 'Pošlji sporočilo $contactName'; - } - @override String get chat_typeMessage => 'Vnesi sporočilo...'; @@ -1998,9 +2001,18 @@ class AppLocalizationsSl extends AppLocalizations { @override String get room_management => 'Upravljanje stremlišča'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Upravne orodje'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; @@ -2035,6 +2047,13 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_settingsSubtitle => 'Konfigurirajte parametre ponovitelja'; + @override + String get repeater_clockSyncAfterLogin => 'Sinhronizacija ure po prijavi'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Samodejno po uspešnem vstopu pošljite obvestilo o sinhronizaciji časa.'; + @override String get repeater_statusTitle => 'Status ponovitelja'; @@ -2409,6 +2428,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliQuickClock => 'Ura'; + @override + String get repeater_cliQuickClockSync => 'Usklajevanje ure'; + + @override + String get repeater_cliQuickDiscovery => 'Odkrijte sosede'; + @override String get repeater_cliHelpAdvert => 'Pošlje paket oglasov'; @@ -3624,15 +3649,14 @@ class AppLocalizationsSl extends AppLocalizations { String get scanner_linuxPairingShowPin => 'Prikaži PIN'; @override - String get scanner_linuxPairingHidePin => 'Skrijte PIN'; + String get scanner_linuxPairingHidePin => 'Skrij PIN'; @override - String get scanner_linuxPairingPinTitle => - 'PIN za združevanje preko Bluetootha'; + String get scanner_linuxPairingPinTitle => 'Bluetooth PIN za seznanjanje'; @override String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Vnesite PIN kodo za $deviceName (ostavite prazno, če nimate kode).'; + return 'Vnesite PIN za $deviceName (pustite prazno, če ga ni).'; } @override diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index cc590c2..efa4d60 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1217,6 +1217,14 @@ class AppLocalizationsSv extends AppLocalizations { @override String get chat_noMessages => 'Inga meddelanden ännu'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Skicka ett meddelande till $contactName'; + } + @override String get chat_sendMessageToStart => 'Skicka ett meddelande för att komma igång'; @@ -1238,11 +1246,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get chat_location => 'Plats'; - @override - String chat_sendMessageTo(String contactName) { - return 'Skicka ett meddelande till $contactName'; - } - @override String get chat_typeMessage => 'Skriv ett meddelande...'; @@ -1987,9 +1990,18 @@ class AppLocalizationsSv extends AppLocalizations { @override String get room_management => 'Rumserverhantering'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Administrationsverktyg'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Status'; @@ -2022,6 +2034,14 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Konfigurera återspolarparametrar'; + @override + String get repeater_clockSyncAfterLogin => + 'Synkronisera klockan efter inloggning'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Automatiskt skicka \"klocksynkronisering\" efter en lyckad inloggning.'; + @override String get repeater_statusTitle => 'Återspelsstatus'; @@ -2394,6 +2414,12 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliQuickClock => 'Klocka'; + @override + String get repeater_cliQuickClockSync => 'Synkronisera klocka'; + + @override + String get repeater_cliQuickDiscovery => 'Upptäck grannar'; + @override String get repeater_cliHelpAdvert => 'Skickar ett annonspaket'; @@ -3599,17 +3625,17 @@ class AppLocalizationsSv extends AppLocalizations { 'Ange först en URL för en specifik modell.'; @override - String get scanner_linuxPairingShowPin => 'Visa PIN-kod'; + String get scanner_linuxPairingShowPin => 'Visa PIN'; @override - String get scanner_linuxPairingHidePin => 'Dölj PIN-kod'; + String get scanner_linuxPairingHidePin => 'Dölj PIN'; @override - String get scanner_linuxPairingPinTitle => 'PIN-kod för Bluetooth-anslutning'; + String get scanner_linuxPairingPinTitle => 'Bluetooth‑parnings‑PIN'; @override String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Ange PIN-kod för $deviceName (lämna tomt om ingen finns).'; + return 'Ange PIN för $deviceName (lämna tomt om ingen).'; } @override diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index dd7bf63..6134c64 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1230,6 +1230,14 @@ class AppLocalizationsUk extends AppLocalizations { @override String get chat_noMessages => 'Поки немає повідомлень.'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Надіслати повідомлення $contactName'; + } + @override String get chat_sendMessageToStart => 'Надішліть повідомлення, щоб почати'; @@ -1250,11 +1258,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get chat_location => 'Розташування'; - @override - String chat_sendMessageTo(String contactName) { - return 'Надіслати повідомлення $contactName'; - } - @override String get chat_typeMessage => 'Введіть повідомлення...'; @@ -2011,9 +2014,18 @@ class AppLocalizationsUk extends AppLocalizations { @override String get room_management => 'Адміністрування сервера кімнати'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => 'Інструменти керування'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => 'Статус'; @@ -2047,6 +2059,13 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_settingsSubtitle => 'Налаштувати параметри ретранслятора'; + @override + String get repeater_clockSyncAfterLogin => 'Синхронізація годин після входу'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => + 'Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.'; + @override String get repeater_statusTitle => 'Статус ретранслятора'; @@ -2427,6 +2446,12 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_cliQuickClock => 'Годинник'; + @override + String get repeater_cliQuickClockSync => 'Синхронізація годинника'; + + @override + String get repeater_cliQuickDiscovery => 'Відкрити сусідів'; + @override String get repeater_cliHelpAdvert => 'Надсилає пакет оголошення'; @@ -3655,18 +3680,17 @@ class AppLocalizationsUk extends AppLocalizations { String get translation_enterUrlFirst => 'Спочатку введіть URL моделі.'; @override - String get scanner_linuxPairingShowPin => 'Показати PIN-код'; + String get scanner_linuxPairingShowPin => 'Показати PIN'; @override - String get scanner_linuxPairingHidePin => 'Приховати PIN-код'; + String get scanner_linuxPairingHidePin => 'Приховати PIN'; @override - String get scanner_linuxPairingPinTitle => - 'PIN для з\'єднання через Bluetooth'; + String get scanner_linuxPairingPinTitle => 'PIN‑код спарювання Bluetooth'; @override String scanner_linuxPairingPinPrompt(String deviceName) { - return 'Введіть PIN-код для $deviceName (залиште поле порожнім, якщо немає).'; + return 'Введіть PIN для $deviceName (залиште порожнім, якщо його немає).'; } @override diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 8910dcd..56f235c 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1161,6 +1161,14 @@ class AppLocalizationsZh extends AppLocalizations { @override String get chat_noMessages => '暂无消息'; + @override + String get chat_sendMessage => 'Send message'; + + @override + String chat_sendMessageTo(String contactName) { + return '发送消息给 $contactName'; + } + @override String get chat_sendMessageToStart => '发送消息开始对话'; @@ -1180,11 +1188,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get chat_location => '位置'; - @override - String chat_sendMessageTo(String contactName) { - return '发送消息给 $contactName'; - } - @override String get chat_typeMessage => '输入消息...'; @@ -1887,9 +1890,18 @@ class AppLocalizationsZh extends AppLocalizations { @override String get room_management => '房间服务器管理'; + @override + String get repeater_guest => 'Repeater Information'; + + @override + String get room_guest => 'Room Server Information'; + @override String get repeater_managementTools => '管理工具'; + @override + String get repeater_guestTools => 'Guest Tools'; + @override String get repeater_status => '状态'; @@ -1920,6 +1932,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get repeater_settingsSubtitle => '配置转发节点参数'; + @override + String get repeater_clockSyncAfterLogin => '登录后,自动同步时钟'; + + @override + String get repeater_clockSyncAfterLoginSubtitle => '在成功登录后,自动发送“时钟同步”指令。'; + @override String get repeater_statusTitle => '转发节点状态'; @@ -2277,6 +2295,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get repeater_cliQuickClock => '时钟'; + @override + String get repeater_cliQuickClockSync => '同步时钟'; + + @override + String get repeater_cliQuickDiscovery => '发现邻居'; + @override String get repeater_cliHelpAdvert => '发送广播包'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index cb1a11c..abfd5e7 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -2042,10 +2042,6 @@ } } }, - "scanner_linuxPairingPinTitle": "PIN voor Bluetooth-koppeling", - "scanner_linuxPairingHidePin": "Verberg PIN", - "scanner_linuxPairingPinPrompt": "Voer het pincode-in voor {deviceName} in (laat dit leeg als er geen is).", - "scanner_linuxPairingShowPin": "Toon PIN", "@translation_translateTo": { "placeholders": { "language": { @@ -2059,5 +2055,19 @@ "translation_messageTranslation": "Berichtvertaling", "translation_translationOptions": "Opties voor vertaling", "translation_systemLanguage": "Taal van het systeem", - "translation_translateTo": "Vertalen naar {language}" + "translation_translateTo": "Vertalen naar {language}", + "scanner_linuxPairingShowPin": "Toon PIN", + "scanner_linuxPairingHidePin": "PIN verbergen", + "scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).", + "scanner_linuxPairingPinTitle": "Bluetooth‑koppelings‑PIN", + "repeater_cliQuickDiscovery": "Ontdek Buren", + "repeater_cliQuickClockSync": "Kloksynchronisatie", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Automatisch een \"klok synchroniseren\" bericht versturen na een succesvolle inlog.", + "repeater_clockSyncAfterLogin": "Na het inloggen, klok synchroniseren" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index aa3049f..e626708 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -2080,10 +2080,6 @@ } } }, - "scanner_linuxPairingShowPin": "Wyświetl kod PIN", - "scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pust, jeśli nie jest wymagany).", - "scanner_linuxPairingHidePin": "Ukryj kod PIN", - "scanner_linuxPairingPinTitle": "PIN do sparowania przez Bluetooth", "@translation_translateTo": { "placeholders": { "language": { @@ -2097,5 +2093,19 @@ "translation_messageTranslation": "Tłumaczenie wiadomości", "translation_translationOptions": "Opcje tłumaczenia", "translation_systemLanguage": "Język systemu", - "translation_translateTo": "Tłumacz na {language}" + "translation_translateTo": "Tłumacz na {language}", + "scanner_linuxPairingShowPin": "Pokaż PIN", + "scanner_linuxPairingHidePin": "Ukryj PIN", + "scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pozostaw puste, jeśli brak).", + "scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth", + "repeater_cliQuickClockSync": "Synchronizacja zegara", + "repeater_cliQuickDiscovery": "Odkryj Sąsiadów", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLogin": "Synchronizacja zegara po zalogowaniu", + "repeater_clockSyncAfterLoginSubtitle": "Automatycznie wysyłaj powiadomienie \"synchronizacja zegara\" po pomyślnym zalogowaniu." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index c667cb0..bacc1ca 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -2042,10 +2042,6 @@ } } }, - "scanner_linuxPairingHidePin": "Esconder o PIN", - "scanner_linuxPairingShowPin": "Mostrar PIN", - "scanner_linuxPairingPinTitle": "PIN de pareamento Bluetooth", - "scanner_linuxPairingPinPrompt": "Insira o código PIN para {deviceName} (deixe em branco se não houver).", "@translation_translateTo": { "placeholders": { "language": { @@ -2059,5 +2055,19 @@ "translation_composerDisabledHint": "Envie mensagens no idioma original, conforme digitado.", "translation_translateTo": "Traduzir para {language}", "translation_translationOptions": "Opções de tradução", - "translation_systemLanguage": "Idioma do sistema" + "translation_systemLanguage": "Idioma do sistema", + "scanner_linuxPairingShowPin": "Mostrar PIN", + "scanner_linuxPairingHidePin": "Ocultar PIN", + "scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).", + "scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth", + "repeater_cliQuickClockSync": "Sincronização do Relógio", + "repeater_cliQuickDiscovery": "Descobrir Vizinhos", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Enviar automaticamente a sincronização do \"relógio\" após um login bem-sucedido.", + "repeater_clockSyncAfterLogin": "Sincronização do relógio após o login" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 730cfc9..e4dad42 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1282,10 +1282,6 @@ } } }, - "scanner_linuxPairingPinPrompt": "Введите PIN-код для {deviceName} (оставьте поле пустым, если PIN-код отсутствует).", - "scanner_linuxPairingHidePin": "Скрыть PIN-код", - "scanner_linuxPairingPinTitle": "PIN для сопряжения устройств по Bluetooth", - "scanner_linuxPairingShowPin": "Показать PIN-код", "@translation_translateTo": { "placeholders": { "language": { @@ -1299,5 +1295,19 @@ "translation_composerDisabledHint": "Отправляйте сообщения на языке, в котором они были изначально набраны.", "translation_translateTo": "Перевести на {language}", "translation_translationOptions": "Варианты перевода", - "translation_systemLanguage": "Язык системы" + "translation_systemLanguage": "Язык системы", + "scanner_linuxPairingShowPin": "Показать PIN", + "scanner_linuxPairingPinPrompt": "Введите PIN‑код для {deviceName} (оставьте пустым, если нет).", + "scanner_linuxPairingHidePin": "Скрыть PIN", + "scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth", + "repeater_cliQuickDiscovery": "Обнаружить Соседей", + "repeater_cliQuickClockSync": "Синхронизация часов", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLogin": "Синхронизация часов после входа в систему", + "repeater_clockSyncAfterLoginSubtitle": "Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index cf99ca8..937bacb 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -2059,5 +2059,15 @@ "translation_messageTranslation": "Preklad textu", "translation_translateTo": "Preložte do {language}", "translation_translationOptions": "Možnosti prekladania", - "translation_systemLanguage": "Jazyk systému" + "translation_systemLanguage": "Jazyk systému", + "repeater_cliQuickClockSync": "Synchronizácia hodin", + "repeater_cliQuickDiscovery": "Objaviť susedov", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLogin": "Synchronizácia hodiniek po prihlávení", + "repeater_clockSyncAfterLoginSubtitle": "Automaticky posielajte notifikáciu \"synchronizácia času\" po úspešnom prihládení." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 0c29a86..58d0f9e 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -2042,10 +2042,6 @@ } } }, - "scanner_linuxPairingHidePin": "Skrijte PIN", - "scanner_linuxPairingShowPin": "Prikaži PIN", - "scanner_linuxPairingPinPrompt": "Vnesite PIN kodo za {deviceName} (ostavite prazno, če nimate kode).", - "scanner_linuxPairingPinTitle": "PIN za združevanje preko Bluetootha", "@translation_translateTo": { "placeholders": { "language": { @@ -2059,5 +2055,19 @@ "translation_messageTranslation": "Prevod sporočila", "translation_translateTo": "Prevesti v {language}", "translation_translationOptions": "Možnosti prevoda", - "translation_systemLanguage": "Jezik sistema" + "translation_systemLanguage": "Jezik sistema", + "scanner_linuxPairingShowPin": "Prikaži PIN", + "scanner_linuxPairingHidePin": "Skrij PIN", + "scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).", + "scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje", + "repeater_cliQuickDiscovery": "Odkrijte sosede", + "repeater_cliQuickClockSync": "Usklajevanje ure", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Samodejno po uspešnem vstopu pošljite obvestilo o sinhronizaciji časa.", + "repeater_clockSyncAfterLogin": "Sinhronizacija ure po prijavi" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 3232888..59b27a4 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -2042,10 +2042,6 @@ } } }, - "scanner_linuxPairingPinPrompt": "Ange PIN-kod för {deviceName} (lämna tomt om ingen finns).", - "scanner_linuxPairingPinTitle": "PIN-kod för Bluetooth-anslutning", - "scanner_linuxPairingShowPin": "Visa PIN-kod", - "scanner_linuxPairingHidePin": "Dölj PIN-kod", "@translation_translateTo": { "placeholders": { "language": { @@ -2059,5 +2055,19 @@ "translation_messageTranslation": "Meddelandets översättning", "translation_translateTo": "Översätt till {language}", "translation_translationOptions": "Översättningsalternativ", - "translation_systemLanguage": "Språk för systemet" + "translation_systemLanguage": "Språk för systemet", + "scanner_linuxPairingShowPin": "Visa PIN", + "scanner_linuxPairingPinTitle": "Bluetooth‑parnings‑PIN", + "scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).", + "scanner_linuxPairingHidePin": "Dölj PIN", + "repeater_cliQuickDiscovery": "Upptäck grannar", + "repeater_cliQuickClockSync": "Synkronisera klocka", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Automatiskt skicka \"klocksynkronisering\" efter en lyckad inloggning.", + "repeater_clockSyncAfterLogin": "Synkronisera klockan efter inloggning" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index ddab576..c19f3bd 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -2042,10 +2042,6 @@ } } }, - "scanner_linuxPairingPinTitle": "PIN для з'єднання через Bluetooth", - "scanner_linuxPairingShowPin": "Показати PIN-код", - "scanner_linuxPairingPinPrompt": "Введіть PIN-код для {deviceName} (залиште поле порожнім, якщо немає).", - "scanner_linuxPairingHidePin": "Приховати PIN-код", "@translation_translateTo": { "placeholders": { "language": { @@ -2059,5 +2055,19 @@ "translation_translateBeforeSending": "Перекладіть перед відправкою", "translation_translateTo": "Перекласти на {language}", "translation_translationOptions": "Варіанти перекладу", - "translation_systemLanguage": "Мова системи" + "translation_systemLanguage": "Мова системи", + "scanner_linuxPairingPinTitle": "PIN‑код спарювання Bluetooth", + "scanner_linuxPairingShowPin": "Показати PIN", + "scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).", + "scanner_linuxPairingHidePin": "Приховати PIN", + "repeater_cliQuickClockSync": "Синхронізація годинника", + "repeater_cliQuickDiscovery": "Відкрити сусідів", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLoginSubtitle": "Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.", + "repeater_clockSyncAfterLogin": "Синхронізація годин після входу" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 766be44..3fbfc39 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -2064,5 +2064,15 @@ "translation_translateBeforeSending": "在发送前进行翻译", "translation_translateTo": "翻译成 {language}", "translation_translationOptions": "翻译选项", - "translation_systemLanguage": "系统语言" + "translation_systemLanguage": "系统语言", + "repeater_cliQuickDiscovery": "发现邻居", + "repeater_cliQuickClockSync": "同步时钟", + "@repeater_clockSyncAfterLogin": { + "description": "Repeater setting: auto sync device clock after successful login" + }, + "@repeater_clockSyncAfterLoginSubtitle": { + "description": "Repeater setting subtitle: describes the clock sync after login behavior" + }, + "repeater_clockSyncAfterLogin": "登录后,自动同步时钟", + "repeater_clockSyncAfterLoginSubtitle": "在成功登录后,自动发送“时钟同步”指令。" } diff --git a/lib/screens/app_debug_log_screen.dart b/lib/screens/app_debug_log_screen.dart index 4877038..ca6a6bf 100644 --- a/lib/screens/app_debug_log_screen.dart +++ b/lib/screens/app_debug_log_screen.dart @@ -5,6 +5,7 @@ import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../services/app_debug_log_service.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../helpers/snack_bar_builder.dart'; class AppDebugLogScreen extends StatelessWidget { const AppDebugLogScreen({super.key}); @@ -34,8 +35,9 @@ class AppDebugLogScreen extends StatelessWidget { .join('\n'); await Clipboard.setData(ClipboardData(text: text)); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.debugLog_copied)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.debugLog_copied), ); } : null, diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index cd7fb67..80d8adb 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -10,6 +10,7 @@ import '../services/app_settings_service.dart'; import '../services/notification_service.dart'; import '../services/translation_service.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../helpers/snack_bar_builder.dart'; import 'map_cache_screen.dart'; class AppSettingsScreen extends StatelessWidget { @@ -151,13 +152,12 @@ class AppSettingsScreen extends StatelessWidget { .requestPermissions(); if (!granted) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.appSettings_notificationPermissionDenied, - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.appSettings_notificationPermissionDenied, ), + duration: const Duration(seconds: 2), ); } return; @@ -166,15 +166,14 @@ class AppSettingsScreen extends StatelessWidget { await settingsService.setNotificationsEnabled(value); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - value - ? context.l10n.appSettings_notificationsEnabled - : context.l10n.appSettings_notificationsDisabled, - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + value + ? context.l10n.appSettings_notificationsEnabled + : context.l10n.appSettings_notificationsDisabled, ), + duration: const Duration(seconds: 2), ); } }, @@ -301,15 +300,14 @@ class AppSettingsScreen extends StatelessWidget { value: settingsService.settings.clearPathOnMaxRetry, onChanged: (value) { settingsService.setClearPathOnMaxRetry(value); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - value - ? context.l10n.appSettings_pathsWillBeCleared - : context.l10n.appSettings_pathsWillNotBeCleared, - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + value + ? context.l10n.appSettings_pathsWillBeCleared + : context.l10n.appSettings_pathsWillNotBeCleared, ), + duration: const Duration(seconds: 2), ); }, ), @@ -329,15 +327,14 @@ class AppSettingsScreen extends StatelessWidget { value: settingsService.settings.autoRouteRotationEnabled, onChanged: (value) { settingsService.setAutoRouteRotationEnabled(value); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - value - ? context.l10n.appSettings_autoRouteRotationEnabled - : context.l10n.appSettings_autoRouteRotationDisabled, - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + value + ? context.l10n.appSettings_autoRouteRotationEnabled + : context.l10n.appSettings_autoRouteRotationDisabled, ), + duration: const Duration(seconds: 2), ); }, ), @@ -1065,25 +1062,25 @@ class AppSettingsScreen extends StatelessWidget { children: [ Text(context.l10n.appSettings_showNodesDiscoveredWithin), const SizedBox(height: 16), - ListTile( + RadioListTile( title: Text(context.l10n.appSettings_allTime), - leading: Radio(value: 0), + value: 0, ), - ListTile( + RadioListTile( title: Text(context.l10n.appSettings_lastHour), - leading: Radio(value: 1), + value: 1, ), - ListTile( + RadioListTile( title: Text(context.l10n.appSettings_last6Hours), - leading: Radio(value: 6), + value: 6, ), - ListTile( + RadioListTile( title: Text(context.l10n.appSettings_last24Hours), - leading: Radio(value: 24), + value: 24, ), - ListTile( + RadioListTile( title: Text(context.l10n.appSettings_lastWeek), - leading: Radio(value: 168), + value: 168, ), ], ), @@ -1117,13 +1114,13 @@ class AppSettingsScreen extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - ListTile( + RadioListTile( title: Text(context.l10n.appSettings_unitsMetric), - leading: const Radio(value: UnitSystem.metric), + value: UnitSystem.metric, ), - ListTile( + RadioListTile( title: Text(context.l10n.appSettings_unitsImperial), - leading: const Radio(value: UnitSystem.imperial), + value: UnitSystem.imperial, ), ], ), @@ -1164,8 +1161,9 @@ class AppSettingsScreen extends StatelessWidget { String? id, }) async { if (sourceUrl.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.translation_enterUrlFirst)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.translation_enterUrlFirst), ); return; } @@ -1176,22 +1174,23 @@ class AppSettingsScreen extends StatelessWidget { id: id, ); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.translation_modelDownloaded)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.translation_modelDownloaded), ); await settingsService.setTranslationEnabled(true); } on TranslationDownloadCancelled { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.translation_downloadStopped)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.translation_downloadStopped), ); } catch (error) { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.translation_downloadFailed(error.toString()), - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.translation_downloadFailed(error.toString()), ), ); } @@ -1236,16 +1235,16 @@ class AppSettingsScreen extends StatelessWidget { try { await translationService.removeModel(model); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - // TODO: l10n - content: Text('Deleted ${translationModelFriendlyName(model)}.'), - ), + showDismissibleSnackBar( + context, + // TODO: l10n + content: Text('Deleted ${translationModelFriendlyName(model)}.'), ); } catch (error) { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Delete failed: $error')), + showDismissibleSnackBar( + context, + content: Text('Delete failed: $error'), ); // TODO: l10n } } @@ -1279,15 +1278,14 @@ class AppSettingsScreen extends StatelessWidget { onChanged: (value) async { await settingsService.setAppDebugLogEnabled(value); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - value - ? context.l10n.appSettings_appDebugLoggingEnabled - : context.l10n.appSettings_appDebugLoggingDisabled, - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + value + ? context.l10n.appSettings_appDebugLoggingEnabled + : context.l10n.appSettings_appDebugLoggingDisabled, ), + duration: const Duration(seconds: 2), ); }, ), diff --git a/lib/screens/ble_debug_log_screen.dart b/lib/screens/ble_debug_log_screen.dart index 1009bc4..6d18697 100644 --- a/lib/screens/ble_debug_log_screen.dart +++ b/lib/screens/ble_debug_log_screen.dart @@ -5,6 +5,7 @@ import '../l10n/l10n.dart'; import '../services/ble_debug_log_service.dart'; import '../connector/meshcore_protocol.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../helpers/snack_bar_builder.dart'; enum _BleLogView { frames, rawLogRx } @@ -52,10 +53,9 @@ class _BleDebugLogScreenState extends State { .join('\n'); await Clipboard.setData(ClipboardData(text: text)); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.debugLog_bleCopied), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.debugLog_bleCopied), ); } : null, diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 7beaaf4..e5b5f67 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -14,6 +14,7 @@ import '../connector/meshcore_protocol.dart'; import '../helpers/gif_helper.dart'; import '../helpers/reaction_helper.dart'; import '../helpers/utf8_length_limiter.dart'; +import '../helpers/snack_bar_builder.dart'; import '../l10n/l10n.dart'; import '../models/channel.dart'; import '../models/channel_message.dart'; @@ -144,11 +145,10 @@ class _ChannelChatScreenState extends State { Future _scrollToMessage(String messageId) async { final key = _messageKeys[messageId]; if (key == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_originalMessageNotFound), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_originalMessageNotFound), + duration: const Duration(seconds: 2), ); return; } @@ -1121,6 +1121,7 @@ class _ChannelChatScreenState extends State { const SizedBox(width: 8), IconButton( icon: const Icon(Icons.send), + tooltip: context.l10n.chat_sendMessage, onPressed: _sendMessage, color: Theme.of(context).colorScheme.primary, ), @@ -1150,9 +1151,10 @@ class _ChannelChatScreenState extends State { final now = DateTime.now(); if (_lastChannelSendAt != null && now.difference(_lastChannelSendAt!) < const Duration(seconds: 1)) { - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown))); + content: Text(context.l10n.chat_sendCooldown), + ); return; } _lastChannelSendAt = now; @@ -1194,8 +1196,9 @@ class _ChannelChatScreenState extends State { final maxBytes = maxChannelMessageBytes(connector.selfName); if (utf8.encode(messageText).length > maxBytes) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_messageTooLong(maxBytes)), ); return; } @@ -1322,17 +1325,19 @@ class _ChannelChatScreenState extends State { void _copyMessageText(String text) { Clipboard.setData(ClipboardData(text: text)); - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied))); + content: Text(context.l10n.chat_messageCopied), + ); } Future _deleteMessage(ChannelMessage message) async { await context.read().deleteChannelMessage(message); if (!mounted) return; - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted))); + content: Text(context.l10n.chat_messageDeleted), + ); } String _formatPathPrefixes(Uint8List pathBytes) { diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 94b8eee..53769d4 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -822,7 +822,8 @@ List<_PathHop> _buildPathHops( ) { if (pathBytes.isEmpty) return const []; final candidatesByPrefix = >{}; - for (final contact in connector.allContacts) { + final allContacts = connector.allContacts; + for (final contact in allContacts) { if (contact.publicKey.isEmpty) continue; if (contact.type != advTypeRepeater && contact.type != advTypeRoom) { continue; @@ -839,7 +840,8 @@ List<_PathHop> _buildPathHops( : null; var previousPosition = startPoint; final distance = Distance(); - + var lastDistance = 0.0; + var bestDistance = 0.0; final hops = <_PathHop>[]; for (var i = 0; i < pathBytes.length; i++) { final searchPoint = i == 0 ? startPoint : previousPosition; @@ -848,7 +850,7 @@ List<_PathHop> _buildPathHops( if (candidates != null && candidates.isNotEmpty) { var bestIndex = 0; if (searchPoint != null) { - var bestDistance = double.infinity; + bestDistance = double.infinity; for (var j = 0; j < candidates.length; j++) { final candidate = candidates[j]; if (!candidate.hasLocation || @@ -876,6 +878,16 @@ List<_PathHop> _buildPathHops( if (resolvedPosition != null) { previousPosition = resolvedPosition; } + // If the best candidate is much farther than the previous hop, it's likely not the correct match. + if (lastDistance + bestDistance > 50000 && + candidates != null && + candidates.isNotEmpty) { + i--; + lastDistance = bestDistance; + continue; + } + lastDistance = bestDistance; + hops.add( _PathHop( index: i + 1, diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index d67d03d..44c7a69 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -24,6 +24,7 @@ import '../widgets/empty_state.dart'; import '../widgets/qr_code_display.dart'; import '../widgets/quick_switch_bar.dart'; import '../widgets/unread_badge.dart'; +import '../helpers/snack_bar_builder.dart'; import 'channel_chat_screen.dart'; import 'community_qr_scanner_screen.dart'; import 'contacts_screen.dart'; @@ -127,7 +128,7 @@ class _ChannelsScreenState extends State canPop: allowBack, child: Scaffold( appBar: AppBar( - title: AppBarTitle(context.l10n.channels_title, indicators: false), + title: AppBarTitle(context.l10n.channels_title), centerTitle: true, automaticallyImplyLeading: false, actions: [ @@ -809,15 +810,12 @@ class _ChannelsScreenState extends State onPressed: () async { final name = nameController.text.trim(); if (name.isEmpty) { - ScaffoldMessenger.of( - dialogContext, - ).showSnackBar( - SnackBar( - content: Text( - dialogContext - .l10n - .channels_enterChannelName, - ), + showDismissibleSnackBar( + context, + content: Text( + dialogContext + .l10n + .channels_enterChannelName, ), ); return; @@ -837,13 +835,10 @@ class _ChannelsScreenState extends State nextIndex, ); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.channels_channelAdded( - name, - ), - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.channels_channelAdded(name), ), ); } @@ -897,15 +892,12 @@ class _ChannelsScreenState extends State final name = nameController.text.trim(); final pskHex = pskController.text.trim(); if (name.isEmpty) { - ScaffoldMessenger.of( - dialogContext, - ).showSnackBar( - SnackBar( - content: Text( - dialogContext - .l10n - .channels_enterChannelName, - ), + showDismissibleSnackBar( + context, + content: Text( + dialogContext + .l10n + .channels_enterChannelName, ), ); return; @@ -914,15 +906,12 @@ class _ChannelsScreenState extends State try { psk = Channel.parsePskHex(pskHex); } on FormatException { - ScaffoldMessenger.of( - dialogContext, - ).showSnackBar( - SnackBar( - content: Text( - dialogContext - .l10n - .channels_pskMustBe32Hex, - ), + showDismissibleSnackBar( + context, + content: Text( + dialogContext + .l10n + .channels_pskMustBe32Hex, ), ); return; @@ -930,13 +919,10 @@ class _ChannelsScreenState extends State Navigator.pop(dialogContext); connector.setChannel(nextIndex, name, psk); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.channels_channelAdded( - name, - ), - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.channels_channelAdded(name), ), ); } @@ -967,11 +953,10 @@ class _ChannelsScreenState extends State Navigator.pop(dialogContext); connector.setChannel(nextIndex, 'Public', psk); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.channels_publicChannelAdded, - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.channels_publicChannelAdded, ), ); } @@ -1097,15 +1082,12 @@ class _ChannelsScreenState extends State onPressed: () async { var hashtag = hashtagController.text.trim(); if (hashtag.isEmpty) { - ScaffoldMessenger.of( - dialogContext, - ).showSnackBar( - SnackBar( - content: Text( - dialogContext - .l10n - .channels_enterChannelName, - ), + showDismissibleSnackBar( + context, + content: Text( + dialogContext + .l10n + .channels_enterChannelName, ), ); return; @@ -1125,15 +1107,12 @@ class _ChannelsScreenState extends State } else { // Community hashtag - HMAC derivation from community secret if (selectedCommunity == null) { - ScaffoldMessenger.of( + showDismissibleSnackBar( dialogContext, - ).showSnackBar( - SnackBar( - content: Text( - dialogContext - .l10n - .community_selectCommunity, - ), + content: Text( + dialogContext + .l10n + .community_selectCommunity, ), ); return; @@ -1159,12 +1138,11 @@ class _ChannelsScreenState extends State psk, ); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.channels_channelAdded( - channelName, - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.channels_channelAdded( + channelName, ), ), ); @@ -1259,13 +1237,10 @@ class _ChannelsScreenState extends State onPressed: () async { final name = nameController.text.trim(); if (name.isEmpty) { - ScaffoldMessenger.of( - dialogContext, - ).showSnackBar( - SnackBar( - content: Text( - dialogContext.l10n.community_enterName, - ), + showDismissibleSnackBar( + context, + content: Text( + dialogContext.l10n.community_enterName, ), ); return; @@ -1301,11 +1276,10 @@ class _ChannelsScreenState extends State _loadCommunities(); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.community_created(name), - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.community_created(name), ), ); @@ -1494,10 +1468,9 @@ class _ChannelsScreenState extends State try { psk = Channel.parsePskHex(pskHex); } on FormatException { - ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar( - content: Text(dialogContext.l10n.channels_pskMustBe32Hex), - ), + showDismissibleSnackBar( + dialogContext, + content: Text(dialogContext.l10n.channels_pskMustBe32Hex), ); return; } @@ -1510,16 +1483,16 @@ class _ChannelsScreenState extends State smazEnabled, ); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.channels_channelUpdated(name)), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.channels_channelUpdated(name)), ); } catch (e, st) { debugPrint(st.toString()); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to update channel: $e')), + showDismissibleSnackBar( + context, + content: Text('Failed to update channel: $e'), ); } }, @@ -1559,21 +1532,19 @@ class _ChannelsScreenState extends State if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.channels_channelDeleted(channel.name), - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.channels_channelDeleted(channel.name), ), ); } catch (e, st) { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.channels_channelDeleteFailed(channel.name), - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.channels_channelDeleteFailed(channel.name), ), ); @@ -1594,8 +1565,9 @@ class _ChannelsScreenState extends State void _addPublicChannel(BuildContext context, MeshCoreConnector connector) { final psk = Channel.parsePskHex(Channel.publicChannelPsk); connector.setChannel(0, 'Public', psk); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.channels_publicChannelAdded)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.channels_publicChannelAdded), ); } @@ -1810,12 +1782,9 @@ class _ChannelsScreenState extends State _loadCommunities(); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.community_deleted(community.name), - ), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.community_deleted(community.name)), ); } }, diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 8057f1f..2aee61c 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -43,6 +43,7 @@ import '../widgets/radio_stats_entry.dart'; import '../widgets/translated_message_content.dart'; import '../utils/app_logger.dart'; import '../l10n/l10n.dart'; +import '../helpers/snack_bar_builder.dart'; import 'telemetry_screen.dart'; class ChatScreen extends StatefulWidget { @@ -294,6 +295,7 @@ class _ChatScreenState extends State { tooltip: context.l10n.chat_pathManagement, onPressed: () => _showPathHistory(context), ), + const RadioStatsIconButton(), Consumer( builder: (context, connector, _) { return PopupMenuButton( @@ -366,7 +368,6 @@ class _ChatScreenState extends State { ); }, ), - const RadioStatsIconButton(), ], ), body: Consumer( @@ -591,6 +592,9 @@ class _ChatScreenState extends State { const SizedBox(width: 8), IconButton.filled( icon: const Icon(Icons.send), + tooltip: context.l10n.chat_sendMessageTo( + _resolveContact(connector).name, + ), onPressed: () => _sendMessage(connector), ), ], @@ -630,9 +634,10 @@ class _ChatScreenState extends State { final now = DateTime.now(); if (_lastTextSendAt != null && now.difference(_lastTextSendAt!) < const Duration(seconds: 1)) { - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown))); + content: Text(context.l10n.chat_sendCooldown), + ); return; } _lastTextSendAt = now; @@ -668,8 +673,9 @@ class _ChatScreenState extends State { } final maxBytes = maxContactMessageBytes(); if (utf8.encode(outgoingText).length > maxBytes) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_messageTooLong(maxBytes)), ); return; } @@ -857,15 +863,12 @@ class _ChatScreenState extends State { _showFullPathDialog(context, path.pathBytes), onTap: () async { if (path.pathBytes.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context - .l10n - .chat_pathDetailsNotAvailable, - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.chat_pathDetailsNotAvailable, ), + duration: const Duration(seconds: 2), ); return; } @@ -949,11 +952,10 @@ class _ChatScreenState extends State { _resolveContact(connector), ); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_pathCleared), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_pathCleared), + duration: const Duration(seconds: 2), ); Navigator.pop(context); }, @@ -979,11 +981,10 @@ class _ChatScreenState extends State { pathLen: -1, ); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_floodModeEnabled), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_floodModeEnabled), + duration: const Duration(seconds: 2), ); Navigator.pop(context); }, @@ -1017,11 +1018,10 @@ class _ChatScreenState extends State { void _showFullPathDialog(BuildContext context, List pathBytes) { if (pathBytes.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_pathDetailsNotAvailable), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_pathDetailsNotAvailable), + duration: const Duration(seconds: 2), ); return; } @@ -1134,11 +1134,10 @@ class _ChatScreenState extends State { : (verified ? context.l10n.chat_pathDeviceConfirmed : context.l10n.chat_pathDeviceNotConfirmed); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_pathSetHops(hopCount, status)), - duration: const Duration(seconds: 3), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.chat_pathSetHops(hopCount, status)), + duration: const Duration(seconds: 3), ); } @@ -1487,26 +1486,29 @@ class _ChatScreenState extends State { void _copyMessageText(String text) { Clipboard.setData(ClipboardData(text: text)); - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied))); + content: Text(context.l10n.chat_messageCopied), + ); } Future _deleteMessage(Message message) async { await context.read().deleteMessage(message); if (!mounted) return; - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted))); + content: Text(context.l10n.chat_messageDeleted), + ); } void _retryMessage(Message message) { final connector = Provider.of(context, listen: false); // Retry using the contact's current path override setting connector.sendMessage(_resolveContact(connector), message.text); - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage))); + content: Text(context.l10n.chat_retryingMessage), + ); } void _showEmojiPicker(Message message, Contact senderContact) { diff --git a/lib/screens/community_qr_scanner_screen.dart b/lib/screens/community_qr_scanner_screen.dart index 6852dfa..6b71715 100644 --- a/lib/screens/community_qr_scanner_screen.dart +++ b/lib/screens/community_qr_scanner_screen.dart @@ -8,6 +8,7 @@ import '../models/community.dart'; import '../storage/community_store.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/qr_scanner_widget.dart'; +import '../helpers/snack_bar_builder.dart'; /// Screen for scanning community QR codes to join communities. /// @@ -76,11 +77,10 @@ class _CommunityQrScannerScreenState extends State { } } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.community_invalidQrCode), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.community_invalidQrCode), + backgroundColor: Colors.red, ); } } finally { @@ -93,12 +93,11 @@ class _CommunityQrScannerScreenState extends State { } void _showInvalidQrError(BuildContext context) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.community_invalidQrCode), - backgroundColor: Colors.orange, - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.community_invalidQrCode), + backgroundColor: Colors.orange, + duration: const Duration(seconds: 2), ); } @@ -229,11 +228,10 @@ class _CommunityQrScannerScreenState extends State { } if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.community_joined(community.name)), - backgroundColor: Colors.green, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.community_joined(community.name)), + backgroundColor: Colors.green, ); // Return to previous screen diff --git a/lib/screens/companion_radio_stats_screen.dart b/lib/screens/companion_radio_stats_screen.dart index 01fb64d..9c37676 100644 --- a/lib/screens/companion_radio_stats_screen.dart +++ b/lib/screens/companion_radio_stats_screen.dart @@ -24,6 +24,7 @@ class _CompanionRadioStatsScreenState extends State { final c = context.read(); _connector = c; c.acquireRadioStatsPolling(); + c.setPollingInterval(1); c.radioStatsNotifier.addListener(_onStatsUpdate); } @@ -44,6 +45,7 @@ class _CompanionRadioStatsScreenState extends State { void dispose() { _connector?.radioStatsNotifier.removeListener(_onStatsUpdate); _connector?.releaseRadioStatsPolling(); + _connector?.setPollingInterval(30); super.dispose(); } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index d5b01f2..54d3299 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -27,6 +27,7 @@ import '../widgets/quick_switch_bar.dart'; import '../widgets/repeater_login_dialog.dart'; import '../widgets/room_login_dialog.dart'; import '../widgets/unread_badge.dart'; +import '../helpers/snack_bar_builder.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; import 'discovery_screen.dart'; @@ -150,9 +151,10 @@ class _ContactsScreenState extends State } void _showGroupsUnavailableMessage(BuildContext context) { - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(context.l10n.common_loading))); + content: Text(context.l10n.common_loading), + ); } void _setupFrameListener() { @@ -169,10 +171,9 @@ class _ContactsScreenState extends State // Validate packet has expected minimum size (98+ bytes per protocol) if (advertPacket.length < 98) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_invalidAdvertFormat), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_invalidAdvertFormat), ); } _pendingOperations.remove(ContactOperationType.export); @@ -187,24 +188,23 @@ class _ContactsScreenState extends State if (!mounted) return; if (_pendingOperations.contains(ContactOperationType.import)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_contactImported)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactImported), ); } if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_zeroHopContactAdvertSent), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_zeroHopContactAdvertSent), ); } if (_pendingOperations.contains(ContactOperationType.export)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_contactAdvertCopied), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactAdvertCopied), ); } @@ -216,25 +216,22 @@ class _ContactsScreenState extends State if (!mounted) return; if (_pendingOperations.contains(ContactOperationType.import)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_contactImportFailed), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactImportFailed), ); } if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), ); } if (_pendingOperations.contains(ContactOperationType.export)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_contactAdvertCopyFailed), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactAdvertCopyFailed), ); } @@ -271,8 +268,9 @@ class _ContactsScreenState extends State final clipboardData = await Clipboard.getData('text/plain'); if (clipboardData == null || clipboardData.text == null) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_clipboardEmpty), ); } return; @@ -280,8 +278,9 @@ class _ContactsScreenState extends State final text = clipboardData.text!.trim(); if (!text.startsWith('meshcore://')) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_invalidAdvertFormat), ); } return; @@ -294,8 +293,9 @@ class _ContactsScreenState extends State connector.importContact(importContactFrame); } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_invalidAdvertFormat), ); } } @@ -330,10 +330,9 @@ class _ContactsScreenState extends State ), onTap: () => { connector.sendSelfAdvert(flood: false), - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.settings_advertisementSent), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.settings_advertisementSent), ), }, ), @@ -347,10 +346,9 @@ class _ContactsScreenState extends State ), onTap: () => { connector.sendSelfAdvert(flood: true), - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.settings_advertisementSent), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.settings_advertisementSent), ), }, ), @@ -394,7 +392,7 @@ class _ContactsScreenState extends State children: [ const Icon(Icons.person_add_rounded), const SizedBox(width: 8), - Text("Discovered Contacts"), + Text(context.l10n.discoveredContacts_Title), ], ), onTap: () => Navigator.push( @@ -963,13 +961,16 @@ class _ContactsScreenState extends State context: context, builder: (context) => RepeaterLoginDialog( repeater: repeater, - onLogin: (password) { + onLogin: (password, isAdmin) { // Navigate to repeater hub screen after successful login Navigator.push( context, MaterialPageRoute( - builder: (context) => - RepeaterHubScreen(repeater: repeater, password: password), + builder: (context) => RepeaterHubScreen( + repeater: repeater, + password: password, + isAdmin: isAdmin, + ), ), ); }, @@ -986,14 +987,18 @@ class _ContactsScreenState extends State context: context, builder: (context) => RoomLoginDialog( room: room, - onLogin: (password) { + onLogin: (password, isAdmin) { context.read().markContactRead(room.publicKeyHex); Navigator.push( context, MaterialPageRoute( builder: (context) => destination == RoomLoginDestination.management - ? RepeaterHubScreen(repeater: room, password: password) + ? RepeaterHubScreen( + repeater: room, + password: password, + isAdmin: isAdmin, + ) : ChatScreen(contact: room), ), ); @@ -1146,19 +1151,17 @@ class _ContactsScreenState extends State onPressed: () async { final name = nameController.text.trim(); if (name.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_groupNameRequired), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_groupNameRequired), ); return; } if (name.toLowerCase() == contactsAllGroupsValue.toLowerCase()) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_groupNameReserved), - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_groupNameReserved), ); return; } @@ -1167,11 +1170,10 @@ class _ContactsScreenState extends State return g.name.toLowerCase() == name.toLowerCase(); }); if (exists) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.contacts_groupAlreadyExists(name), - ), + showDismissibleSnackBar( + context, + content: Text( + context.l10n.contacts_groupAlreadyExists(name), ), ); return; @@ -1240,9 +1242,7 @@ class _ContactsScreenState extends State if (isRepeater) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), - title: contact.pathBytesForDisplay.isNotEmpty - ? Text(context.l10n.contacts_pathTrace) - : Text(context.l10n.contacts_ping), + title: Text(context.l10n.contacts_ping), onTap: () { final hw = context .read() @@ -1251,11 +1251,8 @@ class _ContactsScreenState extends State context, MaterialPageRoute( builder: (context) => PathTraceMapScreen( - title: contact.pathBytesForDisplay.isNotEmpty - ? context.l10n.contacts_repeaterPathTrace - : context.l10n.contacts_repeaterPing, - path: contact.pathBytesForDisplay, - flipPathAround: true, + title: context.l10n.contacts_repeaterPing, + path: Uint8List.fromList([contact.publicKey.first]), targetContact: contact, pathHashByteWidth: hw, ), @@ -1274,9 +1271,7 @@ class _ContactsScreenState extends State ] else if (isRoom) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), - title: contact.pathLength > 0 - ? Text(context.l10n.contacts_pathTrace) - : Text(context.l10n.contacts_ping), + title: Text(context.l10n.contacts_pathTrace), onTap: () { final hw = context .read() @@ -1288,7 +1283,9 @@ class _ContactsScreenState extends State title: contact.pathBytesForDisplay.isNotEmpty ? context.l10n.contacts_roomPathTrace : context.l10n.contacts_roomPing, - path: contact.pathBytesForDisplay, + path: contact.pathBytesForDisplay.isNotEmpty + ? contact.pathBytesForDisplay + : Uint8List.fromList([contact.publicKey.first]), flipPathAround: contact.pathBytesForDisplay.isNotEmpty, targetContact: contact, pathHashByteWidth: hw, diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index 4e7c6e8..f9f0e07 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -12,6 +12,7 @@ import '../utils/contact_search.dart'; import '../utils/platform_info.dart'; import '../widgets/app_bar.dart'; import '../widgets/list_filter_widget.dart'; +import '../helpers/snack_bar_builder.dart'; enum DiscoverySortOption { lastSeen, name, type } @@ -38,6 +39,13 @@ class _DiscoveryScreenState extends State { super.dispose(); } + DateTime _resolveLastSeen(Contact contact) { + if (contact.type != advTypeChat) return contact.lastSeen; + return contact.lastMessageAt.isAfter(contact.lastSeen) + ? contact.lastMessageAt + : contact.lastSeen; + } + @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -108,11 +116,56 @@ class _DiscoveryScreenState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - trailing: Text( - _formatLastSeen(context, contact.lastSeen), - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], + // Clamp text scaling in trailing section to prevent overflow while + // maintaining accessibility. Primary content (title/subtitle) scales normally. + trailing: MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear( + MediaQuery.textScalerOf( + context, + ).scale(1.0).clamp(1.0, 1.3), + ), + ), + child: SizedBox( + width: 120, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + _formatLastSeen( + context, + _resolveLastSeen(contact), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (contact.hasLocation) + Icon( + Icons.location_on, + size: 14, + color: Colors.grey[400], + ), + if (contact.rawPacket != null) + const SizedBox(width: 2), + if (contact.rawPacket != null) + Icon( + Icons.cell_tower, + size: 14, + color: Colors.grey[400], + ), + ], + ), + ], + ), ), ), onTap: () { @@ -182,8 +235,9 @@ class _DiscoveryScreenState extends State { final hexString = pubKeyToHex(contact.rawPacket!); Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.contacts_contactAdvertCopied), ); break; case 'delete_contact': diff --git a/lib/screens/map_cache_screen.dart b/lib/screens/map_cache_screen.dart index 1391660..1eb59a8 100644 --- a/lib/screens/map_cache_screen.dart +++ b/lib/screens/map_cache_screen.dart @@ -8,6 +8,7 @@ import '../l10n/l10n.dart'; import '../services/app_settings_service.dart'; import '../services/map_tile_cache_service.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../helpers/snack_bar_builder.dart'; class MapCacheScreen extends StatefulWidget { const MapCacheScreen({super.key}); @@ -112,15 +113,17 @@ class _MapCacheScreenState extends State { Future _startDownload() async { final bounds = _selectedBounds; if (bounds == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.mapCache_selectAreaFirst)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.mapCache_selectAreaFirst), ); return; } if (_estimatedTiles == 0) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.mapCache_noTilesToDownload)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.mapCache_noTilesToDownload), ); return; } @@ -182,9 +185,7 @@ class _MapCacheScreenState extends State { result.failed, ) : context.l10n.mapCache_cachedTiles(result.downloaded); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(message))); + showDismissibleSnackBar(context, content: Text(message)); } Future _clearCache() async { @@ -210,8 +211,9 @@ class _MapCacheScreenState extends State { final cacheService = context.read(); await cacheService.clearCache(); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.mapCache_offlineCacheCleared)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.mapCache_offlineCacheCleared), ); } diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 9616d47..6a8acda 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -29,6 +29,7 @@ import 'chat_screen.dart'; import 'contacts_screen.dart'; import '../widgets/repeater_login_dialog.dart'; import '../widgets/room_login_dialog.dart'; +import '../helpers/snack_bar_builder.dart'; import 'repeater_hub_screen.dart'; import 'settings_screen.dart'; import 'line_of_sight_map_screen.dart'; @@ -64,6 +65,7 @@ class _MapScreenState extends State { bool _hasInitializedMap = false; bool _removedMarkersLoaded = false; final List _pathTrace = []; + final List _pathTraceContacts = []; final List _points = []; final List _polylines = []; bool _legendExpanded = false; @@ -488,7 +490,7 @@ class _MapScreenState extends State { ), ), ), - if (!_isBuildingPathTrace) + if (!settings.mapShowOverlaps) ..._buildGuessedMarker( guessedLocations, showLabels: _showNodeLabels, @@ -788,17 +790,26 @@ class _MapScreenState extends State { final markers = []; for (final guess in guessed) { + if (guess.contact.type == advTypeChat && _isBuildingPathTrace) { + continue; + } + final color = _getNodeColor(guess.contact.type); final marker = Marker( point: guess.position, width: 35, height: 35, child: GestureDetector( - onTap: () => _showNodeInfo( - context, - guess.contact, - guessedPosition: guess.position, - ), + onLongPress: () => _isBuildingPathTrace + ? _showNodeInfo(context, guess.contact) + : null, + onTap: () => _isBuildingPathTrace + ? _addToPath(context, guess.contact, position: guess.position) + : _showNodeInfo( + context, + guess.contact, + guessedPosition: guess.position, + ), child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( @@ -870,23 +881,29 @@ class _MapScreenState extends State { addContact = true; } - final hasOverlap = contacts - .where( - (c) => - c.publicKeyHex != contact.publicKeyHex && - c.publicKey.first == contact.publicKey.first && - (c.type == advTypeRepeater || c.type == advTypeRoom) && - (contact.type == advTypeRepeater || - contact.type == advTypeRoom), - ) - .firstOrNull; - - if (hasOverlap == null && - settings.mapShowOverlaps && - !_isBuildingPathTrace) { + if (contact.type == advTypeChat && _isBuildingPathTrace) { addContact = false; } + if (settings.mapShowOverlaps) { + final hasOverlap = contacts + .where( + (c) => + c.publicKeyHex != contact.publicKeyHex && + c.publicKey.first == contact.publicKey.first && + (c.type == advTypeRepeater || c.type == advTypeRoom) && + (contact.type == advTypeRepeater || + contact.type == advTypeRoom), + ) + .firstOrNull; + + if (hasOverlap == null && + settings.mapShowOverlaps && + !_isBuildingPathTrace) { + addContact = false; + } + } + if (addContact) { filtered.add(contact); } @@ -1350,13 +1367,16 @@ class _MapScreenState extends State { context: context, builder: (context) => RepeaterLoginDialog( repeater: repeater, - onLogin: (password) { + onLogin: (password, isAdmin) { // Navigate to repeater hub screen after successful login Navigator.push( context, MaterialPageRoute( - builder: (context) => - RepeaterHubScreen(repeater: repeater, password: password), + builder: (context) => RepeaterHubScreen( + repeater: repeater, + password: password, + isAdmin: isAdmin, + ), ), ); }, @@ -1369,7 +1389,8 @@ class _MapScreenState extends State { context: context, builder: (context) => RoomLoginDialog( room: room, - onLogin: (password) { + // onLogin(password, isAdmin) isAdmin not used for room caht screen + onLogin: (password, _) { // Navigate to chat screen after successful login context.read().markContactRead(room.publicKeyHex); Navigator.push( @@ -1643,7 +1664,10 @@ class _MapScreenState extends State { ); await connector.refreshDeviceInfo(); if (!mounted) return; - messenger.showSnackBar(SnackBar(content: Text(successMsg))); + showDismissibleSnackBar( + messenger.context, + content: Text(successMsg), + ); }, ), ListTile( @@ -1665,8 +1689,9 @@ class _MapScreenState extends State { required String flags, }) async { if (!connector.isConnected) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.map_connectToShareMarkers)), + showDismissibleSnackBar( + context, + content: Text(context.l10n.map_connectToShareMarkers), ); return; } @@ -2121,12 +2146,18 @@ class _MapScreenState extends State { } } - void _addToPath(BuildContext context, Contact contact) { + void _addToPath(BuildContext context, Contact contact, {LatLng? position}) { setState(() { _pathTrace.add( contact.publicKey[0], ); // Add first 16 bytes of public key to path trace - _points.add(LatLng(contact.latitude!, contact.longitude!)); + _pathTraceContacts.add( + contact.copyWith( + latitude: position?.latitude ?? contact.latitude, + longitude: position?.longitude ?? contact.longitude, + ), + ); // Add contact to path trace contacts + _points.add(position ?? LatLng(contact.latitude!, contact.longitude!)); }); } @@ -2134,6 +2165,7 @@ class _MapScreenState extends State { setState(() { _isBuildingPathTrace = true; _pathTrace.clear(); + _pathTraceContacts.clear(); _points.clear(); _polylines.clear(); _points.add(position); @@ -2142,6 +2174,7 @@ class _MapScreenState extends State { void _removePath() { setState(() { + _pathTraceContacts.removeLast(); _pathTrace.removeLast(); // Remove last node from path trace _points.removeLast(); // Remove last point from points list _polylines.clear(); // Clear polylines @@ -2201,6 +2234,7 @@ class _MapScreenState extends State { title: l10n.contacts_pathTrace, path: Uint8List.fromList(_pathTrace), pathHashByteWidth: hashW, + pathContacts: _pathTraceContacts, ), ), ); @@ -2246,8 +2280,9 @@ class _MapScreenState extends State { _points.clear(); _polylines.clear(); }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.map_pathTraceCancelled)), + showDismissibleSnackBar( + context, + content: Text(l10n.map_pathTraceCancelled), ); }, tooltip: l10n.common_cancel, diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index f4c1673..77559d4 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -11,6 +11,7 @@ import '../connector/meshcore_protocol.dart'; import '../services/repeater_command_service.dart'; import '../widgets/path_management_dialog.dart'; import '../widgets/snr_indicator.dart'; +import '../helpers/snack_bar_builder.dart'; class NeighborsScreen extends StatefulWidget { final Contact repeater; @@ -142,7 +143,7 @@ class _NeighborsScreenState extends State { void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) { final buffer = BufferReader(frame); - final contacts = connector.allContacts; + final contacts = connector.allContactsUnfiltered; try { final neighborCount = buffer.readUInt16LE(); final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE()); @@ -163,11 +164,10 @@ class _NeighborsScreenState extends State { _neighborCount = neighborCount; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.neighbors_receivedData), - backgroundColor: Colors.green, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.neighbors_receivedData), + backgroundColor: Colors.green, ); _statusTimeout?.cancel(); if (!mounted) return; @@ -224,11 +224,10 @@ class _NeighborsScreenState extends State { _isLoading = false; _isLoaded = false; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.neighbors_requestTimedOut), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.neighbors_requestTimedOut), + backgroundColor: Colors.red, ); _recordStatusResult(false); }); @@ -239,11 +238,10 @@ class _NeighborsScreenState extends State { _isLoaded = false; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.neighbors_errorLoading(e.toString())), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.neighbors_errorLoading(e.toString())), + backgroundColor: Colors.red, ); } } diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 5b02931..7f3b4eb 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -56,6 +56,7 @@ class PathTraceMapScreen extends StatefulWidget { final bool reversePathAround; final Contact? targetContact; final int pathHashByteWidth; + final List? pathContacts; const PathTraceMapScreen({ super.key, @@ -66,6 +67,7 @@ class PathTraceMapScreen extends StatefulWidget { this.reversePathAround = false, this.targetContact, this.pathHashByteWidth = pathHashSize, + this.pathContacts, }); @override @@ -74,6 +76,8 @@ class PathTraceMapScreen extends StatefulWidget { class _PathTraceMapScreenState extends State { static const double _labelZoomThreshold = 8.5; + //miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path + static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344; StreamSubscription? _frameSubscription; Timer? _timeoutTimer; @@ -266,17 +270,43 @@ class _PathTraceMapScreenState extends State { .toList(); Map pathContacts = {}; - final contacts = connector.allContacts; - contacts.where((c) => c.type != advTypeChat).forEach((repeater) { - for (var repeaterData in pathData) { - if (listEquals( - repeater.publicKey.sublist(0, 1), - Uint8List.fromList([repeaterData]), - )) { - pathContacts[repeaterData] = repeater; + Contact lastContact = Contact( + path: Uint8List(0), + pathLength: 0, + publicKey: connector.selfPublicKey ?? Uint8List(0), + name: context.l10n.pathTrace_you, + type: advTypeChat, + latitude: connector.selfLatitude, + longitude: connector.selfLongitude, + lastSeen: DateTime.now(), + ); + if (widget.pathContacts != null) { + pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c}; + } else { + final contacts = connector.allContactsUnfiltered; + contacts.where((c) => c.type != advTypeChat).forEach((repeater) { + if (lastContact.latitude != null && + lastContact.longitude != null && + repeater.hasLocation && + lastContact.hasLocation && + Distance().distance( + LatLng(lastContact.latitude!, lastContact.longitude!), + LatLng(repeater.latitude!, repeater.longitude!), + ) > + _maxRepeaterMatchDistanceMeters) { + return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches } - } - }); + for (var repeaterData in pathData) { + if (listEquals( + repeater.publicKey.sublist(0, 1), + Uint8List.fromList([repeaterData]), + )) { + pathContacts[repeaterData] = repeater; + lastContact = repeater; + } + } + }); + } // For hops with no GPS contact, infer position from other contacts // with known GPS that share the same last-hop byte. diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index 52d92aa..5e9a462 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -9,6 +9,7 @@ import '../connector/meshcore_protocol.dart'; import '../widgets/debug_frame_viewer.dart'; import '../services/repeater_command_service.dart'; import '../widgets/path_management_dialog.dart'; +import '../helpers/snack_bar_builder.dart'; class RepeaterCliScreen extends StatefulWidget { final Contact repeater; @@ -35,13 +36,15 @@ class _RepeaterCliScreenState extends State { // Common commands for quick access late final List> _quickCommands = [ + {'labelKey': 'advertise', 'command': 'advert'}, {'labelKey': 'getName', 'command': 'get name'}, {'labelKey': 'getRadio', 'command': 'get radio'}, {'labelKey': 'getTx', 'command': 'get tx'}, + {'labelKey': 'discovery', 'command': 'discover.neighbors'}, {'labelKey': 'neighbors', 'command': 'neighbors'}, {'labelKey': 'version', 'command': 'ver'}, - {'labelKey': 'advertise', 'command': 'advert'}, {'labelKey': 'clock', 'command': 'clock'}, + {'labelKey': 'clock sync', 'command': 'clock sync'}, ]; @override @@ -334,8 +337,9 @@ class _RepeaterCliScreenState extends State { if (_commandController.text.trim().isNotEmpty) { _sendCommand(showDebug: true); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.repeater_enterCommandFirst)), + showDismissibleSnackBar( + context, + content: Text(l10n.repeater_enterCommandFirst), ); } }, @@ -407,6 +411,10 @@ class _RepeaterCliScreenState extends State { return l10n.repeater_cliQuickAdvertise; case 'clock': return l10n.repeater_cliQuickClock; + case 'clock sync': + return l10n.repeater_cliQuickClockSync; + case 'discovery': + return l10n.repeater_cliQuickDiscovery; default: return key; } diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index 8a14253..0dc141c 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -13,11 +13,13 @@ import 'neighbors_screen.dart'; class RepeaterHubScreen extends StatelessWidget { final Contact repeater; final String password; + final bool isAdmin; const RepeaterHubScreen({ super.key, required this.repeater, required this.password, + required this.isAdmin, }); @override @@ -33,11 +35,18 @@ class RepeaterHubScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text( - repeater.type == advTypeRepeater - ? l10n.repeater_management - : l10n.room_management, - ), + if (isAdmin) + Text( + repeater.type == advTypeRepeater + ? l10n.repeater_management + : l10n.room_management, + ), + if (!isAdmin) + Text( + repeater.type == advTypeRepeater + ? l10n.repeater_guest + : l10n.room_guest, + ), Text( repeater.name, style: const TextStyle( @@ -113,64 +122,67 @@ class RepeaterHubScreen extends StatelessWidget { ), ), const SizedBox(height: 24), - Card( - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const Icon(Icons.battery_full), - const SizedBox(width: 10), - Expanded( - child: Text( - l10n.appSettings_batteryChemistry, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, + if (isAdmin) + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.battery_full), + const SizedBox(width: 10), + Expanded( + child: Text( + l10n.appSettings_batteryChemistry, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), ), ), - ), - ], - ), - const SizedBox(height: 12), - DropdownButtonFormField( - initialValue: chemistry, - isExpanded: true, - decoration: const InputDecoration( - border: UnderlineInputBorder(), - isDense: true, + ], ), - onChanged: (value) { - if (value == null) return; - settingsService.setBatteryChemistryForRepeater( - repeater.publicKeyHex, - value, - ); - }, - items: [ - DropdownMenuItem( - value: 'nmc', - child: Text(l10n.appSettings_batteryNmc), + const SizedBox(height: 12), + DropdownButtonFormField( + initialValue: chemistry, + isExpanded: true, + decoration: const InputDecoration( + border: UnderlineInputBorder(), + isDense: true, ), - DropdownMenuItem( - value: 'lifepo4', - child: Text(l10n.appSettings_batteryLifepo4), - ), - DropdownMenuItem( - value: 'lipo', - child: Text(l10n.appSettings_batteryLipo), - ), - ], - ), - ], + onChanged: (value) { + if (value == null) return; + settingsService.setBatteryChemistryForRepeater( + repeater.publicKeyHex, + value, + ); + }, + items: [ + DropdownMenuItem( + value: 'nmc', + child: Text(l10n.appSettings_batteryNmc), + ), + DropdownMenuItem( + value: 'lifepo4', + child: Text(l10n.appSettings_batteryLifepo4), + ), + DropdownMenuItem( + value: 'lipo', + child: Text(l10n.appSettings_batteryLipo), + ), + ], + ), + ], + ), ), ), - ), const SizedBox(height: 24), Text( - l10n.repeater_managementTools, + isAdmin + ? l10n.repeater_managementTools + : l10n.repeater_guestTools, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), @@ -210,26 +222,27 @@ class RepeaterHubScreen extends StatelessWidget { ); }, ), - const SizedBox(height: 12), + if (isAdmin) const SizedBox(height: 12), // CLI button - _buildManagementCard( - context, - icon: Icons.terminal, - title: l10n.repeater_cli, - subtitle: l10n.repeater_cliSubtitle, - color: Colors.green, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RepeaterCliScreen( - repeater: repeater, - password: password, + if (isAdmin) + _buildManagementCard( + context, + icon: Icons.terminal, + title: l10n.repeater_cli, + subtitle: l10n.repeater_cliSubtitle, + color: Colors.green, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RepeaterCliScreen( + repeater: repeater, + password: password, + ), ), - ), - ); - }, - ), + ); + }, + ), const SizedBox(height: 12), // Neighbors button _buildManagementCard( @@ -248,26 +261,27 @@ class RepeaterHubScreen extends StatelessWidget { ); }, ), - const SizedBox(height: 12), + if (isAdmin) const SizedBox(height: 12), // Settings button - _buildManagementCard( - context, - icon: Icons.settings, - title: l10n.repeater_settings, - subtitle: l10n.repeater_settingsSubtitle, - color: Colors.deepOrange, - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => RepeaterSettingsScreen( - repeater: repeater, - password: password, + if (isAdmin) + _buildManagementCard( + context, + icon: Icons.settings, + title: l10n.repeater_settings, + subtitle: l10n.repeater_settingsSubtitle, + color: Colors.deepOrange, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RepeaterSettingsScreen( + repeater: repeater, + password: password, + ), ), - ), - ); - }, - ), + ); + }, + ), ], ), ), diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index 6375e0b..6d0b4e6 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -8,7 +8,9 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../services/app_debug_log_service.dart'; import '../services/repeater_command_service.dart'; +import '../services/storage_service.dart'; import '../widgets/path_management_dialog.dart'; +import '../helpers/snack_bar_builder.dart'; class RepeaterSettingsScreen extends StatefulWidget { final Contact repeater; @@ -25,6 +27,8 @@ class RepeaterSettingsScreen extends StatefulWidget { } class _RepeaterSettingsScreenState extends State { + final StorageService _storage = StorageService(); + bool _isLoading = false; bool _hasChanges = false; bool _refreshingBasic = false; @@ -59,6 +63,7 @@ class _RepeaterSettingsScreenState extends State { bool _repeatEnabled = true; bool _allowReadOnly = true; bool _privacyMode = false; + bool _autoClockSyncAfterLogin = false; // Advertisement settings bool _advertEnable = true; @@ -464,18 +469,16 @@ class _RepeaterSettingsScreenState extends State { if (mounted) { if (successCount > 0) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.repeater_refreshed(label)), - backgroundColor: Colors.green, - ), + showDismissibleSnackBar( + context, + content: Text(l10n.repeater_refreshed(label)), + backgroundColor: Colors.green, ); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.repeater_errorRefreshing(label)), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(l10n.repeater_errorRefreshing(label)), + backgroundColor: Colors.red, ); } @@ -566,6 +569,15 @@ class _RepeaterSettingsScreenState extends State { _lonController.text = widget.repeater.longitude?.toString() ?? ''; } }); + + final autoClockSync = await _storage + .getRepeaterAutoClockSyncAfterLoginEnabled( + widget.repeater.publicKeyHex, + ); + if (!mounted) return; + setState(() { + _autoClockSyncAfterLogin = autoClockSync; + }); } Future _saveSettings() async { @@ -653,11 +665,10 @@ class _RepeaterSettingsScreenState extends State { }); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.repeater_settingsSaved), - backgroundColor: Colors.green, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.repeater_settingsSaved), + backgroundColor: Colors.green, ); } } catch (e) { @@ -666,13 +677,12 @@ class _RepeaterSettingsScreenState extends State { }); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.repeater_errorSavingSettings(e.toString()), - ), - backgroundColor: Colors.red, + showDismissibleSnackBar( + context, + content: Text( + context.l10n.repeater_errorSavingSettings(e.toString()), ), + backgroundColor: Colors.red, ); } } @@ -1139,6 +1149,21 @@ class _RepeaterSettingsScreenState extends State { onRefresh: _refreshAllowReadOnly, refreshTooltip: l10n.repeater_refreshGuestAccess, ), + SwitchListTile( + title: Text(l10n.repeater_clockSyncAfterLogin), + subtitle: Text(l10n.repeater_clockSyncAfterLoginSubtitle), + value: _autoClockSyncAfterLogin, + onChanged: (value) async { + setState(() { + _autoClockSyncAfterLogin = value; + }); + await _storage.setRepeaterAutoClockSyncAfterLoginEnabled( + widget.repeater.publicKeyHex, + value, + ); + }, + contentPadding: EdgeInsets.zero, + ), // Privacy mode - hidden until fully implemented // _buildFeatureToggleRow( // title: l10n.repeater_privacyMode, @@ -1401,9 +1426,10 @@ class _RepeaterSettingsScreenState extends State { if (command == 'erase') { if (mounted) { - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(l10n.repeater_eraseSerialOnly))); + content: Text(l10n.repeater_eraseSerialOnly), + ); } return; } @@ -1425,17 +1451,17 @@ class _RepeaterSettingsScreenState extends State { await connector.sendFrame(frame); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.repeater_commandSent(command))), + showDismissibleSnackBar( + context, + content: Text(l10n.repeater_commandSent(command)), ); } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.repeater_errorSendingCommand(e.toString())), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(l10n.repeater_errorSendingCommand(e.toString())), + backgroundColor: Colors.red, ); } } diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index f938419..720c32a 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -12,6 +12,7 @@ import '../services/app_settings_service.dart'; import '../services/repeater_command_service.dart'; import '../utils/battery_utils.dart'; import '../widgets/path_management_dialog.dart'; +import '../helpers/snack_bar_builder.dart'; class RepeaterStatusScreen extends StatefulWidget { final Contact repeater; @@ -309,11 +310,10 @@ class _RepeaterStatusScreenState extends State { setState(() { _isLoading = false; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.repeater_statusRequestTimeout), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.repeater_statusRequestTimeout), + backgroundColor: Colors.red, ); _recordStatusResult(false); }); @@ -323,13 +323,10 @@ class _RepeaterStatusScreenState extends State { _isLoading = false; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.repeater_errorLoadingStatus(e.toString()), - ), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())), + backgroundColor: Colors.red, ); } _recordStatusResult(false); diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 17f26ea..a503ec0 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -10,6 +10,7 @@ import '../services/linux_ble_error_classifier.dart'; import '../utils/app_logger.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/device_tile.dart'; +import '../helpers/snack_bar_builder.dart'; import 'contacts_screen.dart'; import 'tcp_screen.dart'; import 'usb_screen.dart'; @@ -317,11 +318,10 @@ class _ScannerScreenState extends State { return; } if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.scanner_connectionFailed(e.toString())), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.scanner_connectionFailed(e.toString())), + backgroundColor: Colors.red, ); } } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index d9e0d20..47b9b9c 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:meshcore_open/utils/gpx_export.dart'; import 'package:meshcore_open/widgets/elements_ui.dart'; @@ -8,12 +9,29 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/radio_settings.dart'; +import '../services/app_debug_log_service.dart'; import '../widgets/app_bar.dart'; +import '../helpers/snack_bar_builder.dart'; import 'app_settings_screen.dart'; import 'app_debug_log_screen.dart'; import 'ble_debug_log_screen.dart'; import '../widgets/radio_stats_entry.dart'; +/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others) +/// to the UI enum range (always 5-8). +int _toUiCodingRate(int deviceCr) { + return deviceCr <= 4 ? deviceCr + 4 : deviceCr; +} + +/// Convert UI coding-rate value (5-8) back to firmware encoding. +/// Uses the current device CR to detect which encoding the firmware expects. +int _toDeviceCodingRate(int uiCr, int? deviceCr) { + if (deviceCr != null && deviceCr <= 4) { + return uiCr - 4; + } + return uiCr; +} + class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -496,8 +514,9 @@ class _SettingsScreenState extends State { await connector.setNodeName(controller.text); await connector.refreshDeviceInfo(); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_nodeNameUpdated)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_nodeNameUpdated), ); }, child: Text(l10n.common_save), @@ -611,10 +630,9 @@ class _SettingsScreenState extends State { final interval = int.tryParse(intervalText); if (interval == null || interval < 60 || interval >= 86400) { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.settings_locationIntervalInvalid), - ), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_locationIntervalInvalid), ); return; } @@ -622,8 +640,9 @@ class _SettingsScreenState extends State { await connector.setCustomVar("gps_interval:$interval"); await connector.refreshDeviceInfo(); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_locationUpdated)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_locationUpdated), ); } @@ -643,15 +662,17 @@ class _SettingsScreenState extends State { : currentLon; if (lat == null || lon == null) { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_locationBothRequired)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_locationBothRequired), ); return; } if (lat < -90 || lat > 90 || lon < -180 || lon > 180) { if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_locationInvalid)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_locationInvalid), ); return; } @@ -659,8 +680,9 @@ class _SettingsScreenState extends State { await connector.setNodeLocation(lat: lat, lon: lon); await connector.refreshDeviceInfo(); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_locationUpdated)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_locationUpdated), ); }, child: Text(l10n.common_save), @@ -674,9 +696,10 @@ class _SettingsScreenState extends State { void _syncTime(BuildContext context, MeshCoreConnector connector) { final l10n = context.l10n; connector.syncTime(); - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(l10n.settings_timeSynchronized))); + content: Text(l10n.settings_timeSynchronized), + ); } void _confirmReboot(BuildContext context, MeshCoreConnector connector) { @@ -741,23 +764,27 @@ class _SettingsScreenState extends State { if (!mounted) return; switch (result) { case gpxExportSuccess: - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess))); + content: Text(l10n.settings_gpxExportSuccess), + ); case gpxExportNoContacts: - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_gpxExportNoContacts)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_gpxExportNoContacts), ); break; case gpxExportNotAvailable: - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_gpxExportNotAvailable)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_gpxExportNotAvailable), ); break; case gpxExportFailed: - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError))); + content: Text(l10n.settings_gpxExportError), + ); break; } } @@ -1060,8 +1087,9 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) { ); await connector.refreshDeviceInfo(); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_telemetryModeUpdated)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_telemetryModeUpdated), ); }, child: Text(l10n.common_save), @@ -1088,6 +1116,11 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5; final _txPowerController = TextEditingController(text: '20'); bool _clientRepeat = false; + int? _selectedPresetIndex; + _RadioSettingsSnapshot? _lastNonRepeatSnapshot; + + AppDebugLogService get _appLog => + Provider.of(context, listen: false); @override void initState() { @@ -1139,6 +1172,21 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } _clientRepeat = widget.connector.clientRepeat ?? false; + _selectedPresetIndex = _findMatchingPresetIndex(); + if (_clientRepeat) { + _lastNonRepeatSnapshot = + _sessionRememberedNonRepeatSnapshot() ?? + _inferNonRepeatSnapshotForRepeatEnabled(); + _selectedPresetIndex = _findMatchingPresetIndexForSnapshot( + _lastNonRepeatSnapshot!, + ); + } else { + _lastNonRepeatSnapshot = _nonRepeatSnapshotForCurrentSelection(); + } + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _logRadioSettingsState('Dialog initialized'); + }); } @override @@ -1148,14 +1196,223 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { super.dispose(); } - void _applyPreset(RadioSettings preset) { + void _applyPreset(int index) { setState(() { - _frequencyController.text = preset.frequencyMHz.toString(); - _bandwidth = preset.bandwidth; - _spreadingFactor = preset.spreadingFactor; - _codingRate = preset.codingRate; - _txPowerController.text = preset.txPowerDbm.toString(); + _applyPresetState(index); }); + _logRadioSettingsState( + 'Applied preset ${RadioSettings.presets[index].$1} (#$index)', + ); + } + + int? _findMatchingPresetIndex() { + return _findMatchingPresetIndexForSnapshot(_currentSnapshot()); + } + + int? _findMatchingPresetIndexForSnapshot(_RadioSettingsSnapshot snapshot) { + for (final i in _visiblePresetIndexes()) { + final preset = RadioSettings.presets[i].$2; + if (preset.frequencyHz == snapshot.frequencyHz && + preset.bandwidth == snapshot.bandwidth && + preset.spreadingFactor == snapshot.spreadingFactor && + preset.codingRate == snapshot.codingRate && + preset.txPowerDbm == snapshot.txPowerDbm) { + return i; + } + } + return null; + } + + Iterable _visiblePresetIndexes() sync* { + for (var i = 0; i < RadioSettings.presets.length; i++) { + if (_isOffGridPresetIndex(i)) { + continue; + } + yield i; + } + } + + _RadioSettingsSnapshot _currentSnapshot() { + final frequencyMHz = double.tryParse(_frequencyController.text) ?? 915.0; + final txPowerDbm = int.tryParse(_txPowerController.text) ?? 20; + return _RadioSettingsSnapshot( + frequencyMHz: frequencyMHz, + bandwidth: _bandwidth, + spreadingFactor: _spreadingFactor, + codingRate: _codingRate, + txPowerDbm: txPowerDbm, + ); + } + + bool _isOffGridPresetIndex(int? index) { + if (index == null) return false; + return RadioSettings.presets[index].$1.startsWith('Off-Grid '); + } + + double _offGridFrequencyForBaseFrequency(double baseFrequencyMHz) { + if (baseFrequencyMHz < 500) return 433.0; + if (baseFrequencyMHz < 900) return 869.0; + return 918.0; + } + + double _normalFrequencyForBand(double frequencyMHz) { + if (frequencyMHz < 500) return 433.650; + if (frequencyMHz < 900) return 869.432; + return 915.8; + } + + _RadioSettingsSnapshot _fallbackNonRepeatSnapshot( + double currentFrequencyMHz, + ) { + return _RadioSettingsSnapshot( + frequencyMHz: _normalFrequencyForBand(currentFrequencyMHz), + bandwidth: _bandwidth, + spreadingFactor: _spreadingFactor, + codingRate: _codingRate, + txPowerDbm: int.tryParse(_txPowerController.text) ?? 20, + ); + } + + _RadioSettingsSnapshot _nonRepeatSnapshotForCurrentSelection() { + final current = _currentSnapshot(); + if (!_isOffGridPresetIndex(_selectedPresetIndex)) { + return current; + } + return _fallbackNonRepeatSnapshot(current.frequencyMHz); + } + + _RadioSettingsSnapshot? _sessionRememberedNonRepeatSnapshot() { + final snapshot = widget.connector.rememberedNonRepeatRadioState; + if (snapshot == null) return null; + return _RadioSettingsSnapshot.fromMeshCoreSnapshot(snapshot); + } + + _RadioSettingsSnapshot _inferNonRepeatSnapshotForRepeatEnabled() { + final current = _currentSnapshot(); + for (final i in _visiblePresetIndexes()) { + final preset = RadioSettings.presets[i].$2; + final offGridFreqHz = + (_offGridFrequencyForBaseFrequency(preset.frequencyMHz) * 1000) + .round(); + if (offGridFreqHz == current.frequencyHz && + preset.bandwidth == current.bandwidth && + preset.spreadingFactor == current.spreadingFactor && + preset.codingRate == current.codingRate && + preset.txPowerDbm == current.txPowerDbm) { + return _RadioSettingsSnapshot( + frequencyMHz: preset.frequencyMHz, + bandwidth: preset.bandwidth, + spreadingFactor: preset.spreadingFactor, + codingRate: preset.codingRate, + txPowerDbm: preset.txPowerDbm, + ); + } + } + return _fallbackNonRepeatSnapshot(current.frequencyMHz); + } + + void _applySnapshot(_RadioSettingsSnapshot snapshot) { + _frequencyController.text = snapshot.frequencyMHz.toStringAsFixed(3); + _bandwidth = snapshot.bandwidth; + _spreadingFactor = snapshot.spreadingFactor; + _codingRate = snapshot.codingRate; + _txPowerController.text = snapshot.txPowerDbm.toString(); + } + + void _applyPresetState(int index) { + final preset = RadioSettings.presets[index].$2; + final baseSnapshot = _RadioSettingsSnapshot( + frequencyMHz: preset.frequencyMHz, + bandwidth: preset.bandwidth, + spreadingFactor: preset.spreadingFactor, + codingRate: preset.codingRate, + txPowerDbm: preset.txPowerDbm, + ); + final frequencyMHz = _clientRepeat + ? _offGridFrequencyForBaseFrequency(baseSnapshot.frequencyMHz) + : baseSnapshot.frequencyMHz; + _frequencyController.text = frequencyMHz.toString(); + _bandwidth = preset.bandwidth; + _spreadingFactor = preset.spreadingFactor; + _codingRate = preset.codingRate; + _txPowerController.text = preset.txPowerDbm.toString(); + _selectedPresetIndex = index; + _lastNonRepeatSnapshot = baseSnapshot; + } + + void _syncPresetSelection() { + final previousPresetIndex = _selectedPresetIndex; + final previousLastNonRepeat = _lastNonRepeatSnapshot; + if (_clientRepeat) { + final baseSnapshot = + previousLastNonRepeat ?? _inferNonRepeatSnapshotForRepeatEnabled(); + if (_bandwidth != baseSnapshot.bandwidth || + _spreadingFactor != baseSnapshot.spreadingFactor || + _codingRate != baseSnapshot.codingRate || + (int.tryParse(_txPowerController.text) ?? 20) != + baseSnapshot.txPowerDbm) { + _lastNonRepeatSnapshot = _RadioSettingsSnapshot( + frequencyMHz: baseSnapshot.frequencyMHz, + bandwidth: _bandwidth, + spreadingFactor: _spreadingFactor, + codingRate: _codingRate, + txPowerDbm: int.tryParse(_txPowerController.text) ?? 20, + ); + } + _selectedPresetIndex = _findMatchingPresetIndexForSnapshot( + _lastNonRepeatSnapshot ?? baseSnapshot, + ); + if (previousPresetIndex != _selectedPresetIndex || + previousLastNonRepeat != _lastNonRepeatSnapshot) { + _logRadioSettingsState( + 'Preset match updated while repeat enabled: ${_presetLabel(previousPresetIndex)} -> ${_presetLabel(_selectedPresetIndex)}', + ); + } + return; + } + _lastNonRepeatSnapshot = _nonRepeatSnapshotForCurrentSelection(); + _selectedPresetIndex = _findMatchingPresetIndexForSnapshot( + _lastNonRepeatSnapshot!, + ); + if (previousPresetIndex != _selectedPresetIndex || + previousLastNonRepeat != _lastNonRepeatSnapshot) { + _logRadioSettingsState( + 'Preset sync updated state from ${_presetLabel(previousPresetIndex)} to ${_presetLabel(_selectedPresetIndex)}', + ); + } + } + + void _handleManualSettingsChanged(String source) { + _logRadioSettingsState('Manual settings edit: $source'); + setState(_syncPresetSelection); + } + + void _handleClientRepeatChanged(bool enabled) { + _logRadioSettingsState( + 'Off-grid repeat toggle requested: $_clientRepeat -> $enabled', + ); + setState(() { + final currentSnapshot = _currentSnapshot(); + if (enabled) { + if (!_clientRepeat) { + _syncPresetSelection(); + } + final baseSnapshot = _lastNonRepeatSnapshot ?? currentSnapshot; + _clientRepeat = true; + _frequencyController.text = _offGridFrequencyForBaseFrequency( + baseSnapshot.frequencyMHz, + ).toStringAsFixed(3); + return; + } + + _clientRepeat = false; + _applySnapshot( + _lastNonRepeatSnapshot ?? + _fallbackNonRepeatSnapshot(currentSnapshot.frequencyMHz), + ); + _syncPresetSelection(); + }); + _logRadioSettingsState('Off-grid repeat toggle applied'); } Future _saveSettings() async { @@ -1164,18 +1421,18 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { final txPower = int.tryParse(_txPowerController.text); if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) { - ScaffoldMessenger.of( + showDismissibleSnackBar( context, - ).showSnackBar(SnackBar(content: Text(l10n.settings_frequencyInvalid))); + content: Text(l10n.settings_frequencyInvalid), + ); return; } final maxTxPower = widget.connector.maxTxPower ?? 22; if (txPower == null || txPower < 0 || txPower > maxTxPower) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'), - ), + showDismissibleSnackBar( + context, + content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'), ); return; } @@ -1195,14 +1452,16 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { if (knownRepeat) { const validRepeatFreqsKHz = {433000, 869000, 918000}; if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_clientRepeatFreqWarning)), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_clientRepeatFreqWarning), ); return; } } try { + _logRadioSettingsState('Saving radio settings'); await widget.connector.sendFrame( buildSetRadioParamsFrame( freqHz, @@ -1214,29 +1473,64 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ); await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower)); await widget.connector.refreshDeviceInfo(); + final rememberedSnapshot = _clientRepeat + ? _lastNonRepeatSnapshot + : _currentSnapshot(); + if (rememberedSnapshot != null) { + widget.connector.rememberNonRepeatRadioState( + rememberedSnapshot.toMeshCoreSnapshot(widget.connector.currentCr), + ); + } if (!mounted) return; - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_radioSettingsUpdated)), + _logRadioSettingsState('Radio settings saved successfully'); + showDismissibleSnackBar( + context, + content: Text(l10n.settings_radioSettingsUpdated), ); } catch (e) { + _appLog.warn('Radio settings save failed: $e', tag: 'RadioSettings'); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.settings_error(e.toString()))), + showDismissibleSnackBar( + context, + content: Text(l10n.settings_error(e.toString())), ); } + Navigator.pop(context); } - int _toUiCodingRate(int deviceCr) { - return deviceCr <= 4 ? deviceCr + 4 : deviceCr; - } - - int _toDeviceCodingRate(int uiCr, int? deviceCr) { - if (deviceCr != null && deviceCr <= 4) { - return uiCr - 4; + String _presetLabel(int? index) { + if (index == null) { + return 'custom'; } - return uiCr; + return '${RadioSettings.presets[index].$1} (#$index)'; + } + + String _formatSnapshot(_RadioSettingsSnapshot? snapshot) { + if (snapshot == null) { + return 'null'; + } + return '${snapshot.frequencyMHz.toStringAsFixed(3)}MHz/' + '${snapshot.bandwidth.label}/' + '${snapshot.spreadingFactor.label}/' + '${snapshot.codingRate.label}/' + '${snapshot.txPowerDbm}dBm'; + } + + void _logRadioSettingsState(String message) { + if (!kDebugMode) return; + _appLog.info( + '$message | ' + 'freq=${_frequencyController.text}MHz ' + 'bw=${_bandwidth.label} ' + 'sf=${_spreadingFactor.label} ' + 'cr=${_codingRate.label} ' + 'tx=${_txPowerController.text}dBm ' + 'repeat=$_clientRepeat ' + 'preset=${_presetLabel(_selectedPresetIndex)} ' + 'lastNonRepeat=${_formatSnapshot(_lastNonRepeatSnapshot)}', + tag: 'RadioSettings', + ); } @override @@ -1250,12 +1544,14 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { crossAxisAlignment: CrossAxisAlignment.start, children: [ DropdownButtonFormField( + key: ValueKey(_selectedPresetIndex), + initialValue: _selectedPresetIndex, decoration: InputDecoration( labelText: l10n.settings_presets, border: const OutlineInputBorder(), ), items: [ - for (var i = 0; i < RadioSettings.presets.length; i++) + for (final i in _visiblePresetIndexes()) DropdownMenuItem( value: i, child: Text(RadioSettings.presets[i].$1), @@ -1263,13 +1559,14 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ], onChanged: (index) { if (index != null) { - _applyPreset(RadioSettings.presets[index].$2); + _applyPreset(index); } }, ), const SizedBox(height: 16), TextField( controller: _frequencyController, + onChanged: (_) => _handleManualSettingsChanged('frequency'), decoration: InputDecoration( labelText: l10n.settings_frequency, border: const OutlineInputBorder(), @@ -1292,7 +1589,13 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ) .toList(), onChanged: (value) { - if (value != null) setState(() => _bandwidth = value); + if (value != null) { + setState(() { + _bandwidth = value; + _syncPresetSelection(); + }); + _logRadioSettingsState('Manual settings edit: bandwidth'); + } }, ), const SizedBox(height: 16), @@ -1308,7 +1611,15 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ) .toList(), onChanged: (value) { - if (value != null) setState(() => _spreadingFactor = value); + if (value != null) { + setState(() { + _spreadingFactor = value; + _syncPresetSelection(); + }); + _logRadioSettingsState( + 'Manual settings edit: spreading factor', + ); + } }, ), const SizedBox(height: 16), @@ -1324,12 +1635,19 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ) .toList(), onChanged: (value) { - if (value != null) setState(() => _codingRate = value); + if (value != null) { + setState(() { + _codingRate = value; + _syncPresetSelection(); + }); + _logRadioSettingsState('Manual settings edit: coding rate'); + } }, ), const SizedBox(height: 16), TextField( controller: _txPowerController, + onChanged: (_) => _handleManualSettingsChanged('tx power'), decoration: InputDecoration( labelText: l10n.settings_txPower, border: const OutlineInputBorder(), @@ -1345,7 +1663,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { title: Text(l10n.settings_clientRepeat), subtitle: Text(l10n.settings_clientRepeatSubtitle), value: _clientRepeat, - onChanged: (value) => setState(() => _clientRepeat = value), + onChanged: _handleClientRepeatChanged, contentPadding: EdgeInsets.zero, ), ], @@ -1362,3 +1680,75 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ); } } + +class _RadioSettingsSnapshot { + final double frequencyMHz; + final LoRaBandwidth bandwidth; + final LoRaSpreadingFactor spreadingFactor; + final LoRaCodingRate codingRate; + final int txPowerDbm; + + const _RadioSettingsSnapshot({ + required this.frequencyMHz, + required this.bandwidth, + required this.spreadingFactor, + required this.codingRate, + required this.txPowerDbm, + }); + + /// Frequency in integer Hz — avoids floating-point comparison issues. + int get frequencyHz => (frequencyMHz * 1000).round(); + + /// Convert from the connector's raw-int snapshot to UI-enum snapshot. + static _RadioSettingsSnapshot? fromMeshCoreSnapshot( + MeshCoreRadioStateSnapshot snapshot, + ) { + final bw = LoRaBandwidth.values + .where((b) => b.hz == snapshot.bwHz) + .firstOrNull; + final sf = LoRaSpreadingFactor.values + .where((s) => s.value == snapshot.sf) + .firstOrNull; + final cr = LoRaCodingRate.values + .where((c) => c.value == _toUiCodingRate(snapshot.cr)) + .firstOrNull; + if (bw == null || sf == null || cr == null) return null; + return _RadioSettingsSnapshot( + frequencyMHz: snapshot.freqHz / 1000.0, + bandwidth: bw, + spreadingFactor: sf, + codingRate: cr, + txPowerDbm: snapshot.txPowerDbm, + ); + } + + /// Convert back to the connector's raw-int snapshot. + MeshCoreRadioStateSnapshot toMeshCoreSnapshot(int? deviceCr) { + return MeshCoreRadioStateSnapshot( + freqHz: frequencyHz, + bwHz: bandwidth.hz, + sf: spreadingFactor.value, + cr: _toDeviceCodingRate(codingRate.value, deviceCr), + txPowerDbm: txPowerDbm, + ); + } + + @override + bool operator ==(Object other) { + return other is _RadioSettingsSnapshot && + frequencyHz == other.frequencyHz && + bandwidth == other.bandwidth && + spreadingFactor == other.spreadingFactor && + codingRate == other.codingRate && + txPowerDbm == other.txPowerDbm; + } + + @override + int get hashCode => Object.hash( + frequencyHz, + bandwidth, + spreadingFactor, + codingRate, + txPowerDbm, + ); +} diff --git a/lib/screens/tcp_screen.dart b/lib/screens/tcp_screen.dart index 11ab80a..3bd1b0b 100644 --- a/lib/screens/tcp_screen.dart +++ b/lib/screens/tcp_screen.dart @@ -8,6 +8,7 @@ import '../l10n/l10n.dart'; import '../services/app_settings_service.dart'; import '../utils/platform_info.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../helpers/snack_bar_builder.dart'; import 'contacts_screen.dart'; import 'usb_screen.dart'; @@ -270,8 +271,10 @@ class _TcpScreenState extends State { void _showError(String message) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: Colors.red), + showDismissibleSnackBar( + context, + content: Text(message), + backgroundColor: Colors.red, ); } diff --git a/lib/screens/telemetry_screen.dart b/lib/screens/telemetry_screen.dart index 66911dc..47593a3 100644 --- a/lib/screens/telemetry_screen.dart +++ b/lib/screens/telemetry_screen.dart @@ -14,6 +14,7 @@ import '../utils/app_logger.dart'; import '../widgets/path_management_dialog.dart'; import '../helpers/cayenne_lpp.dart'; import '../utils/battery_utils.dart'; +import '../helpers/snack_bar_builder.dart'; class TelemetryScreen extends StatefulWidget { final Contact contact; @@ -86,11 +87,10 @@ class _TelemetryScreenState extends State { _isLoading = false; _isLoaded = false; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.telemetry_requestTimeout), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.telemetry_requestTimeout), + backgroundColor: Colors.red, ); _recordTelemetryResult(false); }); @@ -137,11 +137,10 @@ class _TelemetryScreenState extends State { _parsedTelemetry = parsedTelemetry; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.telemetry_receivedData), - backgroundColor: Colors.green, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.telemetry_receivedData), + backgroundColor: Colors.green, ); _statusTimeout?.cancel(); if (!mounted) return; @@ -182,11 +181,10 @@ class _TelemetryScreenState extends State { _isLoaded = false; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.telemetry_errorLoading(e.toString())), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.telemetry_errorLoading(e.toString())), + backgroundColor: Colors.red, ); } } diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 2f2713a..6b8fe9d 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -10,6 +10,7 @@ import '../utils/app_logger.dart'; import '../utils/platform_info.dart'; import '../utils/usb_port_labels.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../helpers/snack_bar_builder.dart'; import 'contacts_screen.dart'; import 'scanner_screen.dart'; import 'tcp_screen.dart'; @@ -383,11 +384,10 @@ class _UsbScreenState extends State { void _showError(Object error) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(_friendlyErrorMessage(error)), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(_friendlyErrorMessage(error)), + backgroundColor: Colors.red, ); } diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index a86c1f6..0c78c59 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -7,8 +7,42 @@ class StorageService { static const String _pathHistoryPrefix = 'path_history_'; static const String _pendingMessagesKey = 'pending_messages'; static const String _repeaterPasswordsKey = 'repeater_passwords'; + static const String _repeaterAutoClockSyncAfterLoginKey = + 'repeater_auto_clock_sync_after_login'; static const String _deliveryObservationsKey = 'delivery_observations'; + Future> _loadRepeaterAutoClockSyncAfterLogin() async { + final prefs = PrefsManager.instance; + final jsonStr = prefs.getString(_repeaterAutoClockSyncAfterLoginKey); + + if (jsonStr == null) return {}; + + try { + final json = jsonDecode(jsonStr) as Map; + return json.map((key, value) => MapEntry(key, value == true)); + } catch (e) { + return {}; + } + } + + Future getRepeaterAutoClockSyncAfterLoginEnabled( + String repeaterPubKeyHex, + ) async { + final settings = await _loadRepeaterAutoClockSyncAfterLogin(); + return settings[repeaterPubKeyHex] ?? false; + } + + Future setRepeaterAutoClockSyncAfterLoginEnabled( + String repeaterPubKeyHex, + bool enabled, + ) async { + final prefs = PrefsManager.instance; + final settings = await _loadRepeaterAutoClockSyncAfterLogin(); + settings[repeaterPubKeyHex] = enabled; + final jsonStr = jsonEncode(settings); + await prefs.setString(_repeaterAutoClockSyncAfterLoginKey, jsonStr); + } + Future savePathHistory( String contactPubKeyHex, ContactPathHistory history, diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart index b0165bd..296cc3a 100644 --- a/lib/utils/gpx_export.dart +++ b/lib/utils/gpx_export.dart @@ -14,12 +14,13 @@ class ContactExport { final double lon; final String desc; final double? ele; - + final String url; ContactExport({ required this.name, required this.lat, required this.lon, required this.desc, + required this.url, this.ele, }); } @@ -40,6 +41,7 @@ class GpxExport { String name, double lat, double lon, + String url, String desc, [ double? ele, ]) { @@ -50,55 +52,66 @@ class GpxExport { lon: lon, desc: desc.trim(), ele: ele, + url: url, ), ); } void addRepeaters() { - final contacts = _connector.contacts - .where((c) => c.type == advTypeRepeater || c.type == advTypeRoom) - .toList(); + final contacts = _connector.allContacts.where( + (c) => c.type == advTypeRepeater || c.type == advTypeRoom, + ); for (var contact in contacts) { if (contact.latitude == null || contact.longitude == null) { continue; } + final url = contact.rawPacket != null + ? "meshcore://${pubKeyToHex(contact.rawPacket!)}" + : ""; _addContact( contact.name, contact.latitude!, contact.longitude!, "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", + url, ); } } void addContacts() { - final contacts = _connector.contacts - .where((c) => c.type == advTypeChat) - .toList(); + final contacts = _connector.allContacts.where((c) => c.type == advTypeChat); for (var contact in contacts) { if (contact.latitude == null || contact.longitude == null) { continue; } + final url = contact.rawPacket != null + ? "meshcore://${pubKeyToHex(contact.rawPacket!)}" + : ""; _addContact( contact.name, contact.latitude!, contact.longitude!, "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", + url, ); } } void addAll() { - final contacts = _connector.contacts; - for (var contact in contacts.toList()) { + final contacts = _connector.allContacts; + for (var contact in contacts) { if (contact.latitude == null || contact.longitude == null) { continue; } + final url = contact.rawPacket != null + ? "meshcore://${pubKeyToHex(contact.rawPacket!)}" + : ""; _addContact( contact.name, contact.latitude ?? 0.0, contact.longitude ?? 0.0, "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", + url, ); } } @@ -138,6 +151,9 @@ class GpxExport { ele: c.ele, name: c.name, desc: c.desc, + extensions: { + "meshcore": {"url": c.url}, + }, ), ) .toList(); diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index 4e91a69..094805a 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -11,6 +11,7 @@ import '../l10n/l10n.dart'; import '../models/contact.dart'; import '../helpers/path_helper.dart'; import '../services/path_history_service.dart'; +import '../helpers/snack_bar_builder.dart'; import 'path_selection_dialog.dart'; class PathManagementDialog { @@ -65,11 +66,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { void _showFullPathDialog(BuildContext context, List pathBytes) { final l10n = context.l10n; if (pathBytes.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.chat_pathDetailsNotAvailable), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(l10n.chat_pathDetailsNotAvailable), + duration: const Duration(seconds: 2), ); return; } @@ -159,11 +159,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { ); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.chat_hopsCount(result.length)), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(l10n.chat_hopsCount(result.length)), + duration: const Duration(seconds: 2), ); } } @@ -337,13 +336,12 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { _showFullPathDialog(context, path.pathBytes), onTap: () async { if (path.pathBytes.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - l10n.chat_pathDetailsNotAvailable, - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + l10n.chat_pathDetailsNotAvailable, ), + duration: const Duration(seconds: 2), ); return; } @@ -361,13 +359,12 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { if (!context.mounted) return; Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - l10n.path_usingHopsPath(path.hopCount), - ), - duration: const Duration(seconds: 2), + showDismissibleSnackBar( + context, + content: Text( + l10n.path_usingHopsPath(path.hopCount), ), + duration: const Duration(seconds: 2), ); }, ), @@ -459,11 +456,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { onTap: () async { await connector.clearContactPath(currentContact); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.chat_pathCleared), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(l10n.chat_pathCleared), + duration: const Duration(seconds: 2), ); Navigator.pop(context); }, @@ -489,11 +485,10 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { pathLen: -1, ); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.chat_floodModeEnabled), - duration: const Duration(seconds: 2), - ), + showDismissibleSnackBar( + context, + content: Text(l10n.chat_floodModeEnabled), + duration: const Duration(seconds: 2), ); Navigator.pop(context); }, diff --git a/lib/widgets/path_selection_dialog.dart b/lib/widgets/path_selection_dialog.dart index b1733fc..7a890ec 100644 --- a/lib/widgets/path_selection_dialog.dart +++ b/lib/widgets/path_selection_dialog.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; +import '../helpers/snack_bar_builder.dart'; class PathSelectionDialog extends StatefulWidget { final List availableContacts; @@ -138,26 +139,22 @@ class _PathSelectionDialogState extends State { // Show error for invalid prefixes if (invalidPrefixes.isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - l10n.path_invalidHexPrefixes(invalidPrefixes.join(", ")), - ), - duration: const Duration(seconds: 3), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(l10n.path_invalidHexPrefixes(invalidPrefixes.join(", "))), + duration: const Duration(seconds: 3), + backgroundColor: Colors.red, ); return; } // Check max path length (64 hops) if (pathBytesList.length > 64) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(l10n.path_tooLong), - duration: const Duration(seconds: 3), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(l10n.path_tooLong), + duration: const Duration(seconds: 3), + backgroundColor: Colors.red, ); return; } diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index ce6c2b7..d38bd03 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -14,7 +14,7 @@ import 'path_management_dialog.dart'; class RepeaterLoginDialog extends StatefulWidget { final Contact repeater; - final Function(String password) onLogin; + final Function(String password, bool isAdmin) onLogin; const RepeaterLoginDialog({ super.key, @@ -113,12 +113,13 @@ class _RepeaterLoginDialogState extends State { messageBytes: responseBytes, ); final timeoutSeconds = (timeoutMs / 1000).ceil(); - final timeout = Duration(milliseconds: timeoutMs); + final timeout = Duration(milliseconds: timeoutMs + 2000); final selectionLabel = selection.useFlood ? 'flood' : '${selection.hopCount} hops'; appLogger.info('Login routing: $selectionLabel', tag: 'RepeaterLogin'); bool? loginResult; + bool isAdmin = false; for (int attempt = 0; attempt < _maxAttempts; attempt++) { if (!mounted) return; setState(() { @@ -131,7 +132,7 @@ class _RepeaterLoginDialogState extends State { ); await _connector.sendFrame(loginFrame); - loginResult = await _awaitLoginResponse(timeout); + (loginResult, isAdmin) = await _awaitLoginResponse(timeout); if (loginResult == true) { appLogger.info( 'Login succeeded for ${repeater.name}', @@ -187,9 +188,32 @@ class _RepeaterLoginDialogState extends State { await _storage.removeRepeaterPassword(widget.repeater.publicKeyHex); } + final autoClockSync = await _storage + .getRepeaterAutoClockSyncAfterLoginEnabled( + widget.repeater.publicKeyHex, + ); + if (autoClockSync) { + try { + final timestampSeconds = + DateTime.now().millisecondsSinceEpoch ~/ 1000; + await _connector.sendFrame( + buildSendCliCommandFrame( + repeater.publicKey, + 'clock sync', + timestampSeconds: timestampSeconds, + ), + ); + } catch (e) { + appLogger.warn( + 'Auto clock sync failed for ${repeater.name}: $e', + tag: 'RepeaterLogin', + ); + } + } + if (mounted) { Navigator.pop(context, password); - Future.microtask(() => widget.onLogin(password)); + Future.microtask(() => widget.onLogin(password, isAdmin)); } } catch (e) { final repeater = _resolveRepeater(_connector); @@ -206,17 +230,21 @@ class _RepeaterLoginDialogState extends State { } } - Future _awaitLoginResponse(Duration timeout) async { + // _awaitLoginResponse returns a record of bool, for success and if the client is an admin + Future<(bool?, bool)> _awaitLoginResponse(Duration timeout) async { final completer = Completer(); Timer? timer; StreamSubscription? subscription; final targetPrefix = widget.repeater.publicKey.sublist(0, 6); - + bool isAdmin = false; subscription = _connector.receivedFrames.listen((frame) { if (frame.isEmpty) return; final code = frame[0]; if (code != pushCodeLoginSuccess && code != pushCodeLoginFail) return; if (frame.length < 8) return; + // NOTE: a bug in the repeater firmware only ever sends 1 or 0 back, not the + // expected client permissions + isAdmin = (frame[1] == 1); final prefix = frame.sublist(2, 8); if (!listEquals(prefix, targetPrefix)) return; @@ -235,7 +263,7 @@ class _RepeaterLoginDialogState extends State { final result = await completer.future; timer.cancel(); await subscription.cancel(); - return result; + return (result, isAdmin); } @override diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 91d2c8c..4d7f29e 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -10,11 +10,12 @@ import '../services/storage_service.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../utils/app_logger.dart'; +import '../helpers/snack_bar_builder.dart'; import 'path_management_dialog.dart'; class RoomLoginDialog extends StatefulWidget { final Contact room; - final Function(String password) onLogin; + final Function(String password, bool isAdmin) onLogin; const RoomLoginDialog({super.key, required this.room, required this.onLogin}); @@ -108,12 +109,13 @@ class _RoomLoginDialogState extends State { messageBytes: responseBytes, ); final timeoutSeconds = (timeoutMs / 1000).ceil(); - final timeout = Duration(milliseconds: timeoutMs); + final timeout = Duration(milliseconds: timeoutMs + 2000); final selectionLabel = selection.useFlood ? 'flood' : '${selection.hopCount} hops'; appLogger.info('Login routing: $selectionLabel', tag: 'RoomLogin'); bool? loginResult; + bool isAdmin = false; for (int attempt = 0; attempt < _maxAttempts; attempt++) { if (!mounted) return; setState(() { @@ -126,7 +128,7 @@ class _RoomLoginDialogState extends State { ); await _connector.sendFrame(loginFrame); - loginResult = await _awaitLoginResponse(timeout); + (loginResult, isAdmin) = await _awaitLoginResponse(timeout); if (loginResult == true) { appLogger.info('Login succeeded for ${room.name}', tag: 'RoomLogin'); break; @@ -166,7 +168,7 @@ class _RoomLoginDialogState extends State { if (mounted) { Navigator.pop(context, password); - Future.microtask(() => widget.onLogin(password)); + Future.microtask(() => widget.onLogin(password, isAdmin)); } } catch (e) { final room = _resolveRepeater(_connector); @@ -175,26 +177,29 @@ class _RoomLoginDialogState extends State { setState(() { _isLoggingIn = false; }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.login_failed(e.toString())), - backgroundColor: Colors.red, - ), + showDismissibleSnackBar( + context, + content: Text(context.l10n.login_failed(e.toString())), + backgroundColor: Colors.red, ); } } } - Future _awaitLoginResponse(Duration timeout) async { + Future<(bool?, bool)> _awaitLoginResponse(Duration timeout) async { final completer = Completer(); Timer? timer; StreamSubscription? subscription; final targetPrefix = widget.room.publicKey.sublist(0, 6); + bool isAdmin = false; subscription = _connector.receivedFrames.listen((frame) { if (frame.isEmpty) return; final code = frame[0]; if (code != pushCodeLoginSuccess && code != pushCodeLoginFail) return; + // NOTE: a bug in the repeater firmware only ever sends 1 or 0 back, not the + // expected client permissions + isAdmin = (frame[1] == 1); if (frame.length < 8) return; final prefix = frame.sublist(2, 8); if (!listEquals(prefix, targetPrefix)) return; @@ -214,7 +219,7 @@ class _RoomLoginDialogState extends State { final result = await completer.future; timer.cancel(); await subscription.cancel(); - return result; + return (result, isAdmin); } @override diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index 30956e2..99f2053 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -1,8 +1,64 @@ import 'package:flutter/material.dart'; +import 'package:latlong2/latlong.dart'; + import '../connector/meshcore_connector.dart'; +import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; +import '../models/contact.dart'; import 'signal_ui.dart'; +Contact? _getRepeaterPrefixMatchNearLocation( + List contacts, + int pubkeyFirstByte, { + LatLng? searchPoint, + bool preferFavorites = false, +}) { + final candidates = contacts + .where( + (c) => + c.publicKey.isNotEmpty && + c.publicKey.first == pubkeyFirstByte && + (c.type == advTypeRepeater || c.type == advTypeRoom), + ) + .toList(); + + if (candidates.isEmpty) return null; + + candidates.sort((a, b) { + if (preferFavorites) { + final favA = a.isFavorite ? 1 : 0; + final favB = b.isFavorite ? 1 : 0; + final favCompare = favB.compareTo(favA); + if (favCompare != 0) return favCompare; + } + + final seenCompare = b.lastSeen.compareTo(a.lastSeen); + if (seenCompare != 0) return seenCompare; + + return a.publicKeyHex.compareTo(b.publicKeyHex); + }); + + if (searchPoint == null) { + return candidates.first; + } + + final distance = Distance(); + Contact best = candidates.first; + var bestDistance = double.infinity; + + for (final c in candidates) { + if (c.hasLocation && c.latitude != null && c.longitude != null) { + final d = distance(searchPoint, LatLng(c.latitude!, c.longitude!)); + if (d < bestDistance) { + bestDistance = d; + best = c; + } + } + } + + return best; +} + class SNRUi { final IconData icon; final Color color; @@ -64,6 +120,15 @@ class SNRIndicator extends StatefulWidget { } class _SNRIndicatorState extends State { + bool _isValidSelfLocation(double lat, double lon) { + const double epsilon = 1e-6; + return (lat.abs() > epsilon || lon.abs() > epsilon) && + lat >= -90.0 && + lat <= 90.0 && + lon >= -180.0 && + lon <= 180.0; + } + @override Widget build(BuildContext context) { final directRepeaters = widget.connector.directRepeaters; @@ -158,10 +223,25 @@ class _SNRIndicatorState extends State { widget.connector.currentSf, ); final allContacts = widget.connector.allContacts; - final name = allContacts - .where((c) => c.publicKey.first == repeater.pubkeyFirstByte) - .map((c) => c.name) - .firstOrNull; + + final selfLat = widget.connector.selfLatitude; + final selfLon = widget.connector.selfLongitude; + + LatLng? selfPoint; + if (selfLat != null && + selfLon != null && + _isValidSelfLocation(selfLat, selfLon)) { + selfPoint = LatLng(selfLat, selfLon); + } + + final contact = _getRepeaterPrefixMatchNearLocation( + allContacts, + repeater.pubkeyFirstByte, + searchPoint: selfPoint, + preferFavorites: true, + ); + + final name = contact?.name; return Column( children: [ diff --git a/lib/widgets/unread_badge.dart b/lib/widgets/unread_badge.dart index 37db11a..424cb6f 100644 --- a/lib/widgets/unread_badge.dart +++ b/lib/widgets/unread_badge.dart @@ -7,7 +7,7 @@ class UnreadBadge extends StatelessWidget { @override Widget build(BuildContext context) { - final display = count > 99 ? '99+' : count.toString(); + final display = count > 9999 ? '9999+' : count.toString(); return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 379e36f..93e4682 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/untranslated.json b/untranslated.json index 9e26dfe..2b4bbbc 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,120 @@ -{} \ No newline at end of file +{ + "bg": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "de": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "es": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "fr": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "hu": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "it": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "ja": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "ko": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "nl": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "pl": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "pt": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "ru": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "sk": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "sl": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "sv": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "uk": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ], + + "zh": [ + "chat_sendMessage", + "repeater_guest", + "room_guest", + "repeater_guestTools" + ] +} diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f02857f..533a171 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST flserial flutter_local_notifications_windows + jni ) set(PLUGIN_BUNDLED_LIBRARIES)