Merge pull request #385 from zjs81/dev

merge dev to main
This commit is contained in:
zjs81 2026-04-14 21:04:04 -07:00 committed by GitHub
commit 39cd6d5514
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
79 changed files with 2568 additions and 930 deletions

3
.gitignore vendored
View file

@ -33,6 +33,9 @@ migrate_working_dir/
pubspec.lock
/build/
/coverage/
# fvm project files
.fvm/
.fvmrc
# Symbolication related
app.*.symbols

View file

@ -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<Contact> get allContacts => List.unmodifiable([
..._contacts,
..._discoveredContacts.where((c) => !c.isActive),
..._discoveredContacts.where(
(c) => !c.isActive && c.publicKeyHex != selfPublicKeyHex,
),
]);
List<Contact> get allContactsUnfiltered =>
List.unmodifiable([..._contacts, ..._discoveredContacts]);
List<Contact> 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<String, String>? 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<void> 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) {

View file

@ -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;

View file

@ -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';

View file

@ -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,
);
}
}

View file

@ -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';

View file

@ -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<double>? 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,
),
);
}

View file

@ -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": "Синхронизиране на часовника след влизане"
}

View file

@ -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."
}

View file

@ -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.",

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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": "ログインが成功した場合、自動的に「時刻同期」を送信する。"
}

View file

@ -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": "성공적인 로그인 후, 자동으로 \"시간 동기화\"를 전송합니다."
}

View file

@ -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:

View file

@ -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 => 'Изпраща рекламен пакет';

View file

@ -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

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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

View file

@ -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';

View file

@ -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

View file

@ -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 => '광고 패킷을 발송';

View file

@ -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 => 'BluetoothkoppelingsPIN';
@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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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.';

View file

@ -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

View file

@ -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 => 'BluetoothparningsPIN';
@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

View file

@ -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

View file

@ -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 => '发送广播包';

View file

@ -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": "BluetoothkoppelingsPIN",
"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"
}

View file

@ -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."
}

View file

@ -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"
}

View file

@ -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": "Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации."
}

View file

@ -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í."
}

View file

@ -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"
}

View file

@ -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": "BluetoothparningsPIN",
"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"
}

View file

@ -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": "Синхронізація годин після входу"
}

View file

@ -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": "在成功登录后,自动发送“时钟同步”指令。"
}

View file

@ -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,

View file

@ -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<double>(
title: Text(context.l10n.appSettings_allTime),
leading: Radio<double>(value: 0),
value: 0,
),
ListTile(
RadioListTile<double>(
title: Text(context.l10n.appSettings_lastHour),
leading: Radio<double>(value: 1),
value: 1,
),
ListTile(
RadioListTile<double>(
title: Text(context.l10n.appSettings_last6Hours),
leading: Radio<double>(value: 6),
value: 6,
),
ListTile(
RadioListTile<double>(
title: Text(context.l10n.appSettings_last24Hours),
leading: Radio<double>(value: 24),
value: 24,
),
ListTile(
RadioListTile<double>(
title: Text(context.l10n.appSettings_lastWeek),
leading: Radio<double>(value: 168),
value: 168,
),
],
),
@ -1117,13 +1114,13 @@ class AppSettingsScreen extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
RadioListTile<UnitSystem>(
title: Text(context.l10n.appSettings_unitsMetric),
leading: const Radio<UnitSystem>(value: UnitSystem.metric),
value: UnitSystem.metric,
),
ListTile(
RadioListTile<UnitSystem>(
title: Text(context.l10n.appSettings_unitsImperial),
leading: const Radio<UnitSystem>(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),
);
},
),

View file

@ -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<BleDebugLogScreen> {
.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,

View file

@ -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<ChannelChatScreen> {
Future<void> _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<ChannelChatScreen> {
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<ChannelChatScreen> {
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<ChannelChatScreen> {
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<ChannelChatScreen> {
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<void> _deleteMessage(ChannelMessage message) async {
await context.read<MeshCoreConnector>().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) {

View file

@ -822,7 +822,8 @@ List<_PathHop> _buildPathHops(
) {
if (pathBytes.isEmpty) return const [];
final candidatesByPrefix = <int, List<Contact>>{};
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,

View file

@ -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<ChannelsScreen>
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<ChannelsScreen>
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<ChannelsScreen>
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<ChannelsScreen>
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<ChannelsScreen>
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<ChannelsScreen>
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<ChannelsScreen>
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<ChannelsScreen>
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<ChannelsScreen>
} 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<ChannelsScreen>
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<ChannelsScreen>
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<ChannelsScreen>
_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<ChannelsScreen>
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<ChannelsScreen>
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<ChannelsScreen>
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<ChannelsScreen>
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<ChannelsScreen>
_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)),
);
}
},

View file

@ -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<ChatScreen> {
tooltip: context.l10n.chat_pathManagement,
onPressed: () => _showPathHistory(context),
),
const RadioStatsIconButton(),
Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
return PopupMenuButton<String>(
@ -366,7 +368,6 @@ class _ChatScreenState extends State<ChatScreen> {
);
},
),
const RadioStatsIconButton(),
],
),
body: Consumer<MeshCoreConnector>(
@ -591,6 +592,9 @@ class _ChatScreenState extends State<ChatScreen> {
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<ChatScreen> {
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<ChatScreen> {
}
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<ChatScreen> {
_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<ChatScreen> {
_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<ChatScreen> {
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<ChatScreen> {
void _showFullPathDialog(BuildContext context, List<int> 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<ChatScreen> {
: (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<ChatScreen> {
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<void> _deleteMessage(Message message) async {
await context.read<MeshCoreConnector>().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<MeshCoreConnector>(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) {

View file

@ -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<CommunityQrScannerScreen> {
}
} 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<CommunityQrScannerScreen> {
}
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<CommunityQrScannerScreen> {
}
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

View file

@ -24,6 +24,7 @@ class _CompanionRadioStatsScreenState extends State<CompanionRadioStatsScreen> {
final c = context.read<MeshCoreConnector>();
_connector = c;
c.acquireRadioStatsPolling();
c.setPollingInterval(1);
c.radioStatsNotifier.addListener(_onStatsUpdate);
}
@ -44,6 +45,7 @@ class _CompanionRadioStatsScreenState extends State<CompanionRadioStatsScreen> {
void dispose() {
_connector?.radioStatsNotifier.removeListener(_onStatsUpdate);
_connector?.releaseRadioStatsPolling();
_connector?.setPollingInterval(30);
super.dispose();
}

View file

@ -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<ContactsScreen>
}
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<ContactsScreen>
// 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<ContactsScreen>
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<ContactsScreen>
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<ContactsScreen>
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<ContactsScreen>
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<ContactsScreen>
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<ContactsScreen>
),
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<ContactsScreen>
),
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<ContactsScreen>
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<ContactsScreen>
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<ContactsScreen>
context: context,
builder: (context) => RoomLoginDialog(
room: room,
onLogin: (password) {
onLogin: (password, isAdmin) {
context.read<MeshCoreConnector>().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<ContactsScreen>
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<ContactsScreen>
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<ContactsScreen>
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<MeshCoreConnector>()
@ -1251,11 +1251,8 @@ class _ContactsScreenState extends State<ContactsScreen>
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<ContactsScreen>
] 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<MeshCoreConnector>()
@ -1288,7 +1283,9 @@ class _ContactsScreenState extends State<ContactsScreen>
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,

View file

@ -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<DiscoveryScreen> {
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<DiscoveryScreen> {
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<DiscoveryScreen> {
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':

View file

@ -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<MapCacheScreen> {
Future<void> _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<MapCacheScreen> {
result.failed,
)
: context.l10n.mapCache_cachedTiles(result.downloaded);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
showDismissibleSnackBar(context, content: Text(message));
}
Future<void> _clearCache() async {
@ -210,8 +211,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
final cacheService = context.read<MapTileCacheService>();
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),
);
}

View file

@ -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<MapScreen> {
bool _hasInitializedMap = false;
bool _removedMarkersLoaded = false;
final List<int> _pathTrace = [];
final List<Contact> _pathTraceContacts = [];
final List<LatLng> _points = [];
final List<Polyline> _polylines = [];
bool _legendExpanded = false;
@ -488,7 +490,7 @@ class _MapScreenState extends State<MapScreen> {
),
),
),
if (!_isBuildingPathTrace)
if (!settings.mapShowOverlaps)
..._buildGuessedMarker(
guessedLocations,
showLabels: _showNodeLabels,
@ -788,17 +790,26 @@ class _MapScreenState extends State<MapScreen> {
final markers = <Marker>[];
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<MapScreen> {
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<MapScreen> {
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<MapScreen> {
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<MeshCoreConnector>().markContactRead(room.publicKeyHex);
Navigator.push(
@ -1643,7 +1664,10 @@ class _MapScreenState extends State<MapScreen> {
);
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<MapScreen> {
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<MapScreen> {
}
}
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<MapScreen> {
setState(() {
_isBuildingPathTrace = true;
_pathTrace.clear();
_pathTraceContacts.clear();
_points.clear();
_polylines.clear();
_points.add(position);
@ -2142,6 +2174,7 @@ class _MapScreenState extends State<MapScreen> {
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<MapScreen> {
title: l10n.contacts_pathTrace,
path: Uint8List.fromList(_pathTrace),
pathHashByteWidth: hashW,
pathContacts: _pathTraceContacts,
),
),
);
@ -2246,8 +2280,9 @@ class _MapScreenState extends State<MapScreen> {
_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,

View file

@ -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<NeighborsScreen> {
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<NeighborsScreen> {
_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<NeighborsScreen> {
_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<NeighborsScreen> {
_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,
);
}
}

View file

@ -56,6 +56,7 @@ class PathTraceMapScreen extends StatefulWidget {
final bool reversePathAround;
final Contact? targetContact;
final int pathHashByteWidth;
final List<Contact>? 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<PathTraceMapScreen> {
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<Uint8List>? _frameSubscription;
Timer? _timeoutTimer;
@ -266,17 +270,43 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
.toList();
Map<int, Contact> 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.

View file

@ -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<RepeaterCliScreen> {
// Common commands for quick access
late final List<Map<String, String>> _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<RepeaterCliScreen> {
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<RepeaterCliScreen> {
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;
}

View file

@ -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<String>(
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<String>(
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,
),
),
),
);
},
),
);
},
),
],
),
),

View file

@ -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<RepeaterSettingsScreen> {
final StorageService _storage = StorageService();
bool _isLoading = false;
bool _hasChanges = false;
bool _refreshingBasic = false;
@ -59,6 +63,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
_lonController.text = widget.repeater.longitude?.toString() ?? '';
}
});
final autoClockSync = await _storage
.getRepeaterAutoClockSyncAfterLoginEnabled(
widget.repeater.publicKeyHex,
);
if (!mounted) return;
setState(() {
_autoClockSyncAfterLogin = autoClockSync;
});
}
Future<void> _saveSettings() async {
@ -653,11 +665,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
});
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<RepeaterSettingsScreen> {
});
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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<RepeaterSettingsScreen> {
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,
);
}
}

View file

@ -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<RepeaterStatusScreen> {
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<RepeaterStatusScreen> {
_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);

View file

@ -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<ScannerScreen> {
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,
);
}
}

View file

@ -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<SettingsScreen> {
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<SettingsScreen> {
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<SettingsScreen> {
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<SettingsScreen> {
: 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<SettingsScreen> {
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<SettingsScreen> {
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<SettingsScreen> {
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<AppDebugLogService>(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<int> _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<void> _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<int>(
key: ValueKey<int?>(_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,
);
}

View file

@ -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<TcpScreen> {
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,
);
}

View file

@ -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<TelemetryScreen> {
_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<TelemetryScreen> {
_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<TelemetryScreen> {
_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,
);
}
}

View file

@ -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<UsbScreen> {
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,
);
}

View file

@ -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<Map<String, bool>> _loadRepeaterAutoClockSyncAfterLogin() async {
final prefs = PrefsManager.instance;
final jsonStr = prefs.getString(_repeaterAutoClockSyncAfterLoginKey);
if (jsonStr == null) return {};
try {
final json = jsonDecode(jsonStr) as Map<String, dynamic>;
return json.map((key, value) => MapEntry(key, value == true));
} catch (e) {
return {};
}
}
Future<bool> getRepeaterAutoClockSyncAfterLoginEnabled(
String repeaterPubKeyHex,
) async {
final settings = await _loadRepeaterAutoClockSyncAfterLogin();
return settings[repeaterPubKeyHex] ?? false;
}
Future<void> 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<void> savePathHistory(
String contactPubKeyHex,
ContactPathHistory history,

View file

@ -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();

View file

@ -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<int> 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);
},

View file

@ -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<Contact> availableContacts;
@ -138,26 +139,22 @@ class _PathSelectionDialogState extends State<PathSelectionDialog> {
// 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;
}

View file

@ -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<RepeaterLoginDialog> {
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<RepeaterLoginDialog> {
);
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<RepeaterLoginDialog> {
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<RepeaterLoginDialog> {
}
}
Future<bool?> _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<bool?>();
Timer? timer;
StreamSubscription<Uint8List>? 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<RepeaterLoginDialog> {
final result = await completer.future;
timer.cancel();
await subscription.cancel();
return result;
return (result, isAdmin);
}
@override

View file

@ -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<RoomLoginDialog> {
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<RoomLoginDialog> {
);
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<RoomLoginDialog> {
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<RoomLoginDialog> {
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<bool?> _awaitLoginResponse(Duration timeout) async {
Future<(bool?, bool)> _awaitLoginResponse(Duration timeout) async {
final completer = Completer<bool?>();
Timer? timer;
StreamSubscription<Uint8List>? 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<RoomLoginDialog> {
final result = await completer.future;
timer.cancel();
await subscription.cancel();
return result;
return (result, isAdmin);
}
@override

View file

@ -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<Contact> 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<SNRIndicator> {
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<SNRIndicator> {
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: [

View file

@ -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(

View file

@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flserial
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)

View file

@ -1 +1,120 @@
{}
{
"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"
]
}

View file

@ -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)