Merge main into dev-neighbours

This commit is contained in:
zjs81
2026-01-19 19:09:03 -07:00
34 changed files with 1442 additions and 568 deletions
File diff suppressed because it is too large Load Diff
+65 -30
View File
@@ -20,7 +20,8 @@ class BufferReader {
Uint8List readRemainingBytes() => readBytes(remaining); Uint8List readRemainingBytes() => readBytes(remaining);
String readString() => utf8.decode(readRemainingBytes(), allowMalformed: true); String readString() =>
utf8.decode(readRemainingBytes(), allowMalformed: true);
String readCString(int maxLength) { String readCString(int maxLength) {
final value = <int>[]; final value = <int>[];
@@ -38,13 +39,19 @@ class BufferReader {
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0); int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0); int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
int readUInt16LE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.little); int readUInt16LE() =>
int readUInt16BE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.big); readBytes(2).buffer.asByteData().getUint16(0, Endian.little);
int readUInt32LE() => readBytes(4).buffer.asByteData().getUint32(0, Endian.little); int readUInt16BE() =>
int readUInt32BE() => readBytes(4).buffer.asByteData().getUint32(0, Endian.big); readBytes(2).buffer.asByteData().getUint16(0, Endian.big);
int readInt16LE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.little); int readUInt32LE() =>
readBytes(4).buffer.asByteData().getUint32(0, Endian.little);
int readUInt32BE() =>
readBytes(4).buffer.asByteData().getUint32(0, Endian.big);
int readInt16LE() =>
readBytes(2).buffer.asByteData().getInt16(0, Endian.little);
int readInt16BE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.big); int readInt16BE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.big);
int readInt32LE() => readBytes(4).buffer.asByteData().getInt32(0, Endian.little); int readInt32LE() =>
readBytes(4).buffer.asByteData().getInt32(0, Endian.little);
int readInt24BE() { int readInt24BE() {
var value = (readByte() << 16) | (readByte() << 8) | readByte(); var value = (readByte() << 16) | (readByte() << 8) | readByte();
@@ -63,21 +70,25 @@ class BufferWriter {
void writeBytes(Uint8List bytes) => _builder.add(bytes); void writeBytes(Uint8List bytes) => _builder.add(bytes);
void writeUInt16LE(int num) { void writeUInt16LE(int num) {
final bytes = Uint8List(2)..buffer.asByteData().setUint16(0, num, Endian.little); final bytes = Uint8List(2)
..buffer.asByteData().setUint16(0, num, Endian.little);
writeBytes(bytes); writeBytes(bytes);
} }
void writeUInt32LE(int num) { void writeUInt32LE(int num) {
final bytes = Uint8List(4)..buffer.asByteData().setUint32(0, num, Endian.little); final bytes = Uint8List(4)
..buffer.asByteData().setUint32(0, num, Endian.little);
writeBytes(bytes); writeBytes(bytes);
} }
void writeInt32LE(int num) { void writeInt32LE(int num) {
final bytes = Uint8List(4)..buffer.asByteData().setInt32(0, num, Endian.little); final bytes = Uint8List(4)
..buffer.asByteData().setInt32(0, num, Endian.little);
writeBytes(bytes); writeBytes(bytes);
} }
void writeString(String string) => writeBytes(Uint8List.fromList(utf8.encode(string))); void writeString(String string) =>
writeBytes(Uint8List.fromList(utf8.encode(string)));
void writeCString(String string, int maxLength) { void writeCString(String string, int maxLength) {
final bytes = Uint8List(maxLength); final bytes = Uint8List(maxLength);
@@ -118,6 +129,8 @@ const int cmdGetChannel = 31;
const int cmdSetChannel = 32; const int cmdSetChannel = 32;
const int cmdGetRadioSettings = 57; const int cmdGetRadioSettings = 57;
const int cmdGetTelemetryReq = 39; const int cmdGetTelemetryReq = 39;
const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50; const int cmdSendBinaryReq = 50;
// Text message types // Text message types
@@ -152,6 +165,7 @@ const int respCodeContactMsgRecvV3 = 16;
const int respCodeChannelMsgRecvV3 = 17; const int respCodeChannelMsgRecvV3 = 17;
const int respCodeChannelInfo = 18; const int respCodeChannelInfo = 18;
const int respCodeRadioSettings = 25; const int respCodeRadioSettings = 25;
const int respCodeCustomVars = 21;
// Push codes (async from device) // Push codes (async from device)
const int pushCodeAdvert = 0x80; const int pushCodeAdvert = 0x80;
@@ -166,7 +180,6 @@ const int pushCodeNewAdvert = 0x8A;
const int pushCodeTelemetryResponse = 0x8B; const int pushCodeTelemetryResponse = 0x8B;
const int pushCodeBinaryResponse = 0x8C; const int pushCodeBinaryResponse = 0x8C;
// Contact/advertisement types // Contact/advertisement types
const int advTypeChat = 1; const int advTypeChat = 1;
const int advTypeRepeater = 2; const int advTypeRepeater = 2;
@@ -233,10 +246,7 @@ class ParsedContactText {
final Uint8List senderPrefix; final Uint8List senderPrefix;
final String text; final String text;
const ParsedContactText({ const ParsedContactText({required this.senderPrefix, required this.text});
required this.senderPrefix,
required this.text,
});
} }
ParsedContactText? parseContactMessageText(Uint8List frame) { ParsedContactText? parseContactMessageText(Uint8List frame) {
@@ -265,10 +275,17 @@ ParsedContactText? parseContactMessageText(Uint8List frame) {
return null; return null;
} }
var text = readCString(frame, baseTextOffset, frame.length - baseTextOffset).trim(); var text = readCString(
frame,
baseTextOffset,
frame.length - baseTextOffset,
).trim();
if (text.isEmpty && frame.length > baseTextOffset + 4) { if (text.isEmpty && frame.length > baseTextOffset + 4) {
text = text = readCString(
readCString(frame, baseTextOffset + 4, frame.length - (baseTextOffset + 4)).trim(); frame,
baseTextOffset + 4,
frame.length - (baseTextOffset + 4),
).trim();
} }
if (text.isEmpty) return null; if (text.isEmpty) return null;
@@ -362,7 +379,8 @@ Uint8List buildSendTextMsgFrame(
int attempt = 0, int attempt = 0,
int? timestampSeconds, int? timestampSeconds,
}) { }) {
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000); final timestamp =
timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
final writer = BufferWriter(); final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg); writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypePlain); writer.writeByte(txtTypePlain);
@@ -444,7 +462,9 @@ Uint8List buildSendSelfAdvertFrame({bool flood = false}) {
// Format: [cmd][name...] // Format: [cmd][name...]
Uint8List buildSetAdvertNameFrame(String name) { Uint8List buildSetAdvertNameFrame(String name) {
final nameBytes = utf8.encode(name); final nameBytes = utf8.encode(name);
final nameLen = nameBytes.length < maxNameSize ? nameBytes.length : maxNameSize - 1; final nameLen = nameBytes.length < maxNameSize
? nameBytes.length
: maxNameSize - 1;
final writer = BufferWriter(); final writer = BufferWriter();
writer.writeByte(cmdSetAdvertName); writer.writeByte(cmdSetAdvertName);
writer.writeBytes(Uint8List.fromList(nameBytes.sublist(0, nameLen))); writer.writeBytes(Uint8List.fromList(nameBytes.sublist(0, nameLen)));
@@ -461,6 +481,14 @@ Uint8List buildSetAdvertLatLonFrame(double lat, double lon) {
return writer.toBytes(); return writer.toBytes();
} }
Uint8List buildSetCustomVarFrame(String value) {
final writer = BufferWriter();
writer.writeByte(cmdSetCustomVar);
writer.writeString(value);
writer.writeByte(0);
return writer.toBytes();
}
// Build CMD_REBOOT frame // Build CMD_REBOOT frame
// Format: [cmd]["reboot"] // Format: [cmd]["reboot"]
Uint8List buildRebootFrame() { Uint8List buildRebootFrame() {
@@ -544,7 +572,9 @@ Uint8List buildUpdateContactPathFrame(
// Path data (64 bytes, zero-padded) // Path data (64 bytes, zero-padded)
final pathPadded = Uint8List(maxPathSize); final pathPadded = Uint8List(maxPathSize);
if (customPath.isNotEmpty && pathLen > 0) { if (customPath.isNotEmpty && pathLen > 0) {
final copyLen = customPath.length < maxPathSize ? customPath.length : maxPathSize; final copyLen = customPath.length < maxPathSize
? customPath.length
: maxPathSize;
for (int i = 0; i < copyLen; i++) { for (int i = 0; i < copyLen; i++) {
pathPadded[i] = customPath[i]; pathPadded[i] = customPath[i];
} }
@@ -575,6 +605,11 @@ Uint8List buildGetRadioSettingsFrame() {
return Uint8List.fromList([cmdGetRadioSettings]); return Uint8List.fromList([cmdGetRadioSettings]);
} }
//Build CMD_GET_CUSTOM_VARS frame
Uint8List buildGetCustomVarsFrame() {
return Uint8List.fromList([cmdGetCustomVar]);
}
// Calculate LoRa airtime for a packet // Calculate LoRa airtime for a packet
// Based on Semtech SX127x datasheet formula // Based on Semtech SX127x datasheet formula
// Returns airtime in milliseconds // Returns airtime in milliseconds
@@ -598,9 +633,11 @@ int calculateLoRaAirtime({
final crc = 1; // CRC enabled final crc = 1; // CRC enabled
final de = lowDataRateOptimize ? 1 : 0; final de = lowDataRateOptimize ? 1 : 0;
final numerator = 8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes; final numerator =
8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes;
final denominator = 4 * (spreadingFactor - 2 * de); final denominator = 4 * (spreadingFactor - 2 * de);
var payloadSymbols = 8 + ((numerator / denominator).ceil()) * (codingRate + 4); var payloadSymbols =
8 + ((numerator / denominator).ceil()) * (codingRate + 4);
if (payloadSymbols < 0) { if (payloadSymbols < 0) {
payloadSymbols = 8; payloadSymbols = 8;
@@ -647,7 +684,8 @@ Uint8List buildSendCliCommandFrame(
int attempt = 0, int attempt = 0,
int? timestampSeconds, int? timestampSeconds,
}) { }) {
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000); final timestamp =
timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
final writer = BufferWriter(); final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg); writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypeCliData); writer.writeByte(txtTypeCliData);
@@ -661,10 +699,7 @@ Uint8List buildSendCliCommandFrame(
// Build a telemetry request frame // Build a telemetry request frame
// Format: [cmd][pub_key x32][payload] // Format: [cmd][pub_key x32][payload]
Uint8List buildSendBinaryReq( Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
Uint8List repeaterPubKey, {
Uint8List? payload,
}) {
final writer = BufferWriter(); final writer = BufferWriter();
writer.writeByte(cmdSendBinaryReq); writer.writeByte(cmdSendBinaryReq);
writer.writeBytes(repeaterPubKey); writer.writeBytes(repeaterPubKey);
@@ -672,4 +707,4 @@ Uint8List buildSendBinaryReq(
writer.writeBytes(payload); writer.writeBytes(payload);
} }
return writer.toBytes(); return writer.toBytes();
} }
+7 -1
View File
@@ -1378,5 +1378,11 @@
} }
}, },
"neighbors_heardAgo": "Слушано преди {time}.", "neighbors_heardAgo": "Слушано преди {time}.",
"neighbors_unknownContact": "Неизвестна {pubkey}" "neighbors_unknownContact": "Неизвестна {pubkey}",
"settings_locationIntervalSec": "Интервал за GPS (Секунди)",
"settings_locationGPSEnable": "Активиране на GPS",
"settings_locationGPSEnableSubtitle": "Активирайте автоматичното актуализиране на местоположението чрез GPS.",
"settings_locationIntervalInvalid": "Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.",
"room_management": "Управление на сървъра за стая",
"contacts_manageRoom": "Управление на сървър за стая"
} }
+9 -3
View File
@@ -83,9 +83,13 @@
"settings_radioSettingsUpdated": "Radio settings updated", "settings_radioSettingsUpdated": "Radio settings updated",
"settings_location": "Location", "settings_location": "Location",
"settings_locationSubtitle": "GPS coordinates", "settings_locationSubtitle": "GPS coordinates",
"settings_locationUpdated": "Location updated", "settings_locationUpdated": "Location and GPS settings updated",
"settings_locationBothRequired": "Enter both latitude and longitude.", "settings_locationBothRequired": "Enter both latitude and longitude.",
"settings_locationInvalid": "Invalid latitude or longitude.", "settings_locationInvalid": "Invalid latitude or longitude.",
"settings_locationGPSEnable": "GPS Enable",
"settings_locationGPSEnableSubtitle": "Enables GPS to automatically update location.",
"settings_locationIntervalSec": "Interval for GPS (Seconds)",
"settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.",
"settings_latitude": "Latitude", "settings_latitude": "Latitude",
"settings_longitude": "Longitude", "settings_longitude": "Longitude",
"settings_privacyMode": "Privacy Mode", "settings_privacyMode": "Privacy Mode",
@@ -253,7 +257,8 @@
} }
}, },
"contacts_manageRepeater": "Manage Repeater", "contacts_manageRepeater": "Manage Repeater",
"contacts_roomLogin": "Room Login", "contacts_manageRoom": "Manage Room Server",
"contacts_roomLogin": "Room Server Login",
"contacts_openChat": "Open Chat", "contacts_openChat": "Open Chat",
"contacts_editGroup": "Edit Group", "contacts_editGroup": "Edit Group",
"contacts_deleteGroup": "Delete Group", "contacts_deleteGroup": "Delete Group",
@@ -697,7 +702,7 @@
"dialog_disconnectConfirm": "Are you sure you want to disconnect from this device?", "dialog_disconnectConfirm": "Are you sure you want to disconnect from this device?",
"login_repeaterLogin": "Repeater Login", "login_repeaterLogin": "Repeater Login",
"login_roomLogin": "Room Login", "login_roomLogin": "Room Server Login",
"login_password": "Password", "login_password": "Password",
"login_enterPassword": "Enter password", "login_enterPassword": "Enter password",
"login_savePassword": "Save password", "login_savePassword": "Save password",
@@ -760,6 +765,7 @@
"path_setPath": "Set Path", "path_setPath": "Set Path",
"repeater_management": "Repeater Management", "repeater_management": "Repeater Management",
"room_management": "Room Server Management",
"repeater_managementTools": "Management Tools", "repeater_managementTools": "Management Tools",
"repeater_status": "Status", "repeater_status": "Status",
"repeater_statusSubtitle": "View repeater status, stats, and neighbors", "repeater_statusSubtitle": "View repeater status, stats, and neighbors",
+7 -1
View File
@@ -1378,5 +1378,11 @@
} }
}, },
"neighbors_unknownContact": "Clave pública desconocida {pubkey}", "neighbors_unknownContact": "Clave pública desconocida {pubkey}",
"neighbors_heardAgo": "Escuchado: {time} hace atrás" "neighbors_heardAgo": "Escuchado: {time} hace atrás",
"settings_locationGPSEnable": "Habilitar GPS",
"settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.",
"settings_locationIntervalSec": "Intervalo para GPS (Segundos)",
"settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.",
"contacts_manageRoom": "Gestionar Servidor de Habitación",
"room_management": "Administración del Servidor de Habitación"
} }
+7 -1
View File
@@ -1378,5 +1378,11 @@
} }
}, },
"neighbors_unknownContact": "Clé publique inconnue {pubkey}", "neighbors_unknownContact": "Clé publique inconnue {pubkey}",
"neighbors_heardAgo": "Écouté : {time} auparavant" "neighbors_heardAgo": "Écouté : {time} auparavant",
"settings_locationGPSEnable": "Habilita GPS",
"settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.",
"settings_locationIntervalSec": "Intervalo pour GPS (Segundos)",
"settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.",
"contacts_manageRoom": "Gestionar Servidor de Habitación",
"room_management": "Administración del Servidor de Habitación"
} }
+7 -1
View File
@@ -1378,5 +1378,11 @@
} }
}, },
"neighbors_heardAgo": "Sentito: {time} fa", "neighbors_heardAgo": "Sentito: {time} fa",
"neighbors_unknownContact": "Chiave pubblica sconosciuta {pubkey}" "neighbors_unknownContact": "Chiave pubblica sconosciuta {pubkey}",
"settings_locationGPSEnable": "Abilita GPS",
"settings_locationGPSEnableSubtitle": "Abilita l'aggiornamento automatico della posizione tramite GPS.",
"settings_locationIntervalSec": "Intervallo GPS (Secondi)",
"settings_locationIntervalInvalid": "L'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.",
"contacts_manageRoom": "Gestisci Server Camera",
"room_management": "Gestione del Server di Camera"
} }
+39 -3
View File
@@ -465,7 +465,7 @@ abstract class AppLocalizations {
/// No description provided for @settings_locationUpdated. /// No description provided for @settings_locationUpdated.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Location updated'** /// **'Location and GPS settings updated'**
String get settings_locationUpdated; String get settings_locationUpdated;
/// No description provided for @settings_locationBothRequired. /// No description provided for @settings_locationBothRequired.
@@ -480,6 +480,30 @@ abstract class AppLocalizations {
/// **'Invalid latitude or longitude.'** /// **'Invalid latitude or longitude.'**
String get settings_locationInvalid; String get settings_locationInvalid;
/// No description provided for @settings_locationGPSEnable.
///
/// In en, this message translates to:
/// **'GPS Enable'**
String get settings_locationGPSEnable;
/// No description provided for @settings_locationGPSEnableSubtitle.
///
/// In en, this message translates to:
/// **'Enables GPS to automatically update location.'**
String get settings_locationGPSEnableSubtitle;
/// No description provided for @settings_locationIntervalSec.
///
/// In en, this message translates to:
/// **'Interval for GPS (Seconds)'**
String get settings_locationIntervalSec;
/// No description provided for @settings_locationIntervalInvalid.
///
/// In en, this message translates to:
/// **'Interval must be at least 60 seconds, and less than 86400 seconds.'**
String get settings_locationIntervalInvalid;
/// No description provided for @settings_latitude. /// No description provided for @settings_latitude.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1284,10 +1308,16 @@ abstract class AppLocalizations {
/// **'Manage Repeater'** /// **'Manage Repeater'**
String get contacts_manageRepeater; String get contacts_manageRepeater;
/// No description provided for @contacts_manageRoom.
///
/// In en, this message translates to:
/// **'Manage Room Server'**
String get contacts_manageRoom;
/// No description provided for @contacts_roomLogin. /// No description provided for @contacts_roomLogin.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Room Login'** /// **'Room Server Login'**
String get contacts_roomLogin; String get contacts_roomLogin;
/// No description provided for @contacts_openChat. /// No description provided for @contacts_openChat.
@@ -2672,7 +2702,7 @@ abstract class AppLocalizations {
/// No description provided for @login_roomLogin. /// No description provided for @login_roomLogin.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Room Login'** /// **'Room Server Login'**
String get login_roomLogin; String get login_roomLogin;
/// No description provided for @login_password. /// No description provided for @login_password.
@@ -2867,6 +2897,12 @@ abstract class AppLocalizations {
/// **'Repeater Management'** /// **'Repeater Management'**
String get repeater_management; String get repeater_management;
/// No description provided for @room_management.
///
/// In en, this message translates to:
/// **'Room Server Management'**
String get room_management;
/// No description provided for @repeater_managementTools. /// No description provided for @repeater_managementTools.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+20
View File
@@ -201,6 +201,20 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get settings_locationInvalid => 'Невалидна ширина или дължина.'; String get settings_locationInvalid => 'Невалидна ширина или дължина.';
@override
String get settings_locationGPSEnable => 'Активиране на GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Активирайте автоматичното актуализиране на местоположението чрез GPS.';
@override
String get settings_locationIntervalSec => 'Интервал за GPS (Секунди)';
@override
String get settings_locationIntervalInvalid =>
'Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.';
@override @override
String get settings_latitude => 'Широчина'; String get settings_latitude => 'Широчина';
@@ -650,6 +664,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get contacts_manageRepeater => 'Управление на Повтарящ се Елемент'; String get contacts_manageRepeater => 'Управление на Повтарящ се Елемент';
@override
String get contacts_manageRoom => 'Управление на сървър за стая';
@override @override
String get contacts_roomLogin => 'Вход в стаята'; String get contacts_roomLogin => 'Вход в стаята';
@@ -1587,6 +1604,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get repeater_management => 'Управление на повторители'; String get repeater_management => 'Управление на повторители';
@override
String get room_management => 'Управление на сървъра за стая';
@override @override
String get repeater_managementTools => 'Инструменти за управление'; String get repeater_managementTools => 'Инструменти за управление';
+20
View File
@@ -200,6 +200,20 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get settings_locationInvalid => 'Ungültige Breiten- oder Längengrade.'; String get settings_locationInvalid => 'Ungültige Breiten- oder Längengrade.';
@override
String get settings_locationGPSEnable => 'GPS Enable';
@override
String get settings_locationGPSEnableSubtitle =>
'Enables GPS to automatically update location.';
@override
String get settings_locationIntervalSec => 'Interval for GPS (Seconds)';
@override
String get settings_locationIntervalInvalid =>
'Interval must be at least 60 seconds, and less than 86400 seconds.';
@override @override
String get settings_latitude => 'Breitengrad'; String get settings_latitude => 'Breitengrad';
@@ -647,6 +661,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get contacts_manageRepeater => 'Wiederholungen verwalten'; String get contacts_manageRepeater => 'Wiederholungen verwalten';
@override
String get contacts_manageRoom => 'Manage Room Server';
@override @override
String get contacts_roomLogin => 'Raum-Login'; String get contacts_roomLogin => 'Raum-Login';
@@ -1586,6 +1603,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get repeater_management => 'Repeater-Verwaltung'; String get repeater_management => 'Repeater-Verwaltung';
@override
String get room_management => 'Room Server Management';
@override @override
String get repeater_managementTools => 'Verwaltungs-Tools'; String get repeater_managementTools => 'Verwaltungs-Tools';
+23 -3
View File
@@ -190,7 +190,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get settings_locationSubtitle => 'GPS coordinates'; String get settings_locationSubtitle => 'GPS coordinates';
@override @override
String get settings_locationUpdated => 'Location updated'; String get settings_locationUpdated => 'Location and GPS settings updated';
@override @override
String get settings_locationBothRequired => String get settings_locationBothRequired =>
@@ -199,6 +199,20 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settings_locationInvalid => 'Invalid latitude or longitude.'; String get settings_locationInvalid => 'Invalid latitude or longitude.';
@override
String get settings_locationGPSEnable => 'GPS Enable';
@override
String get settings_locationGPSEnableSubtitle =>
'Enables GPS to automatically update location.';
@override
String get settings_locationIntervalSec => 'Interval for GPS (Seconds)';
@override
String get settings_locationIntervalInvalid =>
'Interval must be at least 60 seconds, and less than 86400 seconds.';
@override @override
String get settings_latitude => 'Latitude'; String get settings_latitude => 'Latitude';
@@ -641,7 +655,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get contacts_manageRepeater => 'Manage Repeater'; String get contacts_manageRepeater => 'Manage Repeater';
@override @override
String get contacts_roomLogin => 'Room Login'; String get contacts_manageRoom => 'Manage Room Server';
@override
String get contacts_roomLogin => 'Room Server Login';
@override @override
String get contacts_openChat => 'Open Chat'; String get contacts_openChat => 'Open Chat';
@@ -1439,7 +1456,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get login_repeaterLogin => 'Repeater Login'; String get login_repeaterLogin => 'Repeater Login';
@override @override
String get login_roomLogin => 'Room Login'; String get login_roomLogin => 'Room Server Login';
@override @override
String get login_password => 'Password'; String get login_password => 'Password';
@@ -1561,6 +1578,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get repeater_management => 'Repeater Management'; String get repeater_management => 'Repeater Management';
@override
String get room_management => 'Room Server Management';
@override @override
String get repeater_managementTools => 'Management Tools'; String get repeater_managementTools => 'Management Tools';
+20
View File
@@ -200,6 +200,20 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get settings_locationInvalid => 'Latitud o longitud inválidos.'; String get settings_locationInvalid => 'Latitud o longitud inválidos.';
@override
String get settings_locationGPSEnable => 'Habilitar GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Habilita la actualización automática de la ubicación mediante GPS.';
@override
String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)';
@override
String get settings_locationIntervalInvalid =>
'El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.';
@override @override
String get settings_latitude => 'Latitud'; String get settings_latitude => 'Latitud';
@@ -648,6 +662,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get contacts_manageRepeater => 'Gestionar Repetidor'; String get contacts_manageRepeater => 'Gestionar Repetidor';
@override
String get contacts_manageRoom => 'Gestionar Servidor de Habitación';
@override @override
String get contacts_roomLogin => 'Inicio de Sala'; String get contacts_roomLogin => 'Inicio de Sala';
@@ -1585,6 +1602,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get repeater_management => 'Gestión de Repetidores'; String get repeater_management => 'Gestión de Repetidores';
@override
String get room_management => 'Administración del Servidor de Habitación';
@override @override
String get repeater_managementTools => 'Herramientas de Gestión'; String get repeater_managementTools => 'Herramientas de Gestión';
+20
View File
@@ -200,6 +200,20 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get settings_locationInvalid => 'Latitude ou longitude invalide.'; String get settings_locationInvalid => 'Latitude ou longitude invalide.';
@override
String get settings_locationGPSEnable => 'Habilita GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Habilita la actualización automática de la ubicación mediante GPS.';
@override
String get settings_locationIntervalSec => 'Intervalo pour GPS (Segundos)';
@override
String get settings_locationIntervalInvalid =>
'El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.';
@override @override
String get settings_latitude => 'Latitude'; String get settings_latitude => 'Latitude';
@@ -649,6 +663,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get contacts_manageRepeater => 'Gérer le répétiteur'; String get contacts_manageRepeater => 'Gérer le répétiteur';
@override
String get contacts_manageRoom => 'Gestionar Servidor de Habitación';
@override @override
String get contacts_roomLogin => 'Connexion Salle'; String get contacts_roomLogin => 'Connexion Salle';
@@ -1591,6 +1608,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get repeater_management => 'Gestion des répétiteurs'; String get repeater_management => 'Gestion des répétiteurs';
@override
String get room_management => 'Administración del Servidor de Habitación';
@override @override
String get repeater_managementTools => 'Outils de Gestion'; String get repeater_managementTools => 'Outils de Gestion';
+20
View File
@@ -200,6 +200,20 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get settings_locationInvalid => 'Latitudine o longitudine non valida.'; String get settings_locationInvalid => 'Latitudine o longitudine non valida.';
@override
String get settings_locationGPSEnable => 'Abilita GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Abilita l\'aggiornamento automatico della posizione tramite GPS.';
@override
String get settings_locationIntervalSec => 'Intervallo GPS (Secondi)';
@override
String get settings_locationIntervalInvalid =>
'L\'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.';
@override @override
String get settings_latitude => 'Latitudine'; String get settings_latitude => 'Latitudine';
@@ -646,6 +660,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get contacts_manageRepeater => 'Gestisci Ripetitore'; String get contacts_manageRepeater => 'Gestisci Ripetitore';
@override
String get contacts_manageRoom => 'Gestisci Server Camera';
@override @override
String get contacts_roomLogin => 'Login Camera'; String get contacts_roomLogin => 'Login Camera';
@@ -1583,6 +1600,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_management => 'Gestione Ripetitori'; String get repeater_management => 'Gestione Ripetitori';
@override
String get room_management => 'Gestione del Server di Camera';
@override @override
String get repeater_managementTools => 'Strumenti di Gestione'; String get repeater_managementTools => 'Strumenti di Gestione';
+20
View File
@@ -200,6 +200,20 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_locationInvalid => String get settings_locationInvalid =>
'Ongeldige breedtegraad of lengtegraad.'; 'Ongeldige breedtegraad of lengtegraad.';
@override
String get settings_locationGPSEnable => 'GPS inschakelen';
@override
String get settings_locationGPSEnableSubtitle =>
'Activeer automatisch locatieupdates via GPS.';
@override
String get settings_locationIntervalSec => 'Interval voor GPS (Seconden)';
@override
String get settings_locationIntervalInvalid =>
'De intervallen moeten minstens 60 seconden zijn en minder dan 86400 seconden.';
@override @override
String get settings_latitude => 'Breedtegraad'; String get settings_latitude => 'Breedtegraad';
@@ -644,6 +658,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get contacts_manageRepeater => 'Beheer Repeater'; String get contacts_manageRepeater => 'Beheer Repeater';
@override
String get contacts_manageRoom => 'Beheer Ruimte Server';
@override @override
String get contacts_roomLogin => 'Ruimte Inloggen'; String get contacts_roomLogin => 'Ruimte Inloggen';
@@ -1578,6 +1595,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get repeater_management => 'Beheer Repeaters'; String get repeater_management => 'Beheer Repeaters';
@override
String get room_management => 'Beheer Server Kamer';
@override @override
String get repeater_managementTools => 'Beheerinstrumenten'; String get repeater_managementTools => 'Beheerinstrumenten';
+20
View File
@@ -202,6 +202,20 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_locationInvalid => String get settings_locationInvalid =>
'Nieprawidłowa szerokość geograficzna lub długość geograficzna.'; 'Nieprawidłowa szerokość geograficzna lub długość geograficzna.';
@override
String get settings_locationGPSEnable => 'Włącz GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Włącza automatyczne aktualizowanie pozycji za pomocą GPS.';
@override
String get settings_locationIntervalSec => 'Interwał dla GPS (Sekundy)';
@override
String get settings_locationIntervalInvalid =>
'Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.';
@override @override
String get settings_latitude => 'Szerokość'; String get settings_latitude => 'Szerokość';
@@ -649,6 +663,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get contacts_manageRepeater => 'Zarządzaj Powtórzami'; String get contacts_manageRepeater => 'Zarządzaj Powtórzami';
@override
String get contacts_manageRoom => 'Zarządzaj Serwerem Pokoju';
@override @override
String get contacts_roomLogin => 'Logowanie do pokoju'; String get contacts_roomLogin => 'Logowanie do pokoju';
@@ -1587,6 +1604,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get repeater_management => 'Zarządzanie Powtórzami'; String get repeater_management => 'Zarządzanie Powtórzami';
@override
String get room_management => 'Zarządzanie Serwerem Pokoju';
@override @override
String get repeater_managementTools => 'Narzędzia Zarządzania'; String get repeater_managementTools => 'Narzędzia Zarządzania';
+20
View File
@@ -201,6 +201,20 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get settings_locationInvalid => 'Latitude ou longitude inválidos.'; String get settings_locationInvalid => 'Latitude ou longitude inválidos.';
@override
String get settings_locationGPSEnable => 'Ativar GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Habilita a atualização automática da localização via GPS.';
@override
String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)';
@override
String get settings_locationIntervalInvalid =>
'O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.';
@override @override
String get settings_latitude => 'Latitude'; String get settings_latitude => 'Latitude';
@@ -649,6 +663,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get contacts_manageRepeater => 'Gerenciar Repetidor'; String get contacts_manageRepeater => 'Gerenciar Repetidor';
@override
String get contacts_manageRoom => 'Gerenciar Servidor de Sala';
@override @override
String get contacts_roomLogin => 'Login no Quarto'; String get contacts_roomLogin => 'Login no Quarto';
@@ -1585,6 +1602,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get repeater_management => 'Gerenciamento de Repetidor'; String get repeater_management => 'Gerenciamento de Repetidor';
@override
String get room_management => 'Gerenciamento de Servidor de Sala';
@override @override
String get repeater_managementTools => 'Ferramentas de Gerenciamento'; String get repeater_managementTools => 'Ferramentas de Gerenciamento';
+21 -1
View File
@@ -200,6 +200,20 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get settings_locationInvalid => 'Neplatná šírka alebo dĺžka.'; String get settings_locationInvalid => 'Neplatná šírka alebo dĺžka.';
@override
String get settings_locationGPSEnable => 'Aktivovať GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Povolí automatické aktualizovanie polohy pomocou GPS.';
@override
String get settings_locationIntervalSec => 'Interval pre GPS (Sekundy)';
@override
String get settings_locationIntervalInvalid =>
'Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.';
@override @override
String get settings_latitude => 'Súradnica'; String get settings_latitude => 'Súradnica';
@@ -642,6 +656,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get contacts_manageRepeater => 'Spravovať opakované zoznamy'; String get contacts_manageRepeater => 'Spravovať opakované zoznamy';
@override
String get contacts_manageRoom => 'Spravovať server miestnosti';
@override @override
String get contacts_roomLogin => 'Prihlásenie do miestnosti'; String get contacts_roomLogin => 'Prihlásenie do miestnosti';
@@ -1100,7 +1117,7 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get chat_clearPathSubtitle => String get chat_clearPathSubtitle =>
'Znovu nájsť vynútene pri nasledujacej pošlite'; 'Znovu nájsť vynútene pri nasledujúcej pošlite';
@override @override
String get chat_pathCleared => String get chat_pathCleared =>
@@ -1580,6 +1597,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get repeater_management => 'Správa opakérov'; String get repeater_management => 'Správa opakérov';
@override
String get room_management => 'Správa servera miestnosti';
@override @override
String get repeater_managementTools => 'Nástroje na správu'; String get repeater_managementTools => 'Nástroje na správu';
+24 -4
View File
@@ -200,6 +200,20 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_locationInvalid => String get settings_locationInvalid =>
'Neveljna zemeljska širina ali dolžina.'; 'Neveljna zemeljska širina ali dolžina.';
@override
String get settings_locationGPSEnable => 'Omogoči GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Omogoči samodejno posodabljanje lokacije z GPS-jem.';
@override
String get settings_locationIntervalSec => 'Interval za GPS (Sekunde)';
@override
String get settings_locationIntervalInvalid =>
'Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.';
@override @override
String get settings_latitude => 'Širina'; String get settings_latitude => 'Širina';
@@ -435,7 +449,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get appSettings_enableNotificationsSubtitle => String get appSettings_enableNotificationsSubtitle =>
'Prejmujte obvestila o sporočilih in oglasih'; 'Prejmite obvestila o sporočilih in oglasih';
@override @override
String get appSettings_notificationPermissionDenied => String get appSettings_notificationPermissionDenied =>
@@ -631,7 +645,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get contacts_noContactsFound => String get contacts_noContactsFound =>
'Niti ena osebe ali skupine ni najdena.'; 'Niti ena oseba ali skupine ni najdena.';
@override @override
String get contacts_deleteContact => 'Izbrisati Kontakt'; String get contacts_deleteContact => 'Izbrisati Kontakt';
@@ -644,6 +658,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get contacts_manageRepeater => 'Upravljajte Ponovitve'; String get contacts_manageRepeater => 'Upravljajte Ponovitve';
@override
String get contacts_manageRoom => 'Upravljajte strežnik sobe';
@override @override
String get contacts_roomLogin => 'Vnos v sobo'; String get contacts_roomLogin => 'Vnos v sobo';
@@ -680,7 +697,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get contacts_noContactsMatchFilter => String get contacts_noContactsMatchFilter =>
'Niti ena osebe ne ustreza vašemu kriteriju.'; 'Niti ena oseba ne ustreza vašemu kriteriju.';
@override @override
String get contacts_noMembers => 'Nič članov.'; String get contacts_noMembers => 'Nič članov.';
@@ -1186,7 +1203,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get map_nodesNeedGps => String get map_nodesNeedGps =>
'Omrežje morajo deliti svoje GPS koordinate,\nda se prikazajo na zemljeobrazniku.'; 'Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.';
@override @override
String map_nodesCount(int count) { String map_nodesCount(int count) {
@@ -1580,6 +1597,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get repeater_management => 'Upravljanje ponovitve'; String get repeater_management => 'Upravljanje ponovitve';
@override
String get room_management => 'Upravljanje stremlišča';
@override @override
String get repeater_managementTools => 'Upravne orodje'; String get repeater_managementTools => 'Upravne orodje';
+20
View File
@@ -199,6 +199,20 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get settings_locationInvalid => 'Ogiltig latitud eller longitud.'; String get settings_locationInvalid => 'Ogiltig latitud eller longitud.';
@override
String get settings_locationGPSEnable => 'Aktivera GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Aktivera automatiska uppdateringar av platsen med hjälp av GPS.';
@override
String get settings_locationIntervalSec => 'Interval för GPS (Sekunder)';
@override
String get settings_locationIntervalInvalid =>
'Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.';
@override @override
String get settings_latitude => 'Latitud'; String get settings_latitude => 'Latitud';
@@ -638,6 +652,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get contacts_manageRepeater => 'Hantera Upprepare'; String get contacts_manageRepeater => 'Hantera Upprepare';
@override
String get contacts_manageRoom => 'Hantera Rumserver';
@override @override
String get contacts_roomLogin => 'Rum Inloggning'; String get contacts_roomLogin => 'Rum Inloggning';
@@ -1569,6 +1586,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get repeater_management => 'Återuppspelarens Hantering'; String get repeater_management => 'Återuppspelarens Hantering';
@override
String get room_management => 'Rumserverhantering';
@override @override
String get repeater_managementTools => 'Administrationsverktyg'; String get repeater_managementTools => 'Administrationsverktyg';
+18
View File
@@ -196,6 +196,18 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get settings_locationInvalid => '无效的纬度或经度。'; String get settings_locationInvalid => '无效的纬度或经度。';
@override
String get settings_locationGPSEnable => '启用GPS';
@override
String get settings_locationGPSEnableSubtitle => '启用GPS自动更新位置。';
@override
String get settings_locationIntervalSec => 'GPS 间隔(秒)';
@override
String get settings_locationIntervalInvalid => '时间间隔必须至少为60秒,且小于86400秒。';
@override @override
String get settings_latitude => '纬度'; String get settings_latitude => '纬度';
@@ -611,6 +623,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get contacts_manageRepeater => '管理重复项'; String get contacts_manageRepeater => '管理重复项';
@override
String get contacts_manageRoom => '管理房间服务器';
@override @override
String get contacts_roomLogin => '房间登录'; String get contacts_roomLogin => '房间登录';
@@ -1513,6 +1528,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get repeater_management => '重复器管理'; String get repeater_management => '重复器管理';
@override
String get room_management => '房间服务器管理';
@override @override
String get repeater_managementTools => '管理工具'; String get repeater_managementTools => '管理工具';
+7 -1
View File
@@ -1378,5 +1378,11 @@
} }
}, },
"neighbors_unknownContact": "Onbekende {pubkey}", "neighbors_unknownContact": "Onbekende {pubkey}",
"neighbors_heardAgo": "Horen: {time} geleden" "neighbors_heardAgo": "Horen: {time} geleden",
"settings_locationGPSEnable": "GPS inschakelen",
"settings_locationGPSEnableSubtitle": "Activeer automatisch locatieupdates via GPS.",
"settings_locationIntervalSec": "Interval voor GPS (Seconden)",
"settings_locationIntervalInvalid": "De intervallen moeten minstens 60 seconden zijn en minder dan 86400 seconden.",
"contacts_manageRoom": "Beheer Ruimte Server",
"room_management": "Beheer Server Kamer"
} }
+7 -1
View File
@@ -1378,5 +1378,11 @@
} }
}, },
"neighbors_heardAgo": "Usłyszano: {time} temu", "neighbors_heardAgo": "Usłyszano: {time} temu",
"neighbors_unknownContact": "Nieznana {pubkey}" "neighbors_unknownContact": "Nieznana {pubkey}",
"settings_locationGPSEnable": "Włącz GPS",
"settings_locationGPSEnableSubtitle": "Włącza automatyczne aktualizowanie pozycji za pomocą GPS.",
"settings_locationIntervalSec": "Interwał dla GPS (Sekundy)",
"settings_locationIntervalInvalid": "Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.",
"contacts_manageRoom": "Zarządzaj Serwerem Pokoju",
"room_management": "Zarządzanie Serwerem Pokoju"
} }
+7 -1
View File
@@ -1378,5 +1378,11 @@
} }
}, },
"neighbors_heardAgo": "Ouvido: {time} atrás", "neighbors_heardAgo": "Ouvido: {time} atrás",
"neighbors_unknownContact": "{pubkey} Desconhecido" "neighbors_unknownContact": "{pubkey} Desconhecido",
"settings_locationGPSEnable": "Ativar GPS",
"settings_locationGPSEnableSubtitle": "Habilita a atualização automática da localização via GPS.",
"settings_locationIntervalInvalid": "O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.",
"settings_locationIntervalSec": "Intervalo para GPS (Segundos)",
"contacts_manageRoom": "Gerenciar Servidor de Sala",
"room_management": "Gerenciamento de Servidor de Sala"
} }
+8 -2
View File
@@ -559,7 +559,7 @@
"chat_setCustomPath": "Nastaviť vlastnú cestu", "chat_setCustomPath": "Nastaviť vlastnú cestu",
"chat_setCustomPathSubtitle": "Ručne zadajte trasu.", "chat_setCustomPathSubtitle": "Ručne zadajte trasu.",
"chat_clearPath": "Vyčistiš cestu", "chat_clearPath": "Vyčistiš cestu",
"chat_clearPathSubtitle": "Znovu nájsť vynútene pri nasledujacej pošlite", "chat_clearPathSubtitle": "Znovu nájsť vynútene pri nasledujúcej pošlite",
"chat_pathCleared": "Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.", "chat_pathCleared": "Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.",
"chat_floodModeSubtitle": "Použite prepínanie trasy v navigačnom paneli.", "chat_floodModeSubtitle": "Použite prepínanie trasy v navigačnom paneli.",
"chat_floodModeEnabled": "Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.", "chat_floodModeEnabled": "Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.",
@@ -1378,5 +1378,11 @@
} }
}, },
"neighbors_heardAgo": "Počuli sme to: {time} dozadu", "neighbors_heardAgo": "Počuli sme to: {time} dozadu",
"neighbors_unknownContact": "Neznáma {pubkey}" "neighbors_unknownContact": "Neznáma {pubkey}",
"settings_locationGPSEnable": "Aktivovať GPS",
"settings_locationGPSEnableSubtitle": "Povolí automatické aktualizovanie polohy pomocou GPS.",
"settings_locationIntervalSec": "Interval pre GPS (Sekundy)",
"settings_locationIntervalInvalid": "Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.",
"contacts_manageRoom": "Spravovať server miestnosti",
"room_management": "Správa servera miestnosti"
} }
+11 -5
View File
@@ -176,7 +176,7 @@
"appSettings_languageBg": "Български", "appSettings_languageBg": "Български",
"appSettings_notifications": "Obveščanja", "appSettings_notifications": "Obveščanja",
"appSettings_enableNotifications": "Omogoči obveščanje", "appSettings_enableNotifications": "Omogoči obveščanje",
"appSettings_enableNotificationsSubtitle": "Prejmujte obvestila o sporočilih in oglasih", "appSettings_enableNotificationsSubtitle": "Prejmite obvestila o sporočilih in oglasih",
"appSettings_notificationPermissionDenied": "Odobritev obvestila zavrnjena", "appSettings_notificationPermissionDenied": "Odobritev obvestila zavrnjena",
"appSettings_notificationsEnabled": "Obvestila omogočena", "appSettings_notificationsEnabled": "Obvestila omogočena",
"appSettings_notificationsDisabled": "Obvestila so izklopljena", "appSettings_notificationsDisabled": "Obvestila so izklopljena",
@@ -256,7 +256,7 @@
"contacts_contactsWillAppear": "Kontakti se bodo prikazali, ko naprave oglasijo.", "contacts_contactsWillAppear": "Kontakti se bodo prikazali, ko naprave oglasijo.",
"contacts_searchContacts": "Iskanje kontaktov...", "contacts_searchContacts": "Iskanje kontaktov...",
"contacts_noUnreadContacts": "Nerešeno kontaktov.", "contacts_noUnreadContacts": "Nerešeno kontaktov.",
"contacts_noContactsFound": "Niti ena osebe ali skupine ni najdena.", "contacts_noContactsFound": "Niti ena oseba ali skupine ni najdena.",
"contacts_deleteContact": "Izbrisati Kontakt", "contacts_deleteContact": "Izbrisati Kontakt",
"contacts_removeConfirm": "Izbrisati {contactName} iz kontaktov?", "contacts_removeConfirm": "Izbrisati {contactName} iz kontaktov?",
"@contacts_removeConfirm": { "@contacts_removeConfirm": {
@@ -291,7 +291,7 @@
} }
}, },
"contacts_filterContacts": "Filtri kontakt\\,...", "contacts_filterContacts": "Filtri kontakt\\,...",
"contacts_noContactsMatchFilter": "Niti ena osebe ne ustreza vašemu kriteriju.", "contacts_noContactsMatchFilter": "Niti ena oseba ne ustreza vašemu kriteriju.",
"contacts_noMembers": "Nič članov.", "contacts_noMembers": "Nič članov.",
"contacts_lastSeenNow": "Datum zadnjega vpisa zdaj", "contacts_lastSeenNow": "Datum zadnjega vpisa zdaj",
"contacts_lastSeenMinsAgo": "Zadnjič videti {minutes} minut nazaj", "contacts_lastSeenMinsAgo": "Zadnjič videti {minutes} minut nazaj",
@@ -606,7 +606,7 @@
}, },
"map_title": "Mapa omrežja", "map_title": "Mapa omrežja",
"map_noNodesWithLocation": "Nihče od notranjih elementov nima podatkov o lokaciji.", "map_noNodesWithLocation": "Nihče od notranjih elementov nima podatkov o lokaciji.",
"map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazajo na zemljeobrazniku.", "map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.",
"map_nodesCount": "Omize: {count}", "map_nodesCount": "Omize: {count}",
"@map_nodesCount": { "@map_nodesCount": {
"placeholders": { "placeholders": {
@@ -1378,5 +1378,11 @@
} }
}, },
"neighbors_unknownContact": "Nepoznano {pubkey}", "neighbors_unknownContact": "Nepoznano {pubkey}",
"neighbors_heardAgo": "Udeleženec je prejel sporočilo {time} nazaj." "neighbors_heardAgo": "Udeleženec je prejel sporočilo {time} nazaj.",
"settings_locationGPSEnable": "Omogoči GPS",
"settings_locationGPSEnableSubtitle": "Omogoči samodejno posodabljanje lokacije z GPS-jem.",
"settings_locationIntervalSec": "Interval za GPS (Sekunde)",
"settings_locationIntervalInvalid": "Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.",
"contacts_manageRoom": "Upravljajte strežnik sobe",
"room_management": "Upravljanje stremlišča"
} }
+7 -1
View File
@@ -1378,5 +1378,11 @@
} }
}, },
"neighbors_heardAgo": "Hördes: {time} sedan", "neighbors_heardAgo": "Hördes: {time} sedan",
"neighbors_unknownContact": "Okänd {pubkey}" "neighbors_unknownContact": "Okänd {pubkey}",
"settings_locationGPSEnable": "Aktivera GPS",
"settings_locationGPSEnableSubtitle": "Aktivera automatiska uppdateringar av platsen med hjälp av GPS.",
"settings_locationIntervalSec": "Interval för GPS (Sekunder)",
"settings_locationIntervalInvalid": "Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.",
"contacts_manageRoom": "Hantera Rumserver",
"room_management": "Rumserverhantering"
} }
+7 -1
View File
@@ -1378,5 +1378,11 @@
} }
}, },
"neighbors_heardAgo": "听到的时间:{time}前", "neighbors_heardAgo": "听到的时间:{time}前",
"neighbors_unknownContact": "未知{pubkey}" "neighbors_unknownContact": "未知{pubkey}",
"settings_locationGPSEnable": "启用GPS",
"settings_locationGPSEnableSubtitle": "启用GPS自动更新位置。",
"settings_locationIntervalSec": "GPS 间隔(秒)",
"settings_locationIntervalInvalid": "时间间隔必须至少为60秒,且小于86400秒。",
"contacts_manageRoom": "管理房间服务器",
"room_management": "房间服务器管理"
} }
+181 -80
View File
@@ -27,13 +27,15 @@ import 'map_screen.dart';
import 'repeater_hub_screen.dart'; import 'repeater_hub_screen.dart';
import 'settings_screen.dart'; import 'settings_screen.dart';
enum RoomLoginDestination {
chat,
management,
}
class ContactsScreen extends StatefulWidget { class ContactsScreen extends StatefulWidget {
final bool hideBackButton; final bool hideBackButton;
const ContactsScreen({ const ContactsScreen({super.key, this.hideBackButton = false});
super.key,
this.hideBackButton = false,
});
@override @override
State<ContactsScreen> createState() => _ContactsScreenState(); State<ContactsScreen> createState() => _ContactsScreenState();
@@ -114,7 +116,8 @@ class _ContactsScreenState extends State<ContactsScreen>
top: false, top: false,
child: QuickSwitchBar( child: QuickSwitchBar(
selectedIndex: 0, selectedIndex: 0,
onDestinationSelected: (index) => _handleQuickSwitch(index, context), onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
), ),
), ),
), ),
@@ -168,8 +171,9 @@ class _ContactsScreenState extends State<ContactsScreen>
} }
final filteredAndSorted = _filterAndSortContacts(contacts, connector); final filteredAndSorted = _filterAndSortContacts(contacts, connector);
final filteredGroups = final filteredGroups = _showUnreadOnly
_showUnreadOnly ? const <ContactGroup>[] : _filterAndSortGroups(_groups, contacts); ? const <ContactGroup>[]
: _filterAndSortGroups(_groups, contacts);
return Column( return Column(
children: [ children: [
@@ -199,7 +203,10 @@ class _ContactsScreenState extends State<ContactsScreen>
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
), ),
onChanged: (value) { onChanged: (value) {
_searchDebounce?.cancel(); _searchDebounce?.cancel();
@@ -238,14 +245,18 @@ class _ContactsScreenState extends State<ContactsScreen>
final group = filteredGroups[index]; final group = filteredGroups[index];
return _buildGroupTile(context, group, contacts); return _buildGroupTile(context, group, contacts);
} }
final contact = filteredAndSorted[index - filteredGroups.length]; final contact =
final unreadCount = connector.getUnreadCountForContact(contact); filteredAndSorted[index - filteredGroups.length];
final unreadCount = connector.getUnreadCountForContact(
contact,
);
return _ContactTile( return _ContactTile(
contact: contact, contact: contact,
lastSeen: _resolveLastSeen(contact), lastSeen: _resolveLastSeen(contact),
unreadCount: unreadCount, unreadCount: unreadCount,
onTap: () => _openChat(context, contact), onTap: () => _openChat(context, contact),
onLongPress: () => _showContactOptions(context, connector, contact), onLongPress: () =>
_showContactOptions(context, connector, contact),
); );
}, },
), ),
@@ -255,35 +266,48 @@ class _ContactsScreenState extends State<ContactsScreen>
); );
} }
List<ContactGroup> _filterAndSortGroups(List<ContactGroup> groups, List<Contact> contacts) { List<ContactGroup> _filterAndSortGroups(
List<ContactGroup> groups,
List<Contact> contacts,
) {
final query = _searchQuery.trim().toLowerCase(); final query = _searchQuery.trim().toLowerCase();
final contactsByKey = <String, Contact>{}; final contactsByKey = <String, Contact>{};
for (final contact in contacts) { for (final contact in contacts) {
contactsByKey[contact.publicKeyHex] = contact; contactsByKey[contact.publicKeyHex] = contact;
} }
final filtered = groups.where((group) { final filtered = groups
if (query.isEmpty) return true; .where((group) {
if (group.name.toLowerCase().contains(query)) return true; if (query.isEmpty) return true;
for (final key in group.memberKeys) { if (group.name.toLowerCase().contains(query)) return true;
final contact = contactsByKey[key]; for (final key in group.memberKeys) {
if (contact != null && matchesContactQuery(contact, query)) return true; final contact = contactsByKey[key];
} if (contact != null && matchesContactQuery(contact, query)) {
return false; return true;
}).where((group) { }
if (_typeFilter == ContactTypeFilter.all) return true; }
for (final key in group.memberKeys) { return false;
final contact = contactsByKey[key]; })
if (contact != null && _matchesTypeFilter(contact)) return true; .where((group) {
} if (_typeFilter == ContactTypeFilter.all) return true;
return false; for (final key in group.memberKeys) {
}).toList(); final contact = contactsByKey[key];
if (contact != null && _matchesTypeFilter(contact)) return true;
}
return false;
})
.toList();
filtered.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); filtered.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
return filtered; return filtered;
} }
List<Contact> _filterAndSortContacts(List<Contact> contacts, MeshCoreConnector connector) { List<Contact> _filterAndSortContacts(
List<Contact> contacts,
MeshCoreConnector connector,
) {
var filtered = contacts.where((contact) { var filtered = contacts.where((contact) {
if (_searchQuery.isEmpty) return true; if (_searchQuery.isEmpty) return true;
return matchesContactQuery(contact, _searchQuery); return matchesContactQuery(contact, _searchQuery);
@@ -301,19 +325,27 @@ class _ContactsScreenState extends State<ContactsScreen>
switch (_sortOption) { switch (_sortOption) {
case ContactSortOption.lastSeen: case ContactSortOption.lastSeen:
filtered.sort((a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a))); filtered.sort(
(a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)),
);
break; break;
case ContactSortOption.recentMessages: case ContactSortOption.recentMessages:
filtered.sort((a, b) { filtered.sort((a, b) {
final aMessages = connector.getMessages(a); final aMessages = connector.getMessages(a);
final bMessages = connector.getMessages(b); final bMessages = connector.getMessages(b);
final aLastMsg = aMessages.isEmpty ? DateTime(1970) : aMessages.last.timestamp; final aLastMsg = aMessages.isEmpty
final bLastMsg = bMessages.isEmpty ? DateTime(1970) : bMessages.last.timestamp; ? DateTime(1970)
: aMessages.last.timestamp;
final bLastMsg = bMessages.isEmpty
? DateTime(1970)
: bMessages.last.timestamp;
return bLastMsg.compareTo(aLastMsg); return bLastMsg.compareTo(aLastMsg);
}); });
break; break;
case ContactSortOption.name: case ContactSortOption.name:
filtered.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); filtered.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
break; break;
} }
@@ -340,7 +372,11 @@ class _ContactsScreenState extends State<ContactsScreen>
: contact.lastSeen; : contact.lastSeen;
} }
Widget _buildGroupTile(BuildContext context, ContactGroup group, List<Contact> contacts) { Widget _buildGroupTile(
BuildContext context,
ContactGroup group,
List<Contact> contacts,
) {
final memberContacts = _resolveGroupContacts(group, contacts); final memberContacts = _resolveGroupContacts(group, contacts);
final subtitle = _formatGroupMembers(context, memberContacts); final subtitle = _formatGroupMembers(context, memberContacts);
return ListTile( return ListTile(
@@ -359,7 +395,10 @@ class _ContactsScreenState extends State<ContactsScreen>
); );
} }
List<Contact> _resolveGroupContacts(ContactGroup group, List<Contact> contacts) { List<Contact> _resolveGroupContacts(
ContactGroup group,
List<Contact> contacts,
) {
final byKey = <String, Contact>{}; final byKey = <String, Contact>{};
for (final contact in contacts) { for (final contact in contacts) {
byKey[contact.publicKeyHex] = contact; byKey[contact.publicKeyHex] = contact;
@@ -371,7 +410,9 @@ class _ContactsScreenState extends State<ContactsScreen>
resolved.add(contact); resolved.add(contact);
} }
} }
resolved.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); resolved.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
return resolved; return resolved;
} }
@@ -387,7 +428,7 @@ class _ContactsScreenState extends State<ContactsScreen>
if (contact.type == advTypeRepeater) { if (contact.type == advTypeRepeater) {
_showRepeaterLogin(context, contact); _showRepeaterLogin(context, contact);
} else if (contact.type == advTypeRoom) { } else if (contact.type == advTypeRoom) {
_showRoomLogin(context, contact); _showRoomLogin(context, contact, RoomLoginDestination.chat);
} else { } else {
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex); context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
Navigator.push( Navigator.push(
@@ -403,17 +444,13 @@ class _ContactsScreenState extends State<ContactsScreen>
case 1: case 1:
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
buildQuickSwitchRoute( buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
const ChannelsScreen(hideBackButton: true),
),
); );
break; break;
case 2: case 2:
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
buildQuickSwitchRoute( buildQuickSwitchRoute(const MapScreen(hideBackButton: true)),
const MapScreen(hideBackButton: true),
),
); );
break; break;
} }
@@ -429,10 +466,8 @@ class _ContactsScreenState extends State<ContactsScreen>
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => RepeaterHubScreen( builder: (context) =>
repeater: repeater, RepeaterHubScreen(repeater: repeater, password: password),
password: password,
),
), ),
); );
}, },
@@ -440,18 +475,23 @@ class _ContactsScreenState extends State<ContactsScreen>
); );
} }
void _showRoomLogin(BuildContext context, Contact room) { void _showRoomLogin(
BuildContext context,
Contact room,
RoomLoginDestination destination,
) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => RoomLoginDialog( builder: (context) => RoomLoginDialog(
room: room, room: room,
onLogin: (password) { onLogin: (password) {
// Navigate to chat screen after successful login
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex); context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ChatScreen(contact: room), builder: (context) => destination == RoomLoginDestination.management
? RepeaterHubScreen(repeater: room, password: password)
: ChatScreen(contact: room),
), ),
); );
}, },
@@ -459,7 +499,11 @@ class _ContactsScreenState extends State<ContactsScreen>
); );
} }
void _showGroupOptions(BuildContext context, ContactGroup group, List<Contact> contacts) { void _showGroupOptions(
BuildContext context,
ContactGroup group,
List<Contact> contacts,
) {
final members = _resolveGroupContacts(group, contacts); final members = _resolveGroupContacts(group, contacts);
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@@ -478,7 +522,10 @@ class _ContactsScreenState extends State<ContactsScreen>
), ),
ListTile( ListTile(
leading: const Icon(Icons.delete, color: Colors.red), leading: const Icon(Icons.delete, color: Colors.red),
title: Text(context.l10n.contacts_deleteGroup, style: const TextStyle(color: Colors.red)), title: Text(
context.l10n.contacts_deleteGroup,
style: const TextStyle(color: Colors.red),
),
onTap: () { onTap: () {
Navigator.pop(sheetContext); Navigator.pop(sheetContext);
_confirmDeleteGroup(context, group); _confirmDeleteGroup(context, group);
@@ -522,7 +569,10 @@ class _ContactsScreenState extends State<ContactsScreen>
}); });
await _saveGroups(); await _saveGroups();
}, },
child: Text(context.l10n.common_delete, style: const TextStyle(color: Colors.red)), child: Text(
context.l10n.common_delete,
style: const TextStyle(color: Colors.red),
),
), ),
], ],
), ),
@@ -548,10 +598,16 @@ class _ContactsScreenState extends State<ContactsScreen>
final filteredContacts = filterQuery.isEmpty final filteredContacts = filterQuery.isEmpty
? sortedContacts ? sortedContacts
: sortedContacts : sortedContacts
.where((contact) => matchesContactQuery(contact, filterQuery)) .where(
.toList(); (contact) => matchesContactQuery(contact, filterQuery),
)
.toList();
return AlertDialog( return AlertDialog(
title: Text(isEditing ? context.l10n.contacts_editGroup : context.l10n.contacts_newGroup), title: Text(
isEditing
? context.l10n.contacts_editGroup
: context.l10n.contacts_newGroup,
),
content: SizedBox( content: SizedBox(
width: double.maxFinite, width: double.maxFinite,
child: Column( child: Column(
@@ -582,12 +638,18 @@ class _ContactsScreenState extends State<ContactsScreen>
SizedBox( SizedBox(
height: 240, height: 240,
child: filteredContacts.isEmpty child: filteredContacts.isEmpty
? Center(child: Text(context.l10n.contacts_noContactsMatchFilter)) ? Center(
child: Text(
context.l10n.contacts_noContactsMatchFilter,
),
)
: ListView.builder( : ListView.builder(
itemCount: filteredContacts.length, itemCount: filteredContacts.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final contact = filteredContacts[index]; final contact = filteredContacts[index];
final isSelected = selectedKeys.contains(contact.publicKeyHex); final isSelected = selectedKeys.contains(
contact.publicKeyHex,
);
return CheckboxListTile( return CheckboxListTile(
value: isSelected, value: isSelected,
title: Text(contact.name), title: Text(contact.name),
@@ -618,7 +680,9 @@ class _ContactsScreenState extends State<ContactsScreen>
final name = nameController.text.trim(); final name = nameController.text.trim();
if (name.isEmpty) { if (name.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_groupNameRequired)), SnackBar(
content: Text(context.l10n.contacts_groupNameRequired),
),
); );
return; return;
} }
@@ -628,13 +692,19 @@ class _ContactsScreenState extends State<ContactsScreen>
}); });
if (exists) { if (exists) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_groupAlreadyExists(name))), SnackBar(
content: Text(
context.l10n.contacts_groupAlreadyExists(name),
),
),
); );
return; return;
} }
setState(() { setState(() {
if (isEditing) { if (isEditing) {
final index = _groups.indexWhere((g) => g.name == group.name); final index = _groups.indexWhere(
(g) => g.name == group.name,
);
if (index != -1) { if (index != -1) {
_groups[index] = ContactGroup( _groups[index] = ContactGroup(
name: name, name: name,
@@ -642,7 +712,12 @@ class _ContactsScreenState extends State<ContactsScreen>
); );
} }
} else { } else {
_groups.add(ContactGroup(name: name, memberKeys: selectedKeys.toList())); _groups.add(
ContactGroup(
name: name,
memberKeys: selectedKeys.toList(),
),
);
} }
}); });
await _saveGroups(); await _saveGroups();
@@ -650,7 +725,11 @@ class _ContactsScreenState extends State<ContactsScreen>
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
} }
}, },
child: Text(isEditing ? context.l10n.common_save : context.l10n.common_create), child: Text(
isEditing
? context.l10n.common_save
: context.l10n.common_create,
),
), ),
], ],
); );
@@ -682,16 +761,24 @@ class _ContactsScreenState extends State<ContactsScreen>
_showRepeaterLogin(context, contact); _showRepeaterLogin(context, contact);
}, },
) )
else if (isRoom) else if (isRoom) ...[
ListTile( ListTile(
leading: const Icon(Icons.room, color: Colors.blue), leading: const Icon(Icons.room, color: Colors.blue),
title: Text(context.l10n.contacts_roomLogin), title: Text(context.l10n.contacts_roomLogin),
onTap: () { onTap: () {
Navigator.pop(sheetContext); Navigator.pop(sheetContext);
_showRoomLogin(context, contact); _showRoomLogin(context, contact, RoomLoginDestination.chat);
}, },
) ),
else ListTile(
leading: const Icon(Icons.room_preferences, color: Colors.orange),
title: Text(context.l10n.room_management),
onTap: () {
Navigator.pop(sheetContext);
_showRoomLogin(context, contact, RoomLoginDestination.management);
},
),
] else
ListTile( ListTile(
leading: const Icon(Icons.chat), leading: const Icon(Icons.chat),
title: Text(context.l10n.contacts_openChat), title: Text(context.l10n.contacts_openChat),
@@ -702,7 +789,10 @@ class _ContactsScreenState extends State<ContactsScreen>
), ),
ListTile( ListTile(
leading: const Icon(Icons.delete, color: Colors.red), leading: const Icon(Icons.delete, color: Colors.red),
title: Text(context.l10n.contacts_deleteContact, style: const TextStyle(color: Colors.red)), title: Text(
context.l10n.contacts_deleteContact,
style: const TextStyle(color: Colors.red),
),
onTap: () { onTap: () {
Navigator.pop(sheetContext); Navigator.pop(sheetContext);
_confirmDelete(context, connector, contact); _confirmDelete(context, connector, contact);
@@ -734,7 +824,10 @@ class _ContactsScreenState extends State<ContactsScreen>
Navigator.pop(dialogContext); Navigator.pop(dialogContext);
connector.removeContact(contact); connector.removeContact(contact);
}, },
child: Text(context.l10n.common_delete, style: const TextStyle(color: Colors.red)), child: Text(
context.l10n.common_delete,
style: const TextStyle(color: Colors.red),
),
), ),
], ],
), ),
@@ -759,14 +852,17 @@ class _ContactTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final shotPublicKey = "<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>"; final shotPublicKey =
"<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>";
return ListTile( return ListTile(
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: _getTypeColor(contact.type), backgroundColor: _getTypeColor(contact.type),
child: _buildContactAvatar(contact), child: _buildContactAvatar(contact),
), ),
title: Text(contact.name), title: Text(contact.name),
subtitle: Text('${contact.typeLabel}${contact.pathLabel} $shotPublicKey'), subtitle: Text(
'${contact.typeLabel}${contact.pathLabel} $shotPublicKey',
),
trailing: Column( trailing: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
@@ -791,10 +887,7 @@ class _ContactTile extends StatelessWidget {
Widget _buildContactAvatar(Contact contact) { Widget _buildContactAvatar(Contact contact) {
final emoji = firstEmoji(contact.name); final emoji = firstEmoji(contact.name);
if (emoji != null) { if (emoji != null) {
return Text( return Text(emoji, style: const TextStyle(fontSize: 18));
emoji,
style: const TextStyle(fontSize: 18),
);
} }
return Icon(_getTypeIcon(contact.type), color: Colors.white, size: 20); return Icon(_getTypeIcon(contact.type), color: Colors.white, size: 20);
} }
@@ -833,13 +926,21 @@ class _ContactTile extends StatelessWidget {
final now = DateTime.now(); final now = DateTime.now();
final diff = now.difference(lastSeen); final diff = now.difference(lastSeen);
if (diff.isNegative || diff.inMinutes < 5) return context.l10n.contacts_lastSeenNow; if (diff.isNegative || diff.inMinutes < 5) {
if (diff.inMinutes < 60) return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes); return context.l10n.contacts_lastSeenNow;
}
if (diff.inMinutes < 60) {
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
}
if (diff.inHours < 24) { if (diff.inHours < 24) {
final hours = diff.inHours; final hours = diff.inHours;
return hours == 1 ? context.l10n.contacts_lastSeenHourAgo : context.l10n.contacts_lastSeenHoursAgo(hours); return hours == 1
? context.l10n.contacts_lastSeenHourAgo
: context.l10n.contacts_lastSeenHoursAgo(hours);
} }
final days = diff.inDays; final days = diff.inDays;
return days == 1 ? context.l10n.contacts_lastSeenDayAgo : context.l10n.contacts_lastSeenDaysAgo(days); return days == 1
? context.l10n.contacts_lastSeenDayAgo
: context.l10n.contacts_lastSeenDaysAgo(days);
} }
} }
+3
View File
@@ -273,6 +273,9 @@ class _MapScreenState extends State<MapScreen> {
initialZoom: initialZoom, initialZoom: initialZoom,
minZoom: 2.0, minZoom: 2.0,
maxZoom: 18.0, maxZoom: 18.0,
interactionOptions: InteractionOptions(
flags: ~InteractiveFlag.rotate
),
onTap: (_, latLng) { onTap: (_, latLng) {
if (_isSelectingPoi) { if (_isSelectingPoi) {
setState(() { setState(() {
+162 -145
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import 'repeater_status_screen.dart'; import 'repeater_status_screen.dart';
@@ -26,10 +27,17 @@ class RepeaterHubScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(l10n.repeater_management), Text(
repeater.type == advTypeRepeater
? l10n.repeater_management
: l10n.room_management,
),
Text( Text(
repeater.name, repeater.name,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal), style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
), ),
], ],
), ),
@@ -40,155 +48,167 @@ class RepeaterHubScreen extends StatelessWidget {
child: ListView( child: ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ children: [
// Repeater info card // Repeater info card
Card( Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 40, radius: 40,
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
child: const Icon(Icons.cell_tower, size: 40, color: Colors.white), child: const Icon(
Icons.cell_tower,
size: 40,
color: Colors.white,
), ),
const SizedBox(height: 16), ),
Text( const SizedBox(height: 16),
repeater.name, Text(
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), repeater.name,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
), ),
const SizedBox(height: 8), ),
Text( const SizedBox(height: 8),
'<${repeater.publicKeyHex.substring(0, 8)}...${repeater.publicKeyHex.substring(repeater.publicKeyHex.length - 8)}>', Text(
style: TextStyle(fontSize: 14, color: Colors.grey[600]), '<${repeater.publicKeyHex.substring(0, 8)}...${repeater.publicKeyHex.substring(repeater.publicKeyHex.length - 8)}>',
), style: TextStyle(fontSize: 14, color: Colors.grey[600]),
const SizedBox(height: 8), ),
Text( const SizedBox(height: 8),
repeater.pathLabel, Text(
style: TextStyle(fontSize: 14, color: Colors.grey[600]), repeater.pathLabel,
), style: TextStyle(fontSize: 14, color: Colors.grey[600]),
if (repeater.hasLocation) ...[ ),
const SizedBox(height: 4), if (repeater.hasLocation) ...[
Row( const SizedBox(height: 4),
mainAxisAlignment: MainAxisAlignment.center, Row(
children: [ mainAxisAlignment: MainAxisAlignment.center,
Icon(Icons.location_on, size: 14, color: Colors.grey[600]), children: [
const SizedBox(width: 4), Icon(
Text( Icons.location_on,
'${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}', size: 14,
style: TextStyle(fontSize: 12, color: Colors.grey[600]), color: Colors.grey[600],
),
const SizedBox(width: 4),
Text(
'${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
), ),
], ),
), ],
], ),
], ],
), ],
), ),
), ),
const SizedBox(height: 24), ),
Text( const SizedBox(height: 24),
l10n.repeater_managementTools, Text(
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), l10n.repeater_managementTools,
), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
const SizedBox(height: 16), ),
// Status button const SizedBox(height: 16),
_buildManagementCard( // Status button
context, _buildManagementCard(
icon: Icons.analytics, context,
title: l10n.repeater_status, icon: Icons.analytics,
subtitle: l10n.repeater_statusSubtitle, title: l10n.repeater_status,
color: Colors.blue, subtitle: l10n.repeater_statusSubtitle,
onTap: () { color: Colors.blue,
Navigator.push( onTap: () {
context, Navigator.push(
MaterialPageRoute( context,
builder: (context) => RepeaterStatusScreen( MaterialPageRoute(
repeater: repeater, builder: (context) => RepeaterStatusScreen(
password: password, repeater: repeater,
), password: password,
), ),
); ),
}, );
), },
const SizedBox(height: 16), ),
// Telemetry button const SizedBox(height: 16),
_buildManagementCard( // Telemetry button
context, _buildManagementCard(
icon: Icons.bar_chart_sharp, context,
title: l10n.repeater_telemetry, icon: Icons.bar_chart_sharp,
subtitle: l10n.repeater_telemetrySubtitle, title: l10n.repeater_telemetry,
color: Colors.teal, subtitle: l10n.repeater_telemetrySubtitle,
onTap: () { color: Colors.teal,
Navigator.push( onTap: () {
context, Navigator.push(
MaterialPageRoute( context,
builder: (context) => TelemetryScreen( MaterialPageRoute(
repeater: repeater, builder: (context) =>
password: password, TelemetryScreen(repeater: repeater, password: password),
), ),
);
},
),
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,
), ),
); ),
}, );
), },
const SizedBox(height: 12), ),
// CLI button const SizedBox(height: 12),
_buildManagementCard( // Neighbors button
context, _buildManagementCard(
icon: Icons.terminal, context,
title: l10n.repeater_cli, icon: Icons.group,
subtitle: l10n.repeater_cliSubtitle, title: l10n.repeater_neighbours,
color: Colors.green, subtitle: l10n.repeater_neighboursSubtitle,
onTap: () { color: Colors.orange,
Navigator.push( onTap: () {
context, Navigator.push(
MaterialPageRoute( context,
builder: (context) => RepeaterCliScreen( MaterialPageRoute(
repeater: repeater, builder: (context) => NeighboursScreen(
password: password, repeater: repeater,
), password: password,
), ),
); ),
}, );
), },
const SizedBox(height: 12), ),
// Settings button const SizedBox(height: 12),
_buildManagementCard( // Settings button
context, _buildManagementCard(
icon: Icons.group, context,
title: l10n.repeater_neighbours, icon: Icons.settings,
subtitle: l10n.repeater_neighboursSubtitle, title: l10n.repeater_settings,
color: Colors.orange, subtitle: l10n.repeater_settingsSubtitle,
onTap: () { color: Colors.deepOrange,
Navigator.push( onTap: () {
context, Navigator.push(
MaterialPageRoute( context,
builder: (context) => NeighboursScreen( MaterialPageRoute(
repeater: repeater, builder: (context) => RepeaterSettingsScreen(
password: password, repeater: repeater,
), password: password,
), ),
); ),
}, );
), },
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,
),
),
);
},
),
], ],
), ),
), ),
@@ -235,10 +255,7 @@ class RepeaterHubScreen extends StatelessWidget {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
subtitle, subtitle,
style: TextStyle( style: TextStyle(fontSize: 14, color: Colors.grey[600]),
fontSize: 14,
color: Colors.grey[600],
),
), ),
], ],
), ),
+227 -122
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:meshcore_open/widgets/elements_ui.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@@ -38,10 +39,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = context.l10n; final l10n = context.l10n;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: Text(l10n.settings_title), centerTitle: true),
title: Text(l10n.settings_title),
centerTitle: true,
),
body: SafeArea( body: SafeArea(
top: false, top: false,
child: Consumer<MeshCoreConnector>( child: Consumer<MeshCoreConnector>(
@@ -68,7 +66,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
); );
} }
Widget _buildDeviceInfoCard(BuildContext context, MeshCoreConnector connector) { Widget _buildDeviceInfoCard(
BuildContext context,
MeshCoreConnector connector,
) {
final l10n = context.l10n; final l10n = context.l10n;
return Card( return Card(
child: Padding( child: Padding(
@@ -83,21 +84,38 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
_buildInfoRow(l10n.settings_infoName, connector.deviceDisplayName), _buildInfoRow(l10n.settings_infoName, connector.deviceDisplayName),
_buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel), _buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel),
_buildInfoRow(l10n.settings_infoStatus, connector.isConnected ? l10n.common_connected : l10n.common_disconnected), _buildInfoRow(
l10n.settings_infoStatus,
connector.isConnected
? l10n.common_connected
: l10n.common_disconnected,
),
_buildBatteryInfoRow(context, connector), _buildBatteryInfoRow(context, connector),
if (connector.selfName != null) if (connector.selfName != null)
_buildInfoRow(l10n.settings_nodeName, connector.selfName!), _buildInfoRow(l10n.settings_nodeName, connector.selfName!),
if (connector.selfPublicKey != null) if (connector.selfPublicKey != null)
_buildInfoRow(l10n.settings_infoPublicKey, '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...'), _buildInfoRow(
_buildInfoRow(l10n.settings_infoContactsCount, '${connector.contacts.length}'), l10n.settings_infoPublicKey,
_buildInfoRow(l10n.settings_infoChannelCount, '${connector.channels.length}'), '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...',
),
_buildInfoRow(
l10n.settings_infoContactsCount,
'${connector.contacts.length}',
),
_buildInfoRow(
l10n.settings_infoChannelCount,
'${connector.channels.length}',
),
], ],
), ),
), ),
); );
} }
Widget _buildBatteryInfoRow(BuildContext context, MeshCoreConnector connector) { Widget _buildBatteryInfoRow(
BuildContext context,
MeshCoreConnector connector,
) {
final l10n = context.l10n; final l10n = context.l10n;
final percent = connector.batteryPercent; final percent = connector.batteryPercent;
final millivolts = connector.batteryMillivolts; final millivolts = connector.batteryMillivolts;
@@ -167,7 +185,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
); );
} }
Widget _buildNodeSettingsCard(BuildContext context, MeshCoreConnector connector) { Widget _buildNodeSettingsCard(
BuildContext context,
MeshCoreConnector connector,
) {
final l10n = context.l10n; final l10n = context.l10n;
return Card( return Card(
child: Column( child: Column(
@@ -298,7 +319,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => const BleDebugLogScreen()), MaterialPageRoute(
builder: (context) => const BleDebugLogScreen(),
),
); );
}, },
), ),
@@ -311,7 +334,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => const AppDebugLogScreen()), MaterialPageRoute(
builder: (context) => const AppDebugLogScreen(),
),
); );
}, },
), ),
@@ -334,20 +359,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: [ children: [
Row( Row(
children: [ children: [
if (leading != null) ...[ if (leading != null) ...[leading, const SizedBox(width: 8)],
leading,
const SizedBox(width: 8),
],
Text(label, style: TextStyle(color: Colors.grey[600])), Text(label, style: TextStyle(color: Colors.grey[600])),
], ],
), ),
Flexible( Flexible(
child: Text( child: Text(
value, value,
style: TextStyle( style: TextStyle(fontWeight: FontWeight.w500, color: valueColor),
fontWeight: FontWeight.w500,
color: valueColor,
),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
@@ -413,75 +432,154 @@ class _SettingsScreenState extends State<SettingsScreen> {
final l10n = context.l10n; final l10n = context.l10n;
final latController = TextEditingController(); final latController = TextEditingController();
final lonController = TextEditingController(); final lonController = TextEditingController();
final intervalController = TextEditingController();
latController.text = connector.selfLatitude?.toStringAsFixed(6) ?? '';
lonController.text = connector.selfLongitude?.toStringAsFixed(6) ?? '';
// Safe access to custom vars - may be null before device responds
final customVars = connector.currentCustomVars ?? {};
final bool hasGPS = customVars.containsKey("gps");
bool isGPSEnabled = customVars["gps"] == "1";
// Read current interval or default to 900 (15 minutes)
final currentInterval = int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
intervalController.text = currentInterval.toString();
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (dialogContext) => StatefulBuilder(
title: Text(l10n.settings_location), builder: (context, setDialogState) => AlertDialog(
content: Column( title: Text(l10n.settings_location),
mainAxisSize: MainAxisSize.min, content: Column(
children: [ mainAxisSize: MainAxisSize.min,
TextField( children: [
controller: latController, TextField(
decoration: InputDecoration( controller: latController,
labelText: l10n.settings_latitude, decoration: InputDecoration(
border: const OutlineInputBorder(), labelText: l10n.settings_latitude,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
), ),
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), const SizedBox(height: 16),
TextField(
controller: lonController,
decoration: InputDecoration(
labelText: l10n.settings_longitude,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
),
if (hasGPS) ...[
const SizedBox(height: 16),
TextField(
controller: intervalController,
decoration: InputDecoration(
labelText: l10n.settings_locationIntervalSec,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: false,
signed: false,
),
),
const SizedBox(height: 16),
FeatureToggleRow(
title: l10n.settings_locationGPSEnable,
subtitle: l10n.settings_locationGPSEnableSubtitle,
value: isGPSEnabled,
onChanged: (value) async {
setDialogState(() => isGPSEnabled = value);
if (value) {
await connector.setCustomVar("gps:1");
} else {
await connector.setCustomVar("gps:0");
}
},
),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
), ),
const SizedBox(height: 16), TextButton(
TextField( onPressed: () async {
controller: lonController, Navigator.pop(context);
decoration: InputDecoration(
labelText: l10n.settings_longitude, if (hasGPS) {
border: const OutlineInputBorder(), final intervalText = intervalController.text.trim();
), if (intervalText.isEmpty) {
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), return;
}
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),
),
);
return;
}
await connector.setCustomVar("gps_interval:$interval");
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)),
);
}
final latText = latController.text.trim();
final lonText = lonController.text.trim();
if (latText.isEmpty && lonText.isEmpty) {
return;
}
final currentLat = connector.selfLatitude;
final currentLon = connector.selfLongitude;
final lat = latText.isNotEmpty
? double.tryParse(latText)
: currentLat;
final lon = lonText.isNotEmpty
? double.tryParse(lonText)
: currentLon;
if (lat == null || lon == null) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(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)),
);
return;
}
await connector.setNodeLocation(lat: lat, lon: lon);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)),
);
},
child: Text(l10n.common_save),
), ),
], ],
), ),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
final latText = latController.text.trim();
final lonText = lonController.text.trim();
if (latText.isEmpty && lonText.isEmpty) {
return;
}
final currentLat = connector.selfLatitude;
final currentLon = connector.selfLongitude;
final lat = latText.isNotEmpty ? double.tryParse(latText) : currentLat;
final lon = lonText.isNotEmpty ? double.tryParse(lonText) : currentLon;
if (lat == null || lon == null) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(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)),
);
return;
}
await connector.setNodeLocation(lat: lat, lon: lon);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)),
);
},
child: Text(l10n.common_save),
),
],
), ),
); );
} }
@@ -530,17 +628,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
void _sendAdvert(BuildContext context, MeshCoreConnector connector) { void _sendAdvert(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n; final l10n = context.l10n;
connector.sendSelfAdvert(flood: true); connector.sendSelfAdvert(flood: true);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text(l10n.settings_advertisementSent)), context,
); ).showSnackBar(SnackBar(content: Text(l10n.settings_advertisementSent)));
} }
void _syncTime(BuildContext context, MeshCoreConnector connector) { void _syncTime(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n; final l10n = context.l10n;
connector.syncTime(); connector.syncTime();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text(l10n.settings_timeSynchronized)), context,
); ).showSnackBar(SnackBar(content: Text(l10n.settings_timeSynchronized)));
} }
void _confirmReboot(BuildContext context, MeshCoreConnector connector) { void _confirmReboot(BuildContext context, MeshCoreConnector connector) {
@@ -560,7 +658,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
Navigator.pop(context); Navigator.pop(context);
connector.rebootDevice(); connector.rebootDevice();
}, },
child: Text(l10n.common_reboot, style: const TextStyle(color: Colors.orange)), child: Text(
l10n.common_reboot,
style: const TextStyle(color: Colors.orange),
),
), ),
], ],
), ),
@@ -572,7 +673,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
showAboutDialog( showAboutDialog(
context: context, context: context,
applicationName: l10n.appTitle, applicationName: l10n.appTitle,
applicationVersion: _appVersion.isEmpty ? l10n.common_loading : _appVersion, applicationVersion: _appVersion.isEmpty
? l10n.common_loading
: _appVersion,
applicationLegalese: l10n.settings_aboutLegalese, applicationLegalese: l10n.settings_aboutLegalese,
children: [ children: [
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -604,7 +707,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
// Populate with current settings if available // Populate with current settings if available
if (widget.connector.currentFreqHz != null) { if (widget.connector.currentFreqHz != null) {
_frequencyController.text = (widget.connector.currentFreqHz! / 1000.0).toStringAsFixed(3); _frequencyController.text = (widget.connector.currentFreqHz! / 1000.0)
.toStringAsFixed(3);
} else { } else {
_frequencyController.text = '915.0'; _frequencyController.text = '915.0';
} }
@@ -670,26 +774,31 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
final txPower = int.tryParse(_txPowerController.text); final txPower = int.tryParse(_txPowerController.text);
if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) { if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text(l10n.settings_frequencyInvalid)), context,
); ).showSnackBar(SnackBar(content: Text(l10n.settings_frequencyInvalid)));
return; return;
} }
if (txPower == null || txPower < 0 || txPower > 22) { if (txPower == null || txPower < 0 || txPower > 22) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text(l10n.settings_txPowerInvalid)), context,
); ).showSnackBar(SnackBar(content: Text(l10n.settings_txPowerInvalid)));
return; return;
} }
final freqHz = (freqMHz * 1000).round(); final freqHz = (freqMHz * 1000).round();
final bwHz = _bandwidth.hz; final bwHz = _bandwidth.hz;
final sf = _spreadingFactor.value; final sf = _spreadingFactor.value;
final cr = _toDeviceCodingRate(_codingRate.value, widget.connector.currentCr); final cr = _toDeviceCodingRate(
_codingRate.value,
widget.connector.currentCr,
);
try { try {
await widget.connector.sendFrame(buildSetRadioParamsFrame(freqHz, bwHz, sf, cr)); await widget.connector.sendFrame(
buildSetRadioParamsFrame(freqHz, bwHz, sf, cr),
);
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower)); await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
await widget.connector.refreshDeviceInfo(); await widget.connector.refreshDeviceInfo();
@@ -727,7 +836,10 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(l10n.settings_presets, style: const TextStyle(fontWeight: FontWeight.bold)), Text(
l10n.settings_presets,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8), const SizedBox(height: 8),
Wrap( Wrap(
spacing: 8, spacing: 8,
@@ -762,7 +874,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
helperText: l10n.settings_frequencyHelper, helperText: l10n.settings_frequencyHelper,
), ),
keyboardType: const TextInputType.numberWithOptions(decimal: true), keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
DropdownButtonFormField<LoRaBandwidth>( DropdownButtonFormField<LoRaBandwidth>(
@@ -772,10 +886,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
items: LoRaBandwidth.values items: LoRaBandwidth.values
.map((bw) => DropdownMenuItem( .map(
value: bw, (bw) => DropdownMenuItem(value: bw, child: Text(bw.label)),
child: Text(bw.label), )
))
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
if (value != null) setState(() => _bandwidth = value); if (value != null) setState(() => _bandwidth = value);
@@ -789,10 +902,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
items: LoRaSpreadingFactor.values items: LoRaSpreadingFactor.values
.map((sf) => DropdownMenuItem( .map(
value: sf, (sf) => DropdownMenuItem(value: sf, child: Text(sf.label)),
child: Text(sf.label), )
))
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
if (value != null) setState(() => _spreadingFactor = value); if (value != null) setState(() => _spreadingFactor = value);
@@ -806,10 +918,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
items: LoRaCodingRate.values items: LoRaCodingRate.values
.map((cr) => DropdownMenuItem( .map(
value: cr, (cr) => DropdownMenuItem(value: cr, child: Text(cr.label)),
child: Text(cr.label), )
))
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
if (value != null) setState(() => _codingRate = value); if (value != null) setState(() => _codingRate = value);
@@ -833,10 +944,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel), child: Text(l10n.common_cancel),
), ),
FilledButton( FilledButton(onPressed: _saveSettings, child: Text(l10n.common_save)),
onPressed: _saveSettings,
child: Text(l10n.common_save),
),
], ],
); );
} }
@@ -850,9 +958,6 @@ class _PresetChip extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ActionChip( return ActionChip(label: Text(label), onPressed: onTap);
label: Text(label),
onPressed: onTap,
);
} }
} }
+17 -8
View File
@@ -16,7 +16,9 @@ class BleDebugLogEntry {
String get hexPreview { String get hexPreview {
const maxBytes = 64; const maxBytes = 64;
final bytes = payload.length > maxBytes ? payload.sublist(0, maxBytes) : payload; final bytes = payload.length > maxBytes
? payload.sublist(0, maxBytes)
: payload;
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' '); final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
return payload.length > maxBytes ? '$hex' : hex; return payload.length > maxBytes ? '$hex' : hex;
} }
@@ -26,14 +28,13 @@ class BleRawLogRxEntry {
final DateTime timestamp; final DateTime timestamp;
final Uint8List payload; final Uint8List payload;
BleRawLogRxEntry({ BleRawLogRxEntry({required this.timestamp, required this.payload});
required this.timestamp,
required this.payload,
});
String get hexPreview { String get hexPreview {
const maxBytes = 64; const maxBytes = 64;
final bytes = payload.length > maxBytes ? payload.sublist(0, maxBytes) : payload; final bytes = payload.length > maxBytes
? payload.sublist(0, maxBytes)
: payload;
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' '); final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
return payload.length > maxBytes ? '$hex' : hex; return payload.length > maxBytes ? '$hex' : hex;
} }
@@ -45,7 +46,8 @@ class BleDebugLogService extends ChangeNotifier {
final List<BleRawLogRxEntry> _rawLogRxEntries = []; final List<BleRawLogRxEntry> _rawLogRxEntries = [];
List<BleDebugLogEntry> get entries => List.unmodifiable(_entries); List<BleDebugLogEntry> get entries => List.unmodifiable(_entries);
List<BleRawLogRxEntry> get rawLogRxEntries => List.unmodifiable(_rawLogRxEntries); List<BleRawLogRxEntry> get rawLogRxEntries =>
List.unmodifiable(_rawLogRxEntries);
void logFrame(Uint8List frame, {required bool outgoing, String? note}) { void logFrame(Uint8List frame, {required bool outgoing, String? note}) {
if (frame.isEmpty) return; if (frame.isEmpty) return;
@@ -85,7 +87,12 @@ class BleDebugLogService extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
String _describeFrame(int code, Uint8List frame, bool outgoing, String? note) { String _describeFrame(
int code,
Uint8List frame,
bool outgoing,
String? note,
) {
final label = _codeLabel(code, outgoing: outgoing); final label = _codeLabel(code, outgoing: outgoing);
final prefix = outgoing ? 'TX' : 'RX'; final prefix = outgoing ? 'TX' : 'RX';
final extra = _frameDetail(code, frame); final extra = _frameDetail(code, frame);
@@ -147,6 +154,8 @@ class BleDebugLogService extends ChangeNotifier {
return 'CMD_SET_CHANNEL'; return 'CMD_SET_CHANNEL';
case cmdGetRadioSettings: case cmdGetRadioSettings:
return 'CMD_GET_RADIO_SETTINGS'; return 'CMD_GET_RADIO_SETTINGS';
case cmdSetCustomVar:
return 'CMD_SET_CUSTOM_VAR';
default: default:
return null; return null;
} }
+59
View File
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
class FeatureToggleRow extends StatefulWidget {
final String title;
final String subtitle;
final bool value;
final bool hasRefreshing;
final bool isRefreshing;
final ValueChanged<bool>? onChanged;
final VoidCallback? onRefresh;
final String? refreshTooltip;
const FeatureToggleRow({
super.key,
required this.title,
required this.subtitle,
required this.value,
this.hasRefreshing = false,
this.isRefreshing = false,
this.onChanged,
this.onRefresh,
this.refreshTooltip,
});
@override
State<FeatureToggleRow> createState() => _FeatureToggleRow();
}
class _FeatureToggleRow extends State<FeatureToggleRow> {
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: SwitchListTile(
title: Text(widget.title),
subtitle: Text(widget.subtitle),
value: widget.value,
onChanged: widget.onChanged,
contentPadding: EdgeInsets.zero,
),
),
if (widget.hasRefreshing)
IconButton(
icon: widget.isRefreshing
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh, size: 20),
onPressed: widget.isRefreshing ? null : widget.onRefresh,
tooltip: widget.refreshTooltip,
visualDensity: VisualDensity.compact,
),
],
);
}
}