mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
Merge main into dev-neighbours
This commit is contained in:
commit
31d633ee0b
34 changed files with 1442 additions and 568 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -20,7 +20,8 @@ class BufferReader {
|
|||
|
||||
Uint8List readRemainingBytes() => readBytes(remaining);
|
||||
|
||||
String readString() => utf8.decode(readRemainingBytes(), allowMalformed: true);
|
||||
String readString() =>
|
||||
utf8.decode(readRemainingBytes(), allowMalformed: true);
|
||||
|
||||
String readCString(int maxLength) {
|
||||
final value = <int>[];
|
||||
|
|
@ -38,13 +39,19 @@ class BufferReader {
|
|||
|
||||
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
|
||||
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
|
||||
int readUInt16LE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.little);
|
||||
int readUInt16BE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.big);
|
||||
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 readUInt16LE() =>
|
||||
readBytes(2).buffer.asByteData().getUint16(0, Endian.little);
|
||||
int readUInt16BE() =>
|
||||
readBytes(2).buffer.asByteData().getUint16(0, Endian.big);
|
||||
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 readInt32LE() => readBytes(4).buffer.asByteData().getInt32(0, Endian.little);
|
||||
int readInt32LE() =>
|
||||
readBytes(4).buffer.asByteData().getInt32(0, Endian.little);
|
||||
|
||||
int readInt24BE() {
|
||||
var value = (readByte() << 16) | (readByte() << 8) | readByte();
|
||||
|
|
@ -63,21 +70,25 @@ class BufferWriter {
|
|||
void writeBytes(Uint8List bytes) => _builder.add(bytes);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
final bytes = Uint8List(maxLength);
|
||||
|
|
@ -118,6 +129,8 @@ const int cmdGetChannel = 31;
|
|||
const int cmdSetChannel = 32;
|
||||
const int cmdGetRadioSettings = 57;
|
||||
const int cmdGetTelemetryReq = 39;
|
||||
const int cmdGetCustomVar = 40;
|
||||
const int cmdSetCustomVar = 41;
|
||||
const int cmdSendBinaryReq = 50;
|
||||
|
||||
// Text message types
|
||||
|
|
@ -152,6 +165,7 @@ const int respCodeContactMsgRecvV3 = 16;
|
|||
const int respCodeChannelMsgRecvV3 = 17;
|
||||
const int respCodeChannelInfo = 18;
|
||||
const int respCodeRadioSettings = 25;
|
||||
const int respCodeCustomVars = 21;
|
||||
|
||||
// Push codes (async from device)
|
||||
const int pushCodeAdvert = 0x80;
|
||||
|
|
@ -166,7 +180,6 @@ const int pushCodeNewAdvert = 0x8A;
|
|||
const int pushCodeTelemetryResponse = 0x8B;
|
||||
const int pushCodeBinaryResponse = 0x8C;
|
||||
|
||||
|
||||
// Contact/advertisement types
|
||||
const int advTypeChat = 1;
|
||||
const int advTypeRepeater = 2;
|
||||
|
|
@ -233,10 +246,7 @@ class ParsedContactText {
|
|||
final Uint8List senderPrefix;
|
||||
final String text;
|
||||
|
||||
const ParsedContactText({
|
||||
required this.senderPrefix,
|
||||
required this.text,
|
||||
});
|
||||
const ParsedContactText({required this.senderPrefix, required this.text});
|
||||
}
|
||||
|
||||
ParsedContactText? parseContactMessageText(Uint8List frame) {
|
||||
|
|
@ -265,10 +275,17 @@ ParsedContactText? parseContactMessageText(Uint8List frame) {
|
|||
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) {
|
||||
text =
|
||||
readCString(frame, baseTextOffset + 4, frame.length - (baseTextOffset + 4)).trim();
|
||||
text = readCString(
|
||||
frame,
|
||||
baseTextOffset + 4,
|
||||
frame.length - (baseTextOffset + 4),
|
||||
).trim();
|
||||
}
|
||||
if (text.isEmpty) return null;
|
||||
|
||||
|
|
@ -362,7 +379,8 @@ Uint8List buildSendTextMsgFrame(
|
|||
int attempt = 0,
|
||||
int? timestampSeconds,
|
||||
}) {
|
||||
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
||||
final timestamp =
|
||||
timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSendTxtMsg);
|
||||
writer.writeByte(txtTypePlain);
|
||||
|
|
@ -444,7 +462,9 @@ Uint8List buildSendSelfAdvertFrame({bool flood = false}) {
|
|||
// Format: [cmd][name...]
|
||||
Uint8List buildSetAdvertNameFrame(String 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();
|
||||
writer.writeByte(cmdSetAdvertName);
|
||||
writer.writeBytes(Uint8List.fromList(nameBytes.sublist(0, nameLen)));
|
||||
|
|
@ -461,6 +481,14 @@ Uint8List buildSetAdvertLatLonFrame(double lat, double lon) {
|
|||
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
|
||||
// Format: [cmd]["reboot"]
|
||||
Uint8List buildRebootFrame() {
|
||||
|
|
@ -544,7 +572,9 @@ Uint8List buildUpdateContactPathFrame(
|
|||
// Path data (64 bytes, zero-padded)
|
||||
final pathPadded = Uint8List(maxPathSize);
|
||||
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++) {
|
||||
pathPadded[i] = customPath[i];
|
||||
}
|
||||
|
|
@ -575,6 +605,11 @@ Uint8List buildGetRadioSettingsFrame() {
|
|||
return Uint8List.fromList([cmdGetRadioSettings]);
|
||||
}
|
||||
|
||||
//Build CMD_GET_CUSTOM_VARS frame
|
||||
Uint8List buildGetCustomVarsFrame() {
|
||||
return Uint8List.fromList([cmdGetCustomVar]);
|
||||
}
|
||||
|
||||
// Calculate LoRa airtime for a packet
|
||||
// Based on Semtech SX127x datasheet formula
|
||||
// Returns airtime in milliseconds
|
||||
|
|
@ -598,9 +633,11 @@ int calculateLoRaAirtime({
|
|||
final crc = 1; // CRC enabled
|
||||
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);
|
||||
var payloadSymbols = 8 + ((numerator / denominator).ceil()) * (codingRate + 4);
|
||||
var payloadSymbols =
|
||||
8 + ((numerator / denominator).ceil()) * (codingRate + 4);
|
||||
|
||||
if (payloadSymbols < 0) {
|
||||
payloadSymbols = 8;
|
||||
|
|
@ -647,7 +684,8 @@ Uint8List buildSendCliCommandFrame(
|
|||
int attempt = 0,
|
||||
int? timestampSeconds,
|
||||
}) {
|
||||
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
||||
final timestamp =
|
||||
timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSendTxtMsg);
|
||||
writer.writeByte(txtTypeCliData);
|
||||
|
|
@ -661,10 +699,7 @@ Uint8List buildSendCliCommandFrame(
|
|||
|
||||
// Build a telemetry request frame
|
||||
// Format: [cmd][pub_key x32][payload]
|
||||
Uint8List buildSendBinaryReq(
|
||||
Uint8List repeaterPubKey, {
|
||||
Uint8List? payload,
|
||||
}) {
|
||||
Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSendBinaryReq);
|
||||
writer.writeBytes(repeaterPubKey);
|
||||
|
|
@ -672,4 +707,4 @@ Uint8List buildSendBinaryReq(
|
|||
writer.writeBytes(payload);
|
||||
}
|
||||
return writer.toBytes();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1378,5 +1378,11 @@
|
|||
}
|
||||
},
|
||||
"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": "Управление на сървър за стая"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,9 +83,13 @@
|
|||
"settings_radioSettingsUpdated": "Radio settings updated",
|
||||
"settings_location": "Location",
|
||||
"settings_locationSubtitle": "GPS coordinates",
|
||||
"settings_locationUpdated": "Location updated",
|
||||
"settings_locationUpdated": "Location and GPS settings updated",
|
||||
"settings_locationBothRequired": "Enter both latitude and 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_longitude": "Longitude",
|
||||
"settings_privacyMode": "Privacy Mode",
|
||||
|
|
@ -253,7 +257,8 @@
|
|||
}
|
||||
},
|
||||
"contacts_manageRepeater": "Manage Repeater",
|
||||
"contacts_roomLogin": "Room Login",
|
||||
"contacts_manageRoom": "Manage Room Server",
|
||||
"contacts_roomLogin": "Room Server Login",
|
||||
"contacts_openChat": "Open Chat",
|
||||
"contacts_editGroup": "Edit Group",
|
||||
"contacts_deleteGroup": "Delete Group",
|
||||
|
|
@ -697,7 +702,7 @@
|
|||
"dialog_disconnectConfirm": "Are you sure you want to disconnect from this device?",
|
||||
|
||||
"login_repeaterLogin": "Repeater Login",
|
||||
"login_roomLogin": "Room Login",
|
||||
"login_roomLogin": "Room Server Login",
|
||||
"login_password": "Password",
|
||||
"login_enterPassword": "Enter password",
|
||||
"login_savePassword": "Save password",
|
||||
|
|
@ -760,6 +765,7 @@
|
|||
"path_setPath": "Set Path",
|
||||
|
||||
"repeater_management": "Repeater Management",
|
||||
"room_management": "Room Server Management",
|
||||
"repeater_managementTools": "Management Tools",
|
||||
"repeater_status": "Status",
|
||||
"repeater_statusSubtitle": "View repeater status, stats, and neighbors",
|
||||
|
|
|
|||
|
|
@ -1378,5 +1378,11 @@
|
|||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1378,5 +1378,11 @@
|
|||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1378,5 +1378,11 @@
|
|||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -465,7 +465,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @settings_locationUpdated.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Location updated'**
|
||||
/// **'Location and GPS settings updated'**
|
||||
String get settings_locationUpdated;
|
||||
|
||||
/// No description provided for @settings_locationBothRequired.
|
||||
|
|
@ -480,6 +480,30 @@ abstract class AppLocalizations {
|
|||
/// **'Invalid latitude or longitude.'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -1284,10 +1308,16 @@ abstract class AppLocalizations {
|
|||
/// **'Manage Repeater'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Room Login'**
|
||||
/// **'Room Server Login'**
|
||||
String get contacts_roomLogin;
|
||||
|
||||
/// No description provided for @contacts_openChat.
|
||||
|
|
@ -2672,7 +2702,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @login_roomLogin.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Room Login'**
|
||||
/// **'Room Server Login'**
|
||||
String get login_roomLogin;
|
||||
|
||||
/// No description provided for @login_password.
|
||||
|
|
@ -2867,6 +2897,12 @@ abstract class AppLocalizations {
|
|||
/// **'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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -201,6 +201,20 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_latitude => 'Широчина';
|
||||
|
||||
|
|
@ -650,6 +664,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||
@override
|
||||
String get contacts_manageRepeater => 'Управление на Повтарящ се Елемент';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => 'Управление на сървър за стая';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Вход в стаята';
|
||||
|
||||
|
|
@ -1587,6 +1604,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||
@override
|
||||
String get repeater_management => 'Управление на повторители';
|
||||
|
||||
@override
|
||||
String get room_management => 'Управление на сървъра за стая';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Инструменти за управление';
|
||||
|
||||
|
|
|
|||
|
|
@ -200,6 +200,20 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_latitude => 'Breitengrad';
|
||||
|
||||
|
|
@ -647,6 +661,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get contacts_manageRepeater => 'Wiederholungen verwalten';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => 'Manage Room Server';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Raum-Login';
|
||||
|
||||
|
|
@ -1586,6 +1603,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get repeater_management => 'Repeater-Verwaltung';
|
||||
|
||||
@override
|
||||
String get room_management => 'Room Server Management';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Verwaltungs-Tools';
|
||||
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get settings_locationSubtitle => 'GPS coordinates';
|
||||
|
||||
@override
|
||||
String get settings_locationUpdated => 'Location updated';
|
||||
String get settings_locationUpdated => 'Location and GPS settings updated';
|
||||
|
||||
@override
|
||||
String get settings_locationBothRequired =>
|
||||
|
|
@ -199,6 +199,20 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_latitude => 'Latitude';
|
||||
|
||||
|
|
@ -641,7 +655,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get contacts_manageRepeater => 'Manage Repeater';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Room Login';
|
||||
String get contacts_manageRoom => 'Manage Room Server';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Room Server Login';
|
||||
|
||||
@override
|
||||
String get contacts_openChat => 'Open Chat';
|
||||
|
|
@ -1439,7 +1456,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get login_repeaterLogin => 'Repeater Login';
|
||||
|
||||
@override
|
||||
String get login_roomLogin => 'Room Login';
|
||||
String get login_roomLogin => 'Room Server Login';
|
||||
|
||||
@override
|
||||
String get login_password => 'Password';
|
||||
|
|
@ -1561,6 +1578,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get repeater_management => 'Repeater Management';
|
||||
|
||||
@override
|
||||
String get room_management => 'Room Server Management';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Management Tools';
|
||||
|
||||
|
|
|
|||
|
|
@ -200,6 +200,20 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_latitude => 'Latitud';
|
||||
|
||||
|
|
@ -648,6 +662,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
@override
|
||||
String get contacts_manageRepeater => 'Gestionar Repetidor';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => 'Gestionar Servidor de Habitación';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Inicio de Sala';
|
||||
|
||||
|
|
@ -1585,6 +1602,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
@override
|
||||
String get repeater_management => 'Gestión de Repetidores';
|
||||
|
||||
@override
|
||||
String get room_management => 'Administración del Servidor de Habitación';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Herramientas de Gestión';
|
||||
|
||||
|
|
|
|||
|
|
@ -200,6 +200,20 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_latitude => 'Latitude';
|
||||
|
||||
|
|
@ -649,6 +663,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
@override
|
||||
String get contacts_manageRepeater => 'Gérer le répétiteur';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => 'Gestionar Servidor de Habitación';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Connexion Salle';
|
||||
|
||||
|
|
@ -1591,6 +1608,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
@override
|
||||
String get repeater_management => 'Gestion des répétiteurs';
|
||||
|
||||
@override
|
||||
String get room_management => 'Administración del Servidor de Habitación';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Outils de Gestion';
|
||||
|
||||
|
|
|
|||
|
|
@ -200,6 +200,20 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_latitude => 'Latitudine';
|
||||
|
||||
|
|
@ -646,6 +660,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
@override
|
||||
String get contacts_manageRepeater => 'Gestisci Ripetitore';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => 'Gestisci Server Camera';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Login Camera';
|
||||
|
||||
|
|
@ -1583,6 +1600,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
@override
|
||||
String get repeater_management => 'Gestione Ripetitori';
|
||||
|
||||
@override
|
||||
String get room_management => 'Gestione del Server di Camera';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Strumenti di Gestione';
|
||||
|
||||
|
|
|
|||
|
|
@ -200,6 +200,20 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
String get settings_locationInvalid =>
|
||||
'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
|
||||
String get settings_latitude => 'Breedtegraad';
|
||||
|
||||
|
|
@ -644,6 +658,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
@override
|
||||
String get contacts_manageRepeater => 'Beheer Repeater';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => 'Beheer Ruimte Server';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Ruimte Inloggen';
|
||||
|
||||
|
|
@ -1578,6 +1595,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
@override
|
||||
String get repeater_management => 'Beheer Repeaters';
|
||||
|
||||
@override
|
||||
String get room_management => 'Beheer Server Kamer';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Beheerinstrumenten';
|
||||
|
||||
|
|
|
|||
|
|
@ -202,6 +202,20 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
String get settings_locationInvalid =>
|
||||
'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
|
||||
String get settings_latitude => 'Szerokość';
|
||||
|
||||
|
|
@ -649,6 +663,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
@override
|
||||
String get contacts_manageRepeater => 'Zarządzaj Powtórzami';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => 'Zarządzaj Serwerem Pokoju';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Logowanie do pokoju';
|
||||
|
||||
|
|
@ -1587,6 +1604,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
@override
|
||||
String get repeater_management => 'Zarządzanie Powtórzami';
|
||||
|
||||
@override
|
||||
String get room_management => 'Zarządzanie Serwerem Pokoju';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Narzędzia Zarządzania';
|
||||
|
||||
|
|
|
|||
|
|
@ -201,6 +201,20 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_latitude => 'Latitude';
|
||||
|
||||
|
|
@ -649,6 +663,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
@override
|
||||
String get contacts_manageRepeater => 'Gerenciar Repetidor';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => 'Gerenciar Servidor de Sala';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Login no Quarto';
|
||||
|
||||
|
|
@ -1585,6 +1602,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
@override
|
||||
String get repeater_management => 'Gerenciamento de Repetidor';
|
||||
|
||||
@override
|
||||
String get room_management => 'Gerenciamento de Servidor de Sala';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Ferramentas de Gerenciamento';
|
||||
|
||||
|
|
|
|||
|
|
@ -200,6 +200,20 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_latitude => 'Súradnica';
|
||||
|
||||
|
|
@ -642,6 +656,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||
@override
|
||||
String get contacts_manageRepeater => 'Spravovať opakované zoznamy';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => 'Spravovať server miestnosti';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Prihlásenie do miestnosti';
|
||||
|
||||
|
|
@ -1100,7 +1117,7 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get chat_clearPathSubtitle =>
|
||||
'Znovu nájsť vynútene pri nasledujacej pošlite';
|
||||
'Znovu nájsť vynútene pri nasledujúcej pošlite';
|
||||
|
||||
@override
|
||||
String get chat_pathCleared =>
|
||||
|
|
@ -1580,6 +1597,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||
@override
|
||||
String get repeater_management => 'Správa opakérov';
|
||||
|
||||
@override
|
||||
String get room_management => 'Správa servera miestnosti';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Nástroje na správu';
|
||||
|
||||
|
|
|
|||
|
|
@ -200,6 +200,20 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
String get settings_locationInvalid =>
|
||||
'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
|
||||
String get settings_latitude => 'Širina';
|
||||
|
||||
|
|
@ -435,7 +449,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get appSettings_enableNotificationsSubtitle =>
|
||||
'Prejmujte obvestila o sporočilih in oglasih';
|
||||
'Prejmite obvestila o sporočilih in oglasih';
|
||||
|
||||
@override
|
||||
String get appSettings_notificationPermissionDenied =>
|
||||
|
|
@ -631,7 +645,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get contacts_noContactsFound =>
|
||||
'Niti ena osebe ali skupine ni najdena.';
|
||||
'Niti ena oseba ali skupine ni najdena.';
|
||||
|
||||
@override
|
||||
String get contacts_deleteContact => 'Izbrisati Kontakt';
|
||||
|
|
@ -644,6 +658,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
@override
|
||||
String get contacts_manageRepeater => 'Upravljajte Ponovitve';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => 'Upravljajte strežnik sobe';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Vnos v sobo';
|
||||
|
||||
|
|
@ -680,7 +697,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get contacts_noContactsMatchFilter =>
|
||||
'Niti ena osebe ne ustreza vašemu kriteriju.';
|
||||
'Niti ena oseba ne ustreza vašemu kriteriju.';
|
||||
|
||||
@override
|
||||
String get contacts_noMembers => 'Nič članov.';
|
||||
|
|
@ -1186,7 +1203,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
|
||||
@override
|
||||
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
|
||||
String map_nodesCount(int count) {
|
||||
|
|
@ -1580,6 +1597,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
@override
|
||||
String get repeater_management => 'Upravljanje ponovitve';
|
||||
|
||||
@override
|
||||
String get room_management => 'Upravljanje stremlišča';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Upravne orodje';
|
||||
|
||||
|
|
|
|||
|
|
@ -199,6 +199,20 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_latitude => 'Latitud';
|
||||
|
||||
|
|
@ -638,6 +652,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
String get contacts_manageRepeater => 'Hantera Upprepare';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => 'Hantera Rumserver';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => 'Rum Inloggning';
|
||||
|
||||
|
|
@ -1569,6 +1586,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
String get repeater_management => 'Återuppspelarens Hantering';
|
||||
|
||||
@override
|
||||
String get room_management => 'Rumserverhantering';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => 'Administrationsverktyg';
|
||||
|
||||
|
|
|
|||
|
|
@ -196,6 +196,18 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get settings_latitude => '纬度';
|
||||
|
||||
|
|
@ -611,6 +623,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
@override
|
||||
String get contacts_manageRepeater => '管理重复项';
|
||||
|
||||
@override
|
||||
String get contacts_manageRoom => '管理房间服务器';
|
||||
|
||||
@override
|
||||
String get contacts_roomLogin => '房间登录';
|
||||
|
||||
|
|
@ -1513,6 +1528,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
@override
|
||||
String get repeater_management => '重复器管理';
|
||||
|
||||
@override
|
||||
String get room_management => '房间服务器管理';
|
||||
|
||||
@override
|
||||
String get repeater_managementTools => '管理工具';
|
||||
|
||||
|
|
|
|||
|
|
@ -1378,5 +1378,11 @@
|
|||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1378,5 +1378,11 @@
|
|||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1378,5 +1378,11 @@
|
|||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -559,7 +559,7 @@
|
|||
"chat_setCustomPath": "Nastaviť vlastnú cestu",
|
||||
"chat_setCustomPathSubtitle": "Ručne zadajte trasu.",
|
||||
"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_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.",
|
||||
|
|
@ -1378,5 +1378,11 @@
|
|||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@
|
|||
"appSettings_languageBg": "Български",
|
||||
"appSettings_notifications": "Obveščanja",
|
||||
"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_notificationsEnabled": "Obvestila omogočena",
|
||||
"appSettings_notificationsDisabled": "Obvestila so izklopljena",
|
||||
|
|
@ -256,7 +256,7 @@
|
|||
"contacts_contactsWillAppear": "Kontakti se bodo prikazali, ko naprave oglasijo.",
|
||||
"contacts_searchContacts": "Iskanje 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_removeConfirm": "Izbrisati {contactName} iz kontaktov?",
|
||||
"@contacts_removeConfirm": {
|
||||
|
|
@ -291,7 +291,7 @@
|
|||
}
|
||||
},
|
||||
"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_lastSeenNow": "Datum zadnjega vpisa zdaj",
|
||||
"contacts_lastSeenMinsAgo": "Zadnjič videti {minutes} minut nazaj",
|
||||
|
|
@ -606,7 +606,7 @@
|
|||
},
|
||||
"map_title": "Mapa omrežja",
|
||||
"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": {
|
||||
"placeholders": {
|
||||
|
|
@ -1378,5 +1378,11 @@
|
|||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1378,5 +1378,11 @@
|
|||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1378,5 +1378,11 @@
|
|||
}
|
||||
},
|
||||
"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": "房间服务器管理"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,13 +27,15 @@ import 'map_screen.dart';
|
|||
import 'repeater_hub_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
|
||||
enum RoomLoginDestination {
|
||||
chat,
|
||||
management,
|
||||
}
|
||||
|
||||
class ContactsScreen extends StatefulWidget {
|
||||
final bool hideBackButton;
|
||||
|
||||
const ContactsScreen({
|
||||
super.key,
|
||||
this.hideBackButton = false,
|
||||
});
|
||||
const ContactsScreen({super.key, this.hideBackButton = false});
|
||||
|
||||
@override
|
||||
State<ContactsScreen> createState() => _ContactsScreenState();
|
||||
|
|
@ -114,7 +116,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
top: false,
|
||||
child: QuickSwitchBar(
|
||||
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 filteredGroups =
|
||||
_showUnreadOnly ? const <ContactGroup>[] : _filterAndSortGroups(_groups, contacts);
|
||||
final filteredGroups = _showUnreadOnly
|
||||
? const <ContactGroup>[]
|
||||
: _filterAndSortGroups(_groups, contacts);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
|
|
@ -199,7 +203,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
_searchDebounce?.cancel();
|
||||
|
|
@ -238,14 +245,18 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
final group = filteredGroups[index];
|
||||
return _buildGroupTile(context, group, contacts);
|
||||
}
|
||||
final contact = filteredAndSorted[index - filteredGroups.length];
|
||||
final unreadCount = connector.getUnreadCountForContact(contact);
|
||||
final contact =
|
||||
filteredAndSorted[index - filteredGroups.length];
|
||||
final unreadCount = connector.getUnreadCountForContact(
|
||||
contact,
|
||||
);
|
||||
return _ContactTile(
|
||||
contact: contact,
|
||||
lastSeen: _resolveLastSeen(contact),
|
||||
unreadCount: unreadCount,
|
||||
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 contactsByKey = <String, Contact>{};
|
||||
for (final contact in contacts) {
|
||||
contactsByKey[contact.publicKeyHex] = contact;
|
||||
}
|
||||
|
||||
final filtered = groups.where((group) {
|
||||
if (query.isEmpty) return true;
|
||||
if (group.name.toLowerCase().contains(query)) return true;
|
||||
for (final key in group.memberKeys) {
|
||||
final contact = contactsByKey[key];
|
||||
if (contact != null && matchesContactQuery(contact, query)) return true;
|
||||
}
|
||||
return false;
|
||||
}).where((group) {
|
||||
if (_typeFilter == ContactTypeFilter.all) return true;
|
||||
for (final key in group.memberKeys) {
|
||||
final contact = contactsByKey[key];
|
||||
if (contact != null && _matchesTypeFilter(contact)) return true;
|
||||
}
|
||||
return false;
|
||||
}).toList();
|
||||
final filtered = groups
|
||||
.where((group) {
|
||||
if (query.isEmpty) return true;
|
||||
if (group.name.toLowerCase().contains(query)) return true;
|
||||
for (final key in group.memberKeys) {
|
||||
final contact = contactsByKey[key];
|
||||
if (contact != null && matchesContactQuery(contact, query)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.where((group) {
|
||||
if (_typeFilter == ContactTypeFilter.all) return true;
|
||||
for (final key in group.memberKeys) {
|
||||
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;
|
||||
}
|
||||
|
||||
List<Contact> _filterAndSortContacts(List<Contact> contacts, MeshCoreConnector connector) {
|
||||
List<Contact> _filterAndSortContacts(
|
||||
List<Contact> contacts,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
var filtered = contacts.where((contact) {
|
||||
if (_searchQuery.isEmpty) return true;
|
||||
return matchesContactQuery(contact, _searchQuery);
|
||||
|
|
@ -301,19 +325,27 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
|
||||
switch (_sortOption) {
|
||||
case ContactSortOption.lastSeen:
|
||||
filtered.sort((a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)));
|
||||
filtered.sort(
|
||||
(a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)),
|
||||
);
|
||||
break;
|
||||
case ContactSortOption.recentMessages:
|
||||
filtered.sort((a, b) {
|
||||
final aMessages = connector.getMessages(a);
|
||||
final bMessages = connector.getMessages(b);
|
||||
final aLastMsg = aMessages.isEmpty ? DateTime(1970) : aMessages.last.timestamp;
|
||||
final bLastMsg = bMessages.isEmpty ? DateTime(1970) : bMessages.last.timestamp;
|
||||
final aLastMsg = aMessages.isEmpty
|
||||
? DateTime(1970)
|
||||
: aMessages.last.timestamp;
|
||||
final bLastMsg = bMessages.isEmpty
|
||||
? DateTime(1970)
|
||||
: bMessages.last.timestamp;
|
||||
return bLastMsg.compareTo(aLastMsg);
|
||||
});
|
||||
break;
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -340,7 +372,11 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
: 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 subtitle = _formatGroupMembers(context, memberContacts);
|
||||
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>{};
|
||||
for (final contact in contacts) {
|
||||
byKey[contact.publicKeyHex] = contact;
|
||||
|
|
@ -371,7 +410,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -387,7 +428,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
if (contact.type == advTypeRepeater) {
|
||||
_showRepeaterLogin(context, contact);
|
||||
} else if (contact.type == advTypeRoom) {
|
||||
_showRoomLogin(context, contact);
|
||||
_showRoomLogin(context, contact, RoomLoginDestination.chat);
|
||||
} else {
|
||||
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
|
||||
Navigator.push(
|
||||
|
|
@ -403,17 +444,13 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
case 1:
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
buildQuickSwitchRoute(
|
||||
const ChannelsScreen(hideBackButton: true),
|
||||
),
|
||||
buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
buildQuickSwitchRoute(
|
||||
const MapScreen(hideBackButton: true),
|
||||
),
|
||||
buildQuickSwitchRoute(const MapScreen(hideBackButton: true)),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
|
@ -429,10 +466,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RepeaterHubScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
),
|
||||
builder: (context) =>
|
||||
RepeaterHubScreen(repeater: repeater, 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(
|
||||
context: context,
|
||||
builder: (context) => RoomLoginDialog(
|
||||
room: room,
|
||||
onLogin: (password) {
|
||||
// Navigate to chat screen after successful login
|
||||
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
|
||||
Navigator.push(
|
||||
context,
|
||||
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);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
|
|
@ -478,7 +522,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
),
|
||||
ListTile(
|
||||
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: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_confirmDeleteGroup(context, group);
|
||||
|
|
@ -522,7 +569,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
});
|
||||
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
|
||||
? sortedContacts
|
||||
: sortedContacts
|
||||
.where((contact) => matchesContactQuery(contact, filterQuery))
|
||||
.toList();
|
||||
.where(
|
||||
(contact) => matchesContactQuery(contact, filterQuery),
|
||||
)
|
||||
.toList();
|
||||
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(
|
||||
width: double.maxFinite,
|
||||
child: Column(
|
||||
|
|
@ -582,12 +638,18 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
SizedBox(
|
||||
height: 240,
|
||||
child: filteredContacts.isEmpty
|
||||
? Center(child: Text(context.l10n.contacts_noContactsMatchFilter))
|
||||
? Center(
|
||||
child: Text(
|
||||
context.l10n.contacts_noContactsMatchFilter,
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: filteredContacts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final contact = filteredContacts[index];
|
||||
final isSelected = selectedKeys.contains(contact.publicKeyHex);
|
||||
final isSelected = selectedKeys.contains(
|
||||
contact.publicKeyHex,
|
||||
);
|
||||
return CheckboxListTile(
|
||||
value: isSelected,
|
||||
title: Text(contact.name),
|
||||
|
|
@ -618,7 +680,9 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
final name = nameController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_groupNameRequired)),
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_groupNameRequired),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -628,13 +692,19 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
});
|
||||
if (exists) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_groupAlreadyExists(name))),
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.contacts_groupAlreadyExists(name),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
if (isEditing) {
|
||||
final index = _groups.indexWhere((g) => g.name == group.name);
|
||||
final index = _groups.indexWhere(
|
||||
(g) => g.name == group.name,
|
||||
);
|
||||
if (index != -1) {
|
||||
_groups[index] = ContactGroup(
|
||||
name: name,
|
||||
|
|
@ -642,7 +712,12 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
);
|
||||
}
|
||||
} else {
|
||||
_groups.add(ContactGroup(name: name, memberKeys: selectedKeys.toList()));
|
||||
_groups.add(
|
||||
ContactGroup(
|
||||
name: name,
|
||||
memberKeys: selectedKeys.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
await _saveGroups();
|
||||
|
|
@ -650,7 +725,11 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
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);
|
||||
},
|
||||
)
|
||||
else if (isRoom)
|
||||
else if (isRoom) ...[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.room, color: Colors.blue),
|
||||
title: Text(context.l10n.contacts_roomLogin),
|
||||
onTap: () {
|
||||
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(
|
||||
leading: const Icon(Icons.chat),
|
||||
title: Text(context.l10n.contacts_openChat),
|
||||
|
|
@ -702,7 +789,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
),
|
||||
ListTile(
|
||||
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: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_confirmDelete(context, connector, contact);
|
||||
|
|
@ -734,7 +824,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
|||
Navigator.pop(dialogContext);
|
||||
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
|
||||
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(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: _getTypeColor(contact.type),
|
||||
child: _buildContactAvatar(contact),
|
||||
),
|
||||
title: Text(contact.name),
|
||||
subtitle: Text('${contact.typeLabel} • ${contact.pathLabel} $shotPublicKey'),
|
||||
subtitle: Text(
|
||||
'${contact.typeLabel} • ${contact.pathLabel} $shotPublicKey',
|
||||
),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
|
|
@ -791,10 +887,7 @@ class _ContactTile extends StatelessWidget {
|
|||
Widget _buildContactAvatar(Contact contact) {
|
||||
final emoji = firstEmoji(contact.name);
|
||||
if (emoji != null) {
|
||||
return Text(
|
||||
emoji,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
);
|
||||
return Text(emoji, style: const TextStyle(fontSize: 18));
|
||||
}
|
||||
return Icon(_getTypeIcon(contact.type), color: Colors.white, size: 20);
|
||||
}
|
||||
|
|
@ -833,13 +926,21 @@ class _ContactTile extends StatelessWidget {
|
|||
final now = DateTime.now();
|
||||
final diff = now.difference(lastSeen);
|
||||
|
||||
if (diff.isNegative || diff.inMinutes < 5) return context.l10n.contacts_lastSeenNow;
|
||||
if (diff.inMinutes < 60) return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
|
||||
if (diff.isNegative || diff.inMinutes < 5) {
|
||||
return context.l10n.contacts_lastSeenNow;
|
||||
}
|
||||
if (diff.inMinutes < 60) {
|
||||
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
|
||||
}
|
||||
if (diff.inHours < 24) {
|
||||
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;
|
||||
return days == 1 ? context.l10n.contacts_lastSeenDayAgo : context.l10n.contacts_lastSeenDaysAgo(days);
|
||||
return days == 1
|
||||
? context.l10n.contacts_lastSeenDayAgo
|
||||
: context.l10n.contacts_lastSeenDaysAgo(days);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -273,6 +273,9 @@ class _MapScreenState extends State<MapScreen> {
|
|||
initialZoom: initialZoom,
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate
|
||||
),
|
||||
onTap: (_, latLng) {
|
||||
if (_isSelectingPoi) {
|
||||
setState(() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
import 'repeater_status_screen.dart';
|
||||
|
|
@ -26,10 +27,17 @@ class RepeaterHubScreen extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(l10n.repeater_management),
|
||||
Text(
|
||||
repeater.type == advTypeRepeater
|
||||
? l10n.repeater_management
|
||||
: l10n.room_management,
|
||||
),
|
||||
Text(
|
||||
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(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// Repeater info card
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 40,
|
||||
backgroundColor: Colors.orange,
|
||||
child: const Icon(Icons.cell_tower, size: 40, color: Colors.white),
|
||||
// Repeater info card
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 40,
|
||||
backgroundColor: Colors.orange,
|
||||
child: const Icon(
|
||||
Icons.cell_tower,
|
||||
size: 40,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
repeater.name,
|
||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
repeater.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'<${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(
|
||||
repeater.pathLabel,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
),
|
||||
if (repeater.hasLocation) ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.location_on, size: 14, 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: 8),
|
||||
Text(
|
||||
'<${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(
|
||||
repeater.pathLabel,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
),
|
||||
if (repeater.hasLocation) ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
size: 14,
|
||||
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(
|
||||
l10n.repeater_managementTools,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Status button
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.analytics,
|
||||
title: l10n.repeater_status,
|
||||
subtitle: l10n.repeater_statusSubtitle,
|
||||
color: Colors.blue,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RepeaterStatusScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
l10n.repeater_managementTools,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Status button
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.analytics,
|
||||
title: l10n.repeater_status,
|
||||
subtitle: l10n.repeater_statusSubtitle,
|
||||
color: Colors.blue,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RepeaterStatusScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Telemetry button
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.bar_chart_sharp,
|
||||
title: l10n.repeater_telemetry,
|
||||
subtitle: l10n.repeater_telemetrySubtitle,
|
||||
color: Colors.teal,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TelemetryScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Telemetry button
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.bar_chart_sharp,
|
||||
title: l10n.repeater_telemetry,
|
||||
subtitle: l10n.repeater_telemetrySubtitle,
|
||||
color: Colors.teal,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
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
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.terminal,
|
||||
title: l10n.repeater_cli,
|
||||
subtitle: l10n.repeater_cliSubtitle,
|
||||
color: Colors.green,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RepeaterCliScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Neighbors button
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.group,
|
||||
title: l10n.repeater_neighbours,
|
||||
subtitle: l10n.repeater_neighboursSubtitle,
|
||||
color: Colors.orange,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NeighboursScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Settings button
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.group,
|
||||
title: l10n.repeater_neighbours,
|
||||
subtitle: l10n.repeater_neighboursSubtitle,
|
||||
color: Colors.orange,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NeighboursScreen(
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
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),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/widgets/elements_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
|
|
@ -38,10 +39,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.settings_title),
|
||||
centerTitle: true,
|
||||
),
|
||||
appBar: AppBar(title: Text(l10n.settings_title), centerTitle: true),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
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;
|
||||
return Card(
|
||||
child: Padding(
|
||||
|
|
@ -83,21 +84,38 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
const SizedBox(height: 16),
|
||||
_buildInfoRow(l10n.settings_infoName, connector.deviceDisplayName),
|
||||
_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),
|
||||
if (connector.selfName != null)
|
||||
_buildInfoRow(l10n.settings_nodeName, connector.selfName!),
|
||||
if (connector.selfPublicKey != null)
|
||||
_buildInfoRow(l10n.settings_infoPublicKey, '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...'),
|
||||
_buildInfoRow(l10n.settings_infoContactsCount, '${connector.contacts.length}'),
|
||||
_buildInfoRow(l10n.settings_infoChannelCount, '${connector.channels.length}'),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoPublicKey,
|
||||
'${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 percent = connector.batteryPercent;
|
||||
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;
|
||||
return Card(
|
||||
child: Column(
|
||||
|
|
@ -298,7 +319,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const BleDebugLogScreen()),
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const BleDebugLogScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
@ -311,7 +334,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const AppDebugLogScreen()),
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AppDebugLogScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
@ -334,20 +359,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (leading != null) ...[
|
||||
leading,
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
if (leading != null) ...[leading, const SizedBox(width: 8)],
|
||||
Text(label, style: TextStyle(color: Colors.grey[600])),
|
||||
],
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: valueColor,
|
||||
),
|
||||
style: TextStyle(fontWeight: FontWeight.w500, color: valueColor),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
|
@ -413,75 +432,154 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
final l10n = context.l10n;
|
||||
final latController = 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(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(l10n.settings_location),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: latController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_latitude,
|
||||
border: const OutlineInputBorder(),
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: Text(l10n.settings_location),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: latController,
|
||||
decoration: InputDecoration(
|
||||
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),
|
||||
TextField(
|
||||
controller: lonController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_longitude,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
|
||||
if (hasGPS) {
|
||||
final intervalText = intervalController.text.trim();
|
||||
if (intervalText.isEmpty) {
|
||||
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) {
|
||||
final l10n = context.l10n;
|
||||
connector.sendSelfAdvert(flood: true);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_advertisementSent)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_advertisementSent)));
|
||||
}
|
||||
|
||||
void _syncTime(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
connector.syncTime();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_timeSynchronized)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_timeSynchronized)));
|
||||
}
|
||||
|
||||
void _confirmReboot(BuildContext context, MeshCoreConnector connector) {
|
||||
|
|
@ -560,7 +658,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
Navigator.pop(context);
|
||||
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(
|
||||
context: context,
|
||||
applicationName: l10n.appTitle,
|
||||
applicationVersion: _appVersion.isEmpty ? l10n.common_loading : _appVersion,
|
||||
applicationVersion: _appVersion.isEmpty
|
||||
? l10n.common_loading
|
||||
: _appVersion,
|
||||
applicationLegalese: l10n.settings_aboutLegalese,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -604,7 +707,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||
|
||||
// Populate with current settings if available
|
||||
if (widget.connector.currentFreqHz != null) {
|
||||
_frequencyController.text = (widget.connector.currentFreqHz! / 1000.0).toStringAsFixed(3);
|
||||
_frequencyController.text = (widget.connector.currentFreqHz! / 1000.0)
|
||||
.toStringAsFixed(3);
|
||||
} else {
|
||||
_frequencyController.text = '915.0';
|
||||
}
|
||||
|
|
@ -670,26 +774,31 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||
final txPower = int.tryParse(_txPowerController.text);
|
||||
|
||||
if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_frequencyInvalid)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_frequencyInvalid)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (txPower == null || txPower < 0 || txPower > 22) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_txPowerInvalid)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_txPowerInvalid)));
|
||||
return;
|
||||
}
|
||||
|
||||
final freqHz = (freqMHz * 1000).round();
|
||||
final bwHz = _bandwidth.hz;
|
||||
final sf = _spreadingFactor.value;
|
||||
final cr = _toDeviceCodingRate(_codingRate.value, widget.connector.currentCr);
|
||||
final cr = _toDeviceCodingRate(
|
||||
_codingRate.value,
|
||||
widget.connector.currentCr,
|
||||
);
|
||||
|
||||
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.refreshDeviceInfo();
|
||||
|
||||
|
|
@ -727,7 +836,10 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
|
|
@ -762,7 +874,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||
border: const OutlineInputBorder(),
|
||||
helperText: l10n.settings_frequencyHelper,
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<LoRaBandwidth>(
|
||||
|
|
@ -772,10 +886,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: LoRaBandwidth.values
|
||||
.map((bw) => DropdownMenuItem(
|
||||
value: bw,
|
||||
child: Text(bw.label),
|
||||
))
|
||||
.map(
|
||||
(bw) => DropdownMenuItem(value: bw, child: Text(bw.label)),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) setState(() => _bandwidth = value);
|
||||
|
|
@ -789,10 +902,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: LoRaSpreadingFactor.values
|
||||
.map((sf) => DropdownMenuItem(
|
||||
value: sf,
|
||||
child: Text(sf.label),
|
||||
))
|
||||
.map(
|
||||
(sf) => DropdownMenuItem(value: sf, child: Text(sf.label)),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) setState(() => _spreadingFactor = value);
|
||||
|
|
@ -806,10 +918,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: LoRaCodingRate.values
|
||||
.map((cr) => DropdownMenuItem(
|
||||
value: cr,
|
||||
child: Text(cr.label),
|
||||
))
|
||||
.map(
|
||||
(cr) => DropdownMenuItem(value: cr, child: Text(cr.label)),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) setState(() => _codingRate = value);
|
||||
|
|
@ -833,10 +944,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
|||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: _saveSettings,
|
||||
child: Text(l10n.common_save),
|
||||
),
|
||||
FilledButton(onPressed: _saveSettings, child: Text(l10n.common_save)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
@ -850,9 +958,6 @@ class _PresetChip extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ActionChip(
|
||||
label: Text(label),
|
||||
onPressed: onTap,
|
||||
);
|
||||
return ActionChip(label: Text(label), onPressed: onTap);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ class BleDebugLogEntry {
|
|||
|
||||
String get hexPreview {
|
||||
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(' ');
|
||||
return payload.length > maxBytes ? '$hex …' : hex;
|
||||
}
|
||||
|
|
@ -26,14 +28,13 @@ class BleRawLogRxEntry {
|
|||
final DateTime timestamp;
|
||||
final Uint8List payload;
|
||||
|
||||
BleRawLogRxEntry({
|
||||
required this.timestamp,
|
||||
required this.payload,
|
||||
});
|
||||
BleRawLogRxEntry({required this.timestamp, required this.payload});
|
||||
|
||||
String get hexPreview {
|
||||
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(' ');
|
||||
return payload.length > maxBytes ? '$hex …' : hex;
|
||||
}
|
||||
|
|
@ -45,7 +46,8 @@ class BleDebugLogService extends ChangeNotifier {
|
|||
final List<BleRawLogRxEntry> _rawLogRxEntries = [];
|
||||
|
||||
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}) {
|
||||
if (frame.isEmpty) return;
|
||||
|
|
@ -85,7 +87,12 @@ class BleDebugLogService extends ChangeNotifier {
|
|||
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 prefix = outgoing ? 'TX' : 'RX';
|
||||
final extra = _frameDetail(code, frame);
|
||||
|
|
@ -147,6 +154,8 @@ class BleDebugLogService extends ChangeNotifier {
|
|||
return 'CMD_SET_CHANNEL';
|
||||
case cmdGetRadioSettings:
|
||||
return 'CMD_GET_RADIO_SETTINGS';
|
||||
case cmdSetCustomVar:
|
||||
return 'CMD_SET_CUSTOM_VAR';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
59
lib/widgets/elements_ui.dart
Normal file
59
lib/widgets/elements_ui.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue