Merge branch 'dev' into msg-chars

This commit is contained in:
Ded 2026-04-15 08:02:56 -07:00 committed by GitHub
commit 5d636bb904
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 981 additions and 730 deletions

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

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

@ -2069,5 +2069,9 @@
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Автоматично изпращайте съобщение \"синхронизиране на часовника\" след успешно влизане.",
"repeater_clockSyncAfterLogin": "Синхронизиране на часовника след влизане"
"repeater_clockSyncAfterLogin": "Синхронизиране на часовника след влизане",
"chat_sendMessage": "Изпратете съобщение",
"room_guest": "Информация за сървъра на стаята",
"repeater_guest": "Информация за ретранслаторите",
"repeater_guestTools": "Инструменти за гости"
}

View file

@ -2097,5 +2097,9 @@
"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."
"repeater_clockSyncAfterLoginSubtitle": "Automatisch \"Uhrzeit-Synchronisierung\" nach erfolgreicher Anmeldung senden.",
"repeater_guest": "Informationen zu Repeatern",
"repeater_guestTools": "Gastwerkzeuge",
"chat_sendMessage": "Nachricht senden",
"room_guest": "Informationen zum Room Server"
}

View file

@ -1038,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)",
@ -1105,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",

View file

@ -2097,5 +2097,9 @@
"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"
"repeater_clockSyncAfterLogin": "Sincronización del reloj después de iniciar sesión",
"repeater_guest": "Información sobre repetidores",
"chat_sendMessage": "Enviar mensaje",
"repeater_guestTools": "Herramientas para invitados",
"room_guest": "Información del servidor"
}

View file

@ -2069,5 +2069,9 @@
"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"
"repeater_clockSyncAfterLogin": "Synchronisation de l'horloge après la connexion",
"repeater_guestTools": "Outils pour les invités",
"chat_sendMessage": "Envoyer un message",
"room_guest": "Informations sur le serveur",
"repeater_guest": "Informations sur les répéteurs"
}

View file

@ -2107,5 +2107,9 @@
"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"
"repeater_clockSyncAfterLogin": "Óra szinkronizálás bejelentkezés után",
"repeater_guestTools": "Vendégek számára elérhető eszközök",
"room_guest": "Szoba szerver információk",
"chat_sendMessage": "Üzenet küldése",
"repeater_guest": "Adatok a repeaterről"
}

View file

@ -2069,5 +2069,9 @@
"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"
"repeater_clockSyncAfterLogin": "Sincronizzazione dell'orologio dopo il login",
"repeater_guest": "Informazioni sul ripetitore",
"repeater_guestTools": "Strumenti per gli ospiti",
"chat_sendMessage": "Invia messaggio",
"room_guest": "Informazioni sul server"
}

View file

@ -2107,5 +2107,9 @@
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "ログイン後、時計の時刻を同期する",
"repeater_clockSyncAfterLoginSubtitle": "ログインが成功した場合、自動的に「時刻同期」を送信する。"
"repeater_clockSyncAfterLoginSubtitle": "ログインが成功した場合、自動的に「時刻同期」を送信する。",
"room_guest": "ルームサーバーに関する情報",
"chat_sendMessage": "メッセージを送信する",
"repeater_guest": "繰り返し送信に関する情報",
"repeater_guestTools": "ゲスト向けツール"
}

View file

@ -2107,5 +2107,9 @@
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "로그인 후 시계 동기화",
"repeater_clockSyncAfterLoginSubtitle": "성공적인 로그인 후, 자동으로 \"시간 동기화\"를 전송합니다."
"repeater_clockSyncAfterLoginSubtitle": "성공적인 로그인 후, 자동으로 \"시간 동기화\"를 전송합니다.",
"repeater_guestTools": "손님용 도구",
"chat_sendMessage": "메시지를 보내기",
"repeater_guest": "반복 장비 정보",
"room_guest": "서버 정보"
}

View file

@ -3438,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.
@ -3609,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:

View file

@ -1240,7 +1240,7 @@ class AppLocalizationsBg extends AppLocalizations {
String get chat_noMessages => 'Няма съобщения.';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => 'Изпратете съобщение';
@override
String chat_sendMessageTo(String contactName) {
@ -2019,9 +2019,18 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get room_management => 'Управление на сървъра за стая';
@override
String get repeater_guest => 'Информация за ретранслаторите';
@override
String get room_guest => 'Информация за сървъра на стаята';
@override
String get repeater_managementTools => 'Инструменти за управление';
@override
String get repeater_guestTools => 'Инструменти за гости';
@override
String get repeater_status => 'Статус';

View file

@ -1239,7 +1239,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get chat_noMessages => 'Noch keine Nachrichten.';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => 'Nachricht senden';
@override
String chat_sendMessageTo(String contactName) {
@ -2017,9 +2017,18 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get room_management => 'Raum-Server-Verwaltung';
@override
String get repeater_guest => 'Informationen zu Repeatern';
@override
String get room_guest => 'Informationen zum Room Server';
@override
String get repeater_managementTools => 'Verwaltungs-Tools';
@override
String get repeater_guestTools => 'Gastwerkzeuge';
@override
String get repeater_status => 'Status';

View file

@ -1871,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';
@ -1979,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';

View file

@ -1239,7 +1239,7 @@ class AppLocalizationsEs extends AppLocalizations {
String get chat_noMessages => 'Aún no hay mensajes';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => 'Enviar mensaje';
@override
String chat_sendMessageTo(String contactName) {
@ -2015,9 +2015,18 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get room_management => 'Administración del Servidor de Habitación';
@override
String get repeater_guest => 'Información sobre repetidores';
@override
String get room_guest => 'Información del servidor';
@override
String get repeater_managementTools => 'Herramientas de Gestión';
@override
String get repeater_guestTools => 'Herramientas para invitados';
@override
String get repeater_status => 'Estado';

View file

@ -1244,7 +1244,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get chat_noMessages => 'Aucun message pour le moment.';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => 'Envoyer un message';
@override
String chat_sendMessageTo(String contactName) {
@ -2026,9 +2026,18 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get room_management => 'Administrattion Room Server';
@override
String get repeater_guest => 'Informations sur les répéteurs';
@override
String get room_guest => 'Informations sur le serveur';
@override
String get repeater_managementTools => 'Outils de Gestion';
@override
String get repeater_guestTools => 'Outils pour les invités';
@override
String get repeater_status => 'État';

View file

@ -1247,7 +1247,7 @@ class AppLocalizationsHu extends AppLocalizations {
String get chat_noMessages => 'Még nincs üzenet.';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => 'Üzenet küldése';
@override
String chat_sendMessageTo(String contactName) {
@ -2030,9 +2030,18 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get room_management => 'Szoba-szerver kezelés';
@override
String get repeater_guest => 'Adatok a repeaterről';
@override
String get room_guest => 'Szoba szerver információk';
@override
String get repeater_managementTools => 'Menedzsmentes eszközök';
@override
String get repeater_guestTools => 'Vendégek számára elérhető eszközök';
@override
String get repeater_status => 'Állapot';

View file

@ -1240,7 +1240,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get chat_noMessages => 'Nessun messaggio ancora';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => 'Invia messaggio';
@override
String chat_sendMessageTo(String contactName) {
@ -2016,9 +2016,18 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get room_management => 'Gestione del Server di Camera';
@override
String get repeater_guest => 'Informazioni sul ripetitore';
@override
String get room_guest => 'Informazioni sul server';
@override
String get repeater_managementTools => 'Strumenti di Gestione';
@override
String get repeater_guestTools => 'Strumenti per gli ospiti';
@override
String get repeater_status => 'Stato';

View file

@ -1180,7 +1180,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get chat_noMessages => 'まだメッセージは届いていません';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => 'メッセージを送信する';
@override
String chat_sendMessageTo(String contactName) {
@ -1932,9 +1932,18 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get room_management => 'ルームサーバーの管理';
@override
String get repeater_guest => '繰り返し送信に関する情報';
@override
String get room_guest => 'ルームサーバーに関する情報';
@override
String get repeater_managementTools => '管理ツール';
@override
String get repeater_guestTools => 'ゲスト向けツール';
@override
String get repeater_status => 'ステータス';

View file

@ -1175,7 +1175,7 @@ class AppLocalizationsKo extends AppLocalizations {
String get chat_noMessages => '아직 메시지가 없습니다.';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => '메시지를 보내기';
@override
String chat_sendMessageTo(String contactName) {
@ -1929,9 +1929,18 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get room_management => '방 서버 관리';
@override
String get repeater_guest => '반복 장비 정보';
@override
String get room_guest => '서버 정보';
@override
String get repeater_managementTools => '관리 도구';
@override
String get repeater_guestTools => '손님용 도구';
@override
String get repeater_status => '상태';

View file

@ -1228,7 +1228,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get chat_noMessages => 'Nog geen berichten.';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => 'Verzend bericht';
@override
String chat_sendMessageTo(String contactName) {
@ -2003,9 +2003,18 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get room_management => 'Beheer Server Kamer';
@override
String get repeater_guest => 'Informatie over herhalingsapparatuur';
@override
String get room_guest => 'Informatie over de server';
@override
String get repeater_managementTools => 'Beheerfuncties';
@override
String get repeater_guestTools => 'Gastenfuncties';
@override
String get repeater_status => 'Status';

View file

@ -1248,7 +1248,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get chat_noMessages => 'Brak jeszcze wiadomości';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => 'Wyślij wiadomość';
@override
String chat_sendMessageTo(String contactName) {
@ -2031,9 +2031,18 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get room_management => 'Zarządzanie Serwerem Pokoju';
@override
String get repeater_guest => 'Informacje dotyczące urządzenia powtarzającego';
@override
String get room_guest => 'Informacje o serwerze';
@override
String get repeater_managementTools => 'Narzędzia Zarządzania';
@override
String get repeater_guestTools => 'Narzędzia dla gości';
@override
String get repeater_status => 'Status';

View file

@ -1239,7 +1239,7 @@ class AppLocalizationsPt extends AppLocalizations {
String get chat_noMessages => 'Ainda não existem mensagens.';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => 'Enviar mensagem';
@override
String chat_sendMessageTo(String contactName) {
@ -2015,9 +2015,18 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get room_management => 'Gerenciamento de Servidor de Sala';
@override
String get repeater_guest => 'Informações sobre repetidores';
@override
String get room_guest => 'Informações do Servidor';
@override
String get repeater_managementTools => 'Ferramentas de Gerenciamento';
@override
String get repeater_guestTools => 'Ferramentas para hóspedes';
@override
String get repeater_status => 'Status';

View file

@ -1239,7 +1239,7 @@ class AppLocalizationsRu extends AppLocalizations {
String get chat_noMessages => 'Сообщений пока нет';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => 'Отправить сообщение';
@override
String chat_sendMessageTo(String contactName) {
@ -2019,9 +2019,18 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get room_management => 'Управление сервером комнат';
@override
String get repeater_guest => 'Информация о ретрансляторе';
@override
String get room_guest => 'Информация о сервере';
@override
String get repeater_managementTools => 'Инструменты управления';
@override
String get repeater_guestTools => 'Инструменты для гостей';
@override
String get repeater_status => 'Статус';

View file

@ -1227,7 +1227,7 @@ class AppLocalizationsSk extends AppLocalizations {
String get chat_noMessages => 'Zatiaľ žiadne správy.';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => 'Odoslať správu';
@override
String chat_sendMessageTo(String contactName) {
@ -2004,9 +2004,18 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get room_management => 'Správa servera miestnosti';
@override
String get repeater_guest => 'Informácie o opakovači';
@override
String get room_guest => 'Informácie o serveri';
@override
String get repeater_managementTools => 'Nástroje na správu';
@override
String get repeater_guestTools => 'Nástroje pre hostí';
@override
String get repeater_status => 'Status';

View file

@ -1225,7 +1225,7 @@ class AppLocalizationsSl extends AppLocalizations {
String get chat_noMessages => 'Še ni sporočil.';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => 'Pošlji sporočilo';
@override
String chat_sendMessageTo(String contactName) {
@ -2001,9 +2001,18 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get room_management => 'Upravljanje stremlišča';
@override
String get repeater_guest => 'Informacije o ponovljalniku';
@override
String get room_guest => 'Informacije o strežniku';
@override
String get repeater_managementTools => 'Upravne orodje';
@override
String get repeater_guestTools => 'Naložila za goste';
@override
String get repeater_status => 'Status';

View file

@ -1218,7 +1218,7 @@ class AppLocalizationsSv extends AppLocalizations {
String get chat_noMessages => 'Inga meddelanden ännu';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => 'Skicka meddelande';
@override
String chat_sendMessageTo(String contactName) {
@ -1990,9 +1990,18 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get room_management => 'Rumserverhantering';
@override
String get repeater_guest => 'Information om repetorer';
@override
String get room_guest => 'Information om servern';
@override
String get repeater_managementTools => 'Administrationsverktyg';
@override
String get repeater_guestTools => 'Gästverktyg';
@override
String get repeater_status => 'Status';

View file

@ -1231,7 +1231,7 @@ class AppLocalizationsUk extends AppLocalizations {
String get chat_noMessages => 'Поки немає повідомлень.';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => 'Надіслати повідомлення';
@override
String chat_sendMessageTo(String contactName) {
@ -2014,9 +2014,18 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get room_management => 'Адміністрування сервера кімнати';
@override
String get repeater_guest => 'Інформація про ретранслятор';
@override
String get room_guest => 'Інформація про сервер кімнати';
@override
String get repeater_managementTools => 'Інструменти керування';
@override
String get repeater_guestTools => 'Інструменти для гостей';
@override
String get repeater_status => 'Статус';

View file

@ -1162,7 +1162,7 @@ class AppLocalizationsZh extends AppLocalizations {
String get chat_noMessages => '暂无消息';
@override
String get chat_sendMessage => 'Send message';
String get chat_sendMessage => '发送消息';
@override
String chat_sendMessageTo(String contactName) {
@ -1890,9 +1890,18 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get room_management => '房间服务器管理';
@override
String get repeater_guest => '重复器信息';
@override
String get room_guest => '服务器信息';
@override
String get repeater_managementTools => '管理工具';
@override
String get repeater_guestTools => '访客工具';
@override
String get repeater_status => '状态';

View file

@ -2069,5 +2069,9 @@
"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"
"repeater_clockSyncAfterLogin": "Na het inloggen, klok synchroniseren",
"repeater_guestTools": "Gastenfuncties",
"room_guest": "Informatie over de server",
"chat_sendMessage": "Verzend bericht",
"repeater_guest": "Informatie over herhalingsapparatuur"
}

View file

@ -2107,5 +2107,9 @@
"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."
"repeater_clockSyncAfterLoginSubtitle": "Automatycznie wysyłaj powiadomienie \"synchronizacja zegara\" po pomyślnym zalogowaniu.",
"chat_sendMessage": "Wyślij wiadomość",
"repeater_guestTools": "Narzędzia dla gości",
"repeater_guest": "Informacje dotyczące urządzenia powtarzającego",
"room_guest": "Informacje o serwerze"
}

View file

@ -2069,5 +2069,9 @@
"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"
"repeater_clockSyncAfterLogin": "Sincronização do relógio após o login",
"room_guest": "Informações do Servidor",
"chat_sendMessage": "Enviar mensagem",
"repeater_guest": "Informações sobre repetidores",
"repeater_guestTools": "Ferramentas para hóspedes"
}

View file

@ -1309,5 +1309,9 @@
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "Синхронизация часов после входа в систему",
"repeater_clockSyncAfterLoginSubtitle": "Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации."
"repeater_clockSyncAfterLoginSubtitle": "Автоматически отправлять сообщение \"синхронизация времени\" после успешной авторизации.",
"chat_sendMessage": "Отправить сообщение",
"repeater_guest": "Информация о ретрансляторе",
"room_guest": "Информация о сервере",
"repeater_guestTools": "Инструменты для гостей"
}

View file

@ -2069,5 +2069,9 @@
"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í."
"repeater_clockSyncAfterLoginSubtitle": "Automaticky posielajte notifikáciu \"synchronizácia času\" po úspešnom prihládení.",
"chat_sendMessage": "Odoslať správu",
"repeater_guest": "Informácie o opakovači",
"room_guest": "Informácie o serveri",
"repeater_guestTools": "Nástroje pre hostí"
}

View file

@ -2069,5 +2069,9 @@
"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"
"repeater_clockSyncAfterLogin": "Sinhronizacija ure po prijavi",
"repeater_guest": "Informacije o ponovljalniku",
"chat_sendMessage": "Pošlji sporočilo",
"room_guest": "Informacije o strežniku",
"repeater_guestTools": "Naložila za goste"
}

View file

@ -2069,5 +2069,9 @@
"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"
"repeater_clockSyncAfterLogin": "Synkronisera klockan efter inloggning",
"repeater_guest": "Information om repetorer",
"chat_sendMessage": "Skicka meddelande",
"repeater_guestTools": "Gästverktyg",
"room_guest": "Information om servern"
}

View file

@ -2069,5 +2069,9 @@
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLoginSubtitle": "Автоматично надсилати повідомлення \"синхронізація годин\" після успішного входу.",
"repeater_clockSyncAfterLogin": "Синхронізація годин після входу"
"repeater_clockSyncAfterLogin": "Синхронізація годин після входу",
"repeater_guestTools": "Інструменти для гостей",
"repeater_guest": "Інформація про ретранслятор",
"room_guest": "Інформація про сервер кімнати",
"chat_sendMessage": "Надіслати повідомлення"
}

View file

@ -2074,5 +2074,9 @@
"description": "Repeater setting subtitle: describes the clock sync after login behavior"
},
"repeater_clockSyncAfterLogin": "登录后,自动同步时钟",
"repeater_clockSyncAfterLoginSubtitle": "在成功登录后,自动发送“时钟同步”指令。"
"repeater_clockSyncAfterLoginSubtitle": "在成功登录后,自动发送“时钟同步”指令。",
"repeater_guestTools": "访客工具",
"repeater_guest": "重复器信息",
"chat_sendMessage": "发送消息",
"room_guest": "服务器信息"
}

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,8 @@ import '../helpers/smaz.dart';
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';
@ -145,11 +147,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;
}
@ -1155,9 +1156,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;
@ -1202,9 +1204,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
widget.channel.index,
messageText,
);
if (utf8.encode(outboundText).length > maxBytes) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))),
if (utf8.encode(messageText).length > maxBytes) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.chat_messageTooLong(maxBytes)),
);
return;
}
@ -1331,17 +1334,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

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

@ -44,6 +44,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 {
@ -643,9 +644,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;
@ -684,9 +686,10 @@ class _ChatScreenState extends State<ChatScreen> {
_resolveContact(connector),
outgoingText,
);
if (utf8.encode(outboundText).length > maxBytes) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))),
if (utf8.encode(outgoingText).length > maxBytes) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.chat_messageTooLong(maxBytes)),
);
return;
}
@ -874,15 +877,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;
}
@ -966,11 +966,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);
},
@ -996,11 +995,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);
},
@ -1034,11 +1032,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;
}
@ -1151,11 +1148,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),
);
}
@ -1504,26 +1500,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

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

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 }
@ -234,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';
@ -1366,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,
),
),
);
},
@ -1385,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(
@ -1659,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(
@ -1681,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;
}
@ -2271,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;
@ -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

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

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

@ -10,6 +10,7 @@ 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;
@ -468,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,
);
}
@ -666,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) {
@ -679,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,
);
}
}
@ -1429,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;
}
@ -1453,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

@ -11,6 +11,7 @@ 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';
@ -513,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),
@ -628,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;
}
@ -639,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),
);
}
@ -660,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;
}
@ -676,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),
@ -691,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) {
@ -758,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;
}
}
@ -1077,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),
@ -1410,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;
}
@ -1441,8 +1452,9 @@ 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;
}
@ -1472,14 +1484,16 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
if (!mounted) return;
_logRadioSettingsState('Radio settings saved successfully');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_radioSettingsUpdated)),
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);

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

@ -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,
@ -119,6 +119,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
: '${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}',
@ -212,7 +213,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
if (mounted) {
Navigator.pop(context, password);
Future.microtask(() => widget.onLogin(password));
Future.microtask(() => widget.onLogin(password, isAdmin));
}
} catch (e) {
final repeater = _resolveRepeater(_connector);
@ -229,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;
@ -258,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});
@ -114,6 +115,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
: '${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,69 +1 @@
{
"bg": [
"chat_sendMessage"
],
"de": [
"chat_sendMessage"
],
"es": [
"chat_sendMessage"
],
"fr": [
"chat_sendMessage"
],
"hu": [
"chat_sendMessage"
],
"it": [
"chat_sendMessage"
],
"ja": [
"chat_sendMessage"
],
"ko": [
"chat_sendMessage"
],
"nl": [
"chat_sendMessage"
],
"pl": [
"chat_sendMessage"
],
"pt": [
"chat_sendMessage"
],
"ru": [
"chat_sendMessage"
],
"sk": [
"chat_sendMessage"
],
"sl": [
"chat_sendMessage"
],
"sv": [
"chat_sendMessage"
],
"uk": [
"chat_sendMessage"
],
"zh": [
"chat_sendMessage"
]
}
{}