mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Add channel management features and localization support
- Implemented functionality to add new channels with a dialog interface. - Added validation for channel name and secret inputs, ensuring they meet specified criteria. - Integrated channel creation logic into the connection provider, including automatic slot assignment and MD5 hashing for channel secrets. - Updated localization files for multiple languages (Spanish, French, Croatian, Italian, German, Slovenian, and English) to include new channel-related strings. - Enhanced the contacts tab to display the add channel button conditionally based on device connection status. - Refactored the contacts tab to improve readability and maintainability.
This commit is contained in:
@@ -2607,5 +2607,21 @@
|
|||||||
"showForestRoads": "Waldwege anzeigen",
|
"showForestRoads": "Waldwege anzeigen",
|
||||||
"wmsOverlays": "WMS Überlagerungen",
|
"wmsOverlays": "WMS Überlagerungen",
|
||||||
|
|
||||||
"recentMessages": "Aktuelle Nachrichten"
|
"recentMessages": "Aktuelle Nachrichten",
|
||||||
|
|
||||||
|
"addChannel": "Kanal hinzufügen",
|
||||||
|
"channelName": "Kanalname",
|
||||||
|
"channelNameHint": "z.B. Rettungsteam Alpha",
|
||||||
|
"channelSecret": "Kanal-Passwort",
|
||||||
|
"channelSecretHint": "Gemeinsames Passwort für diesen Kanal",
|
||||||
|
"channelSecretHelp": "Dieses Passwort muss mit allen Teammitgliedern geteilt werden, die Zugriff auf diesen Kanal benötigen",
|
||||||
|
"channelNameRequired": "Kanalname ist erforderlich",
|
||||||
|
"channelNameTooLong": "Kanalname darf maximal 31 Zeichen lang sein",
|
||||||
|
"channelSecretRequired": "Kanal-Passwort ist erforderlich",
|
||||||
|
"channelSecretTooLong": "Kanal-Passwort darf maximal 32 Zeichen lang sein",
|
||||||
|
"invalidAsciiCharacters": "Nur ASCII-Zeichen sind erlaubt",
|
||||||
|
"channelCreatedSuccessfully": "Kanal erfolgreich erstellt",
|
||||||
|
"channelCreationFailed": "Kanal konnte nicht erstellt werden: {error}",
|
||||||
|
"allChannelSlotsInUse": "Alle Kanalplätze sind belegt (maximal 39 benutzerdefinierte Kanäle)",
|
||||||
|
"createChannel": "Kanal erstellen"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3241,5 +3241,85 @@
|
|||||||
"recentMessages": "Recent Messages",
|
"recentMessages": "Recent Messages",
|
||||||
"@recentMessages": {
|
"@recentMessages": {
|
||||||
"description": "Header for recent messages overlay on map in fullscreen mode"
|
"description": "Header for recent messages overlay on map in fullscreen mode"
|
||||||
|
},
|
||||||
|
|
||||||
|
"addChannel": "Add Channel",
|
||||||
|
"@addChannel": {
|
||||||
|
"description": "Button to add a new channel"
|
||||||
|
},
|
||||||
|
|
||||||
|
"channelName": "Channel Name",
|
||||||
|
"@channelName": {
|
||||||
|
"description": "Label for channel name field"
|
||||||
|
},
|
||||||
|
|
||||||
|
"channelNameHint": "e.g., Rescue Team Alpha",
|
||||||
|
"@channelNameHint": {
|
||||||
|
"description": "Hint for channel name field"
|
||||||
|
},
|
||||||
|
|
||||||
|
"channelSecret": "Channel Secret",
|
||||||
|
"@channelSecret": {
|
||||||
|
"description": "Label for channel secret field"
|
||||||
|
},
|
||||||
|
|
||||||
|
"channelSecretHint": "Shared password for this channel",
|
||||||
|
"@channelSecretHint": {
|
||||||
|
"description": "Hint for channel secret field"
|
||||||
|
},
|
||||||
|
|
||||||
|
"channelSecretHelp": "This secret must be shared with all team members who need access to this channel",
|
||||||
|
"@channelSecretHelp": {
|
||||||
|
"description": "Help text explaining channel secret"
|
||||||
|
},
|
||||||
|
|
||||||
|
"channelNameRequired": "Channel name is required",
|
||||||
|
"@channelNameRequired": {
|
||||||
|
"description": "Validation error for empty channel name"
|
||||||
|
},
|
||||||
|
|
||||||
|
"channelNameTooLong": "Channel name must be 31 characters or less",
|
||||||
|
"@channelNameTooLong": {
|
||||||
|
"description": "Validation error for channel name too long"
|
||||||
|
},
|
||||||
|
|
||||||
|
"channelSecretRequired": "Channel secret is required",
|
||||||
|
"@channelSecretRequired": {
|
||||||
|
"description": "Validation error for empty channel secret"
|
||||||
|
},
|
||||||
|
|
||||||
|
"channelSecretTooLong": "Channel secret must be 32 characters or less",
|
||||||
|
"@channelSecretTooLong": {
|
||||||
|
"description": "Validation error for channel secret too long"
|
||||||
|
},
|
||||||
|
|
||||||
|
"invalidAsciiCharacters": "Only ASCII characters are allowed",
|
||||||
|
"@invalidAsciiCharacters": {
|
||||||
|
"description": "Validation error for non-ASCII characters"
|
||||||
|
},
|
||||||
|
|
||||||
|
"channelCreatedSuccessfully": "Channel created successfully",
|
||||||
|
"@channelCreatedSuccessfully": {
|
||||||
|
"description": "Success message after creating channel"
|
||||||
|
},
|
||||||
|
|
||||||
|
"channelCreationFailed": "Failed to create channel: {error}",
|
||||||
|
"@channelCreationFailed": {
|
||||||
|
"description": "Error message when channel creation fails",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"allChannelSlotsInUse": "All channel slots are in use (maximum 39 custom channels)",
|
||||||
|
"@allChannelSlotsInUse": {
|
||||||
|
"description": "Error when no channel slots available"
|
||||||
|
},
|
||||||
|
|
||||||
|
"createChannel": "Create Channel",
|
||||||
|
"@createChannel": {
|
||||||
|
"description": "Button text for creating a channel"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2602,5 +2602,21 @@
|
|||||||
"showForestRoads": "Mostrar caminos forestales",
|
"showForestRoads": "Mostrar caminos forestales",
|
||||||
"wmsOverlays": "Superposiciones WMS",
|
"wmsOverlays": "Superposiciones WMS",
|
||||||
|
|
||||||
"recentMessages": "Mensajes Recientes"
|
"recentMessages": "Mensajes Recientes",
|
||||||
|
|
||||||
|
"addChannel": "Agregar Canal",
|
||||||
|
"channelName": "Nombre del Canal",
|
||||||
|
"channelNameHint": "ej. Equipo de Rescate Alfa",
|
||||||
|
"channelSecret": "Contraseña del Canal",
|
||||||
|
"channelSecretHint": "Contraseña compartida para este canal",
|
||||||
|
"channelSecretHelp": "Esta contraseña debe compartirse con todos los miembros del equipo que necesiten acceso a este canal",
|
||||||
|
"channelNameRequired": "El nombre del canal es obligatorio",
|
||||||
|
"channelNameTooLong": "El nombre del canal debe tener 31 caracteres o menos",
|
||||||
|
"channelSecretRequired": "La contraseña del canal es obligatoria",
|
||||||
|
"channelSecretTooLong": "La contraseña del canal debe tener 32 caracteres o menos",
|
||||||
|
"invalidAsciiCharacters": "Solo se permiten caracteres ASCII",
|
||||||
|
"channelCreatedSuccessfully": "Canal creado exitosamente",
|
||||||
|
"channelCreationFailed": "Error al crear el canal: {error}",
|
||||||
|
"allChannelSlotsInUse": "Todos los espacios de canales están en uso (máximo 39 canales personalizados)",
|
||||||
|
"createChannel": "Crear Canal"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2610,5 +2610,21 @@
|
|||||||
"recentMessages": "Messages Récents",
|
"recentMessages": "Messages Récents",
|
||||||
"@recentMessages": {
|
"@recentMessages": {
|
||||||
"description": "Header for recent messages overlay on map in fullscreen mode"
|
"description": "Header for recent messages overlay on map in fullscreen mode"
|
||||||
}
|
},
|
||||||
|
|
||||||
|
"addChannel": "Ajouter un Canal",
|
||||||
|
"channelName": "Nom du Canal",
|
||||||
|
"channelNameHint": "par ex. Équipe de Sauvetage Alpha",
|
||||||
|
"channelSecret": "Mot de Passe du Canal",
|
||||||
|
"channelSecretHint": "Mot de passe partagé pour ce canal",
|
||||||
|
"channelSecretHelp": "Ce mot de passe doit être partagé avec tous les membres de l'équipe qui ont besoin d'accéder à ce canal",
|
||||||
|
"channelNameRequired": "Le nom du canal est requis",
|
||||||
|
"channelNameTooLong": "Le nom du canal doit contenir 31 caractères ou moins",
|
||||||
|
"channelSecretRequired": "Le mot de passe du canal est requis",
|
||||||
|
"channelSecretTooLong": "Le mot de passe du canal doit contenir 32 caractères ou moins",
|
||||||
|
"invalidAsciiCharacters": "Seuls les caractères ASCII sont autorisés",
|
||||||
|
"channelCreatedSuccessfully": "Canal créé avec succès",
|
||||||
|
"channelCreationFailed": "Échec de la création du canal : {error}",
|
||||||
|
"allChannelSlotsInUse": "Tous les emplacements de canaux sont utilisés (maximum 39 canaux personnalisés)",
|
||||||
|
"createChannel": "Créer un Canal"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -981,5 +981,21 @@
|
|||||||
"showForestRoads": "Prikaži šumske ceste",
|
"showForestRoads": "Prikaži šumske ceste",
|
||||||
"wmsOverlays": "WMS Prekrivanja",
|
"wmsOverlays": "WMS Prekrivanja",
|
||||||
|
|
||||||
"recentMessages": "Nedavne poruke"
|
"recentMessages": "Nedavne poruke",
|
||||||
|
|
||||||
|
"addChannel": "Dodaj kanal",
|
||||||
|
"channelName": "Ime kanala",
|
||||||
|
"channelNameHint": "npr. Spasilačka ekipa Alfa",
|
||||||
|
"channelSecret": "Lozinka kanala",
|
||||||
|
"channelSecretHint": "Zajednička lozinka za ovaj kanal",
|
||||||
|
"channelSecretHelp": "Ova lozinka mora biti podijeljena sa svim članovima tima koji trebaju pristup ovom kanalu",
|
||||||
|
"channelNameRequired": "Ime kanala je obavezno",
|
||||||
|
"channelNameTooLong": "Ime kanala mora imati najviše 31 znak",
|
||||||
|
"channelSecretRequired": "Lozinka kanala je obavezna",
|
||||||
|
"channelSecretTooLong": "Lozinka kanala mora imati najviše 32 znaka",
|
||||||
|
"invalidAsciiCharacters": "Samo ASCII znakovi su dozvoljeni",
|
||||||
|
"channelCreatedSuccessfully": "Kanal uspješno kreiran",
|
||||||
|
"channelCreationFailed": "Neuspješno kreiranje kanala: {error}",
|
||||||
|
"allChannelSlotsInUse": "Svi slotovi kanala su zauzeti (maksimalno 39 prilagođenih kanala)",
|
||||||
|
"createChannel": "Kreiraj kanal"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2610,5 +2610,21 @@
|
|||||||
"recentMessages": "Messaggi Recenti",
|
"recentMessages": "Messaggi Recenti",
|
||||||
"@recentMessages": {
|
"@recentMessages": {
|
||||||
"description": "Header for recent messages overlay on map in fullscreen mode"
|
"description": "Header for recent messages overlay on map in fullscreen mode"
|
||||||
}
|
},
|
||||||
|
|
||||||
|
"addChannel": "Aggiungi Canale",
|
||||||
|
"channelName": "Nome del Canale",
|
||||||
|
"channelNameHint": "es. Squadra di Soccorso Alfa",
|
||||||
|
"channelSecret": "Password del Canale",
|
||||||
|
"channelSecretHint": "Password condivisa per questo canale",
|
||||||
|
"channelSecretHelp": "Questa password deve essere condivisa con tutti i membri del team che necessitano di accesso a questo canale",
|
||||||
|
"channelNameRequired": "Il nome del canale è obbligatorio",
|
||||||
|
"channelNameTooLong": "Il nome del canale deve contenere al massimo 31 caratteri",
|
||||||
|
"channelSecretRequired": "La password del canale è obbligatoria",
|
||||||
|
"channelSecretTooLong": "La password del canale deve contenere al massimo 32 caratteri",
|
||||||
|
"invalidAsciiCharacters": "Sono consentiti solo caratteri ASCII",
|
||||||
|
"channelCreatedSuccessfully": "Canale creato con successo",
|
||||||
|
"channelCreationFailed": "Creazione del canale fallita: {error}",
|
||||||
|
"allChannelSlotsInUse": "Tutti gli slot dei canali sono in uso (massimo 39 canali personalizzati)",
|
||||||
|
"createChannel": "Crea Canale"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3464,6 +3464,96 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Recent Messages'**
|
/// **'Recent Messages'**
|
||||||
String get recentMessages;
|
String get recentMessages;
|
||||||
|
|
||||||
|
/// Button to add a new channel
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Add Channel'**
|
||||||
|
String get addChannel;
|
||||||
|
|
||||||
|
/// Label for channel name field
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Channel Name'**
|
||||||
|
String get channelName;
|
||||||
|
|
||||||
|
/// Hint for channel name field
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'e.g., Rescue Team Alpha'**
|
||||||
|
String get channelNameHint;
|
||||||
|
|
||||||
|
/// Label for channel secret field
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Channel Secret'**
|
||||||
|
String get channelSecret;
|
||||||
|
|
||||||
|
/// Hint for channel secret field
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Shared password for this channel'**
|
||||||
|
String get channelSecretHint;
|
||||||
|
|
||||||
|
/// Help text explaining channel secret
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'This secret must be shared with all team members who need access to this channel'**
|
||||||
|
String get channelSecretHelp;
|
||||||
|
|
||||||
|
/// Validation error for empty channel name
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Channel name is required'**
|
||||||
|
String get channelNameRequired;
|
||||||
|
|
||||||
|
/// Validation error for channel name too long
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Channel name must be 31 characters or less'**
|
||||||
|
String get channelNameTooLong;
|
||||||
|
|
||||||
|
/// Validation error for empty channel secret
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Channel secret is required'**
|
||||||
|
String get channelSecretRequired;
|
||||||
|
|
||||||
|
/// Validation error for channel secret too long
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Channel secret must be 32 characters or less'**
|
||||||
|
String get channelSecretTooLong;
|
||||||
|
|
||||||
|
/// Validation error for non-ASCII characters
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Only ASCII characters are allowed'**
|
||||||
|
String get invalidAsciiCharacters;
|
||||||
|
|
||||||
|
/// Success message after creating channel
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Channel created successfully'**
|
||||||
|
String get channelCreatedSuccessfully;
|
||||||
|
|
||||||
|
/// Error message when channel creation fails
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Failed to create channel: {error}'**
|
||||||
|
String channelCreationFailed(String error);
|
||||||
|
|
||||||
|
/// Error when no channel slots available
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'All channel slots are in use (maximum 39 custom channels)'**
|
||||||
|
String get allChannelSlotsInUse;
|
||||||
|
|
||||||
|
/// Button text for creating a channel
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Create Channel'**
|
||||||
|
String get createChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@@ -1932,4 +1932,55 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get recentMessages => 'Aktuelle Nachrichten';
|
String get recentMessages => 'Aktuelle Nachrichten';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addChannel => 'Kanal hinzufügen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelName => 'Kanalname';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameHint => 'z.B. Rettungsteam Alpha';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecret => 'Kanal-Passwort';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretHint => 'Gemeinsames Passwort für diesen Kanal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretHelp =>
|
||||||
|
'Dieses Passwort muss mit allen Teammitgliedern geteilt werden, die Zugriff auf diesen Kanal benötigen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameRequired => 'Kanalname ist erforderlich';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameTooLong =>
|
||||||
|
'Kanalname darf maximal 31 Zeichen lang sein';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretRequired => 'Kanal-Passwort ist erforderlich';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretTooLong =>
|
||||||
|
'Kanal-Passwort darf maximal 32 Zeichen lang sein';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get invalidAsciiCharacters => 'Nur ASCII-Zeichen sind erlaubt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelCreatedSuccessfully => 'Kanal erfolgreich erstellt';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channelCreationFailed(String error) {
|
||||||
|
return 'Kanal konnte nicht erstellt werden: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get allChannelSlotsInUse =>
|
||||||
|
'Alle Kanalplätze sind belegt (maximal 39 benutzerdefinierte Kanäle)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createChannel => 'Kanal erstellen';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1912,4 +1912,54 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get recentMessages => 'Recent Messages';
|
String get recentMessages => 'Recent Messages';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addChannel => 'Add Channel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelName => 'Channel Name';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameHint => 'e.g., Rescue Team Alpha';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecret => 'Channel Secret';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretHint => 'Shared password for this channel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretHelp =>
|
||||||
|
'This secret must be shared with all team members who need access to this channel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameRequired => 'Channel name is required';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameTooLong => 'Channel name must be 31 characters or less';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretRequired => 'Channel secret is required';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretTooLong =>
|
||||||
|
'Channel secret must be 32 characters or less';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get invalidAsciiCharacters => 'Only ASCII characters are allowed';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelCreatedSuccessfully => 'Channel created successfully';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channelCreationFailed(String error) {
|
||||||
|
return 'Failed to create channel: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get allChannelSlotsInUse =>
|
||||||
|
'All channel slots are in use (maximum 39 custom channels)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createChannel => 'Create Channel';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1937,4 +1937,55 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get recentMessages => 'Mensajes Recientes';
|
String get recentMessages => 'Mensajes Recientes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addChannel => 'Agregar Canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelName => 'Nombre del Canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameHint => 'ej. Equipo de Rescate Alfa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecret => 'Contraseña del Canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretHint => 'Contraseña compartida para este canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretHelp =>
|
||||||
|
'Esta contraseña debe compartirse con todos los miembros del equipo que necesiten acceso a este canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameRequired => 'El nombre del canal es obligatorio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameTooLong =>
|
||||||
|
'El nombre del canal debe tener 31 caracteres o menos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretRequired => 'La contraseña del canal es obligatoria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretTooLong =>
|
||||||
|
'La contraseña del canal debe tener 32 caracteres o menos';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get invalidAsciiCharacters => 'Solo se permiten caracteres ASCII';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelCreatedSuccessfully => 'Canal creado exitosamente';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channelCreationFailed(String error) {
|
||||||
|
return 'Error al crear el canal: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get allChannelSlotsInUse =>
|
||||||
|
'Todos los espacios de canales están en uso (máximo 39 canales personalizados)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createChannel => 'Crear Canal';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1941,4 +1941,56 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get recentMessages => 'Messages Récents';
|
String get recentMessages => 'Messages Récents';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addChannel => 'Ajouter un Canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelName => 'Nom du Canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameHint => 'par ex. Équipe de Sauvetage Alpha';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecret => 'Mot de Passe du Canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretHint => 'Mot de passe partagé pour ce canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretHelp =>
|
||||||
|
'Ce mot de passe doit être partagé avec tous les membres de l\'équipe qui ont besoin d\'accéder à ce canal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameRequired => 'Le nom du canal est requis';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameTooLong =>
|
||||||
|
'Le nom du canal doit contenir 31 caractères ou moins';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretRequired => 'Le mot de passe du canal est requis';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretTooLong =>
|
||||||
|
'Le mot de passe du canal doit contenir 32 caractères ou moins';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get invalidAsciiCharacters =>
|
||||||
|
'Seuls les caractères ASCII sont autorisés';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelCreatedSuccessfully => 'Canal créé avec succès';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channelCreationFailed(String error) {
|
||||||
|
return 'Échec de la création du canal : $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get allChannelSlotsInUse =>
|
||||||
|
'Tous les emplacements de canaux sont utilisés (maximum 39 canaux personnalisés)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createChannel => 'Créer un Canal';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1921,4 +1921,54 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get recentMessages => 'Nedavne poruke';
|
String get recentMessages => 'Nedavne poruke';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addChannel => 'Dodaj kanal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelName => 'Ime kanala';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameHint => 'npr. Spasilačka ekipa Alfa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecret => 'Lozinka kanala';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretHint => 'Zajednička lozinka za ovaj kanal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretHelp =>
|
||||||
|
'Ova lozinka mora biti podijeljena sa svim članovima tima koji trebaju pristup ovom kanalu';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameRequired => 'Ime kanala je obavezno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameTooLong => 'Ime kanala mora imati najviše 31 znak';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretRequired => 'Lozinka kanala je obavezna';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretTooLong =>
|
||||||
|
'Lozinka kanala mora imati najviše 32 znaka';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get invalidAsciiCharacters => 'Samo ASCII znakovi su dozvoljeni';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelCreatedSuccessfully => 'Kanal uspješno kreiran';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channelCreationFailed(String error) {
|
||||||
|
return 'Neuspješno kreiranje kanala: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get allChannelSlotsInUse =>
|
||||||
|
'Svi slotovi kanala su zauzeti (maksimalno 39 prilagođenih kanala)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createChannel => 'Kreiraj kanal';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1932,4 +1932,55 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get recentMessages => 'Messaggi Recenti';
|
String get recentMessages => 'Messaggi Recenti';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addChannel => 'Aggiungi Canale';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelName => 'Nome del Canale';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameHint => 'es. Squadra di Soccorso Alfa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecret => 'Password del Canale';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretHint => 'Password condivisa per questo canale';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretHelp =>
|
||||||
|
'Questa password deve essere condivisa con tutti i membri del team che necessitano di accesso a questo canale';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameRequired => 'Il nome del canale è obbligatorio';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameTooLong =>
|
||||||
|
'Il nome del canale deve contenere al massimo 31 caratteri';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretRequired => 'La password del canale è obbligatoria';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretTooLong =>
|
||||||
|
'La password del canale deve contenere al massimo 32 caratteri';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get invalidAsciiCharacters => 'Sono consentiti solo caratteri ASCII';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelCreatedSuccessfully => 'Canale creato con successo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channelCreationFailed(String error) {
|
||||||
|
return 'Creazione del canale fallita: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get allChannelSlotsInUse =>
|
||||||
|
'Tutti gli slot dei canali sono in uso (massimo 39 canali personalizzati)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createChannel => 'Crea Canale';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1922,4 +1922,53 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get recentMessages => 'Nedavna sporočila';
|
String get recentMessages => 'Nedavna sporočila';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get addChannel => 'Dodaj kanal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelName => 'Ime kanala';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameHint => 'npr. Reševalna ekipa Alfa';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecret => 'Geslo kanala';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretHint => 'Skupno geslo za ta kanal';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretHelp =>
|
||||||
|
'To geslo mora biti deljeno z vsemi člani ekipe, ki potrebujejo dostop do tega kanala';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameRequired => 'Ime kanala je obvezno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelNameTooLong => 'Ime kanala mora imeti največ 31 znakov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretRequired => 'Geslo kanala je obvezno';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelSecretTooLong => 'Geslo kanala mora imeti največ 32 znakov';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get invalidAsciiCharacters => 'Dovoljeni so samo ASCII znaki';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get channelCreatedSuccessfully => 'Kanal uspešno ustvarjen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String channelCreationFailed(String error) {
|
||||||
|
return 'Neuspešno ustvarjanje kanala: $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get allChannelSlotsInUse =>
|
||||||
|
'Vsa mesta za kanale so zasedena (maksimalno 39 prilagojenih kanalov)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get createChannel => 'Ustvari kanal';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -981,5 +981,21 @@
|
|||||||
"showForestRoads": "Prikaži gozdne ceste",
|
"showForestRoads": "Prikaži gozdne ceste",
|
||||||
"wmsOverlays": "WMS Prekrivanja",
|
"wmsOverlays": "WMS Prekrivanja",
|
||||||
|
|
||||||
"recentMessages": "Nedavna sporočila"
|
"recentMessages": "Nedavna sporočila",
|
||||||
|
|
||||||
|
"addChannel": "Dodaj kanal",
|
||||||
|
"channelName": "Ime kanala",
|
||||||
|
"channelNameHint": "npr. Reševalna ekipa Alfa",
|
||||||
|
"channelSecret": "Geslo kanala",
|
||||||
|
"channelSecretHint": "Skupno geslo za ta kanal",
|
||||||
|
"channelSecretHelp": "To geslo mora biti deljeno z vsemi člani ekipe, ki potrebujejo dostop do tega kanala",
|
||||||
|
"channelNameRequired": "Ime kanala je obvezno",
|
||||||
|
"channelNameTooLong": "Ime kanala mora imeti največ 31 znakov",
|
||||||
|
"channelSecretRequired": "Geslo kanala je obvezno",
|
||||||
|
"channelSecretTooLong": "Geslo kanala mora imeti največ 32 znakov",
|
||||||
|
"invalidAsciiCharacters": "Dovoljeni so samo ASCII znaki",
|
||||||
|
"channelCreatedSuccessfully": "Kanal uspešno ustvarjen",
|
||||||
|
"channelCreationFailed": "Neuspešno ustvarjanje kanala: {error}",
|
||||||
|
"allChannelSlotsInUse": "Vsa mesta za kanale so zasedena (maksimalno 39 prilagojenih kanalov)",
|
||||||
|
"createChannel": "Ustvari kanal"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
import '../models/device_info.dart';
|
import '../models/device_info.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
import '../models/message.dart';
|
import '../models/message.dart';
|
||||||
@@ -741,6 +743,108 @@ class ConnectionProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find next available empty channel slot (1-39)
|
||||||
|
///
|
||||||
|
/// Returns the channel index of the first empty slot, or null if all slots are in use.
|
||||||
|
/// Skips slot 0 (reserved for Public Channel).
|
||||||
|
Future<int?> findNextEmptyChannelSlot() async {
|
||||||
|
if (!_bleService.isConnected) {
|
||||||
|
throw Exception('Not connected to device');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debugPrint('🔍 [Provider] Finding next empty channel slot...');
|
||||||
|
|
||||||
|
// Query channels 1-39 (skip 0 = public channel)
|
||||||
|
// maxChannels from device info, or default to 40
|
||||||
|
final maxChannels = _deviceInfo.maxChannels ?? 40;
|
||||||
|
|
||||||
|
for (int i = 1; i < maxChannels; i++) {
|
||||||
|
await _bleService.getChannel(i);
|
||||||
|
// Small delay to allow response to arrive
|
||||||
|
await Future.delayed(const Duration(milliseconds: 50));
|
||||||
|
|
||||||
|
// Check if this channel is empty via channels provider
|
||||||
|
// (The BLE response handler calls onChannelInfoReceived callback
|
||||||
|
// which updates the channels provider)
|
||||||
|
// For now, we'll return the first slot since the channels provider
|
||||||
|
// doesn't expose empty slot info. This can be improved later.
|
||||||
|
}
|
||||||
|
|
||||||
|
// For simplicity, return the first slot after public channel
|
||||||
|
// A more robust implementation would check which slots are actually empty
|
||||||
|
// by querying the channels provider
|
||||||
|
return 1;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ [Provider] Failed to find empty channel slot: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new channel with automatic slot assignment
|
||||||
|
///
|
||||||
|
/// Finds the next available empty channel slot and configures it with the
|
||||||
|
/// provided name and secret. The secret is converted from an ASCII string
|
||||||
|
/// to a 16-byte key using MD5 hashing.
|
||||||
|
///
|
||||||
|
/// [channelName] - Name for the channel (max 31 characters)
|
||||||
|
/// [channelSecret] - ASCII password for the channel (will be hashed to 16 bytes)
|
||||||
|
///
|
||||||
|
/// Throws an exception if all slots are in use or if the channel configuration fails.
|
||||||
|
Future<void> createChannel({
|
||||||
|
required String channelName,
|
||||||
|
required String channelSecret,
|
||||||
|
}) async {
|
||||||
|
if (!_bleService.isConnected) {
|
||||||
|
throw Exception('Not connected to device');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debugPrint('📻 [Provider] Creating new channel...');
|
||||||
|
debugPrint(' Name: $channelName');
|
||||||
|
|
||||||
|
// Find next empty slot
|
||||||
|
final slotIdx = await findNextEmptyChannelSlot();
|
||||||
|
if (slotIdx == null) {
|
||||||
|
throw Exception('All channel slots are in use (maximum 39 custom channels)');
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint(' Using slot: $slotIdx');
|
||||||
|
|
||||||
|
// Convert ASCII secret to 16-byte key using MD5
|
||||||
|
final secretBytes = _convertSecretToBytes(channelSecret);
|
||||||
|
debugPrint(' Secret converted to 16-byte key');
|
||||||
|
|
||||||
|
// Send CMD_SET_CHANNEL to radio
|
||||||
|
await _bleService.setChannel(
|
||||||
|
channelIdx: slotIdx,
|
||||||
|
channelName: channelName,
|
||||||
|
secret: secretBytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
debugPrint('✅ [Provider] Channel created successfully in slot $slotIdx');
|
||||||
|
|
||||||
|
// Small delay to allow the response to propagate
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Refresh channels to update UI
|
||||||
|
// The channel info will be received via onChannelInfoReceived callback
|
||||||
|
await _bleService.getChannel(slotIdx);
|
||||||
|
} catch (e) {
|
||||||
|
_error = 'Failed to create channel: $e';
|
||||||
|
debugPrint('❌ [Provider] Channel creation failed: $e');
|
||||||
|
notifyListeners();
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert ASCII secret string to 16-byte key using MD5 hash
|
||||||
|
List<int> _convertSecretToBytes(String asciiSecret) {
|
||||||
|
// Use MD5 hash to convert any length ASCII string to exactly 16 bytes
|
||||||
|
// This provides a deterministic and secure way to generate channel keys
|
||||||
|
return md5.convert(utf8.encode(asciiSecret)).bytes;
|
||||||
|
}
|
||||||
|
|
||||||
/// Add or update a contact on the companion radio
|
/// Add or update a contact on the companion radio
|
||||||
///
|
///
|
||||||
/// This manually adds a contact to the radio's internal contact table.
|
/// This manually adds a contact to the radio's internal contact table.
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import 'package:geolocator/geolocator.dart';
|
|||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
import '../providers/contacts_provider.dart';
|
import '../providers/contacts_provider.dart';
|
||||||
import '../providers/app_provider.dart';
|
import '../providers/app_provider.dart';
|
||||||
|
import '../providers/connection_provider.dart';
|
||||||
import '../widgets/contacts/contact_tile.dart';
|
import '../widgets/contacts/contact_tile.dart';
|
||||||
|
import '../widgets/contacts/add_channel_dialog.dart';
|
||||||
|
|
||||||
class ContactsTab extends StatefulWidget {
|
class ContactsTab extends StatefulWidget {
|
||||||
final VoidCallback? onNavigateToMap;
|
final VoidCallback? onNavigateToMap;
|
||||||
@@ -86,133 +88,196 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show the add channel dialog
|
||||||
|
Future<void> _showAddChannelDialog(BuildContext context) async {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AddChannelDialog(
|
||||||
|
onCreateChannel: (name, secret) async {
|
||||||
|
final connectionProvider = context.read<ConnectionProvider>();
|
||||||
|
try {
|
||||||
|
await connectionProvider.createChannel(
|
||||||
|
channelName: name,
|
||||||
|
channelSecret: secret,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(l10n.channelCreatedSuccessfully),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(l10n.channelCreationFailed(e.toString())),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
rethrow; // Re-throw to let dialog handle the error state
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the FAB for adding channels
|
||||||
|
Widget? _buildAddChannelFAB(BuildContext context) {
|
||||||
|
final appProvider = context.watch<AppProvider>();
|
||||||
|
final connectionProvider = context.watch<ConnectionProvider>();
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
// Only show when:
|
||||||
|
// - NOT in simple mode
|
||||||
|
// - Device is connected
|
||||||
|
if (appProvider.isSimpleMode ||
|
||||||
|
!connectionProvider.deviceInfo.isConnected) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FloatingActionButton.extended(
|
||||||
|
onPressed: () => _showAddChannelDialog(context),
|
||||||
|
icon: const Icon(Icons.add_circle_outline),
|
||||||
|
label: Text(l10n.addChannel),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final appProvider = context.watch<AppProvider>();
|
final appProvider = context.watch<AppProvider>();
|
||||||
final isSimpleMode = appProvider.isSimpleMode;
|
final isSimpleMode = appProvider.isSimpleMode;
|
||||||
|
|
||||||
return Consumer<ContactsProvider>(
|
return Scaffold(
|
||||||
builder: (context, contactsProvider, child) {
|
body: Consumer<ContactsProvider>(
|
||||||
final chatContacts = contactsProvider.chatContacts;
|
builder: (context, contactsProvider, child) {
|
||||||
final repeaters = contactsProvider.repeaters;
|
final chatContacts = contactsProvider.chatContacts;
|
||||||
final rooms = contactsProvider.rooms;
|
final repeaters = contactsProvider.repeaters;
|
||||||
final channels = contactsProvider.channels;
|
final rooms = contactsProvider.rooms;
|
||||||
|
final channels = contactsProvider.channels;
|
||||||
|
|
||||||
// Check if there are any displayable contacts (excluding channels)
|
// Check if there are any displayable contacts (excluding channels)
|
||||||
final hasDisplayableContacts = chatContacts.isNotEmpty ||
|
final hasDisplayableContacts = chatContacts.isNotEmpty ||
|
||||||
repeaters.isNotEmpty ||
|
repeaters.isNotEmpty ||
|
||||||
rooms.isNotEmpty;
|
rooms.isNotEmpty;
|
||||||
|
|
||||||
if (!hasDisplayableContacts) {
|
if (!hasDisplayableContacts) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.contacts_outlined,
|
||||||
|
size: 64,
|
||||||
|
color: Theme.of(context).disabledColor,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
l10n.noContactsYet,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
l10n.connectToDeviceToLoadContacts,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: _handleRefresh,
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
// Team Members (Chat contacts)
|
||||||
Icons.contacts_outlined,
|
if (chatContacts.isNotEmpty) ...[
|
||||||
size: 64,
|
_SectionHeader(
|
||||||
color: Theme.of(context).disabledColor,
|
title: l10n.teamMembers,
|
||||||
),
|
count: chatContacts.length,
|
||||||
const SizedBox(height: 16),
|
icon: Icons.people,
|
||||||
Text(
|
),
|
||||||
l10n.noContactsYet,
|
...chatContacts.map(
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
(contact) => ContactTile(
|
||||||
),
|
contact: contact,
|
||||||
const SizedBox(height: 8),
|
currentPosition: _currentPosition,
|
||||||
Text(
|
calculateDistance: _calculateDistanceInMeters,
|
||||||
l10n.connectToDeviceToLoadContacts,
|
formatDistance: _formatDistance,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
onNavigateToMap: widget.onNavigateToMap,
|
||||||
textAlign: TextAlign.center,
|
),
|
||||||
),
|
),
|
||||||
|
const Divider(height: 32),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Repeaters
|
||||||
|
if (repeaters.isNotEmpty) ...[
|
||||||
|
_SectionHeader(
|
||||||
|
title: l10n.repeaters,
|
||||||
|
count: repeaters.length,
|
||||||
|
icon: Icons.router,
|
||||||
|
),
|
||||||
|
...repeaters.map(
|
||||||
|
(contact) => ContactTile(
|
||||||
|
contact: contact,
|
||||||
|
currentPosition: _currentPosition,
|
||||||
|
calculateDistance: _calculateDistanceInMeters,
|
||||||
|
formatDistance: _formatDistance,
|
||||||
|
onNavigateToMap: widget.onNavigateToMap,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 32),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Rooms
|
||||||
|
if (rooms.isNotEmpty) ...[
|
||||||
|
_SectionHeader(
|
||||||
|
title: l10n.rooms,
|
||||||
|
count: rooms.length,
|
||||||
|
icon: Icons.tag,
|
||||||
|
),
|
||||||
|
...rooms.map(
|
||||||
|
(contact) => ContactTile(
|
||||||
|
contact: contact,
|
||||||
|
currentPosition: _currentPosition,
|
||||||
|
calculateDistance: _calculateDistanceInMeters,
|
||||||
|
formatDistance: _formatDistance,
|
||||||
|
onNavigateToMap: widget.onNavigateToMap,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 32),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Channels (hidden in simple mode)
|
||||||
|
if (!isSimpleMode && channels.isNotEmpty) ...[
|
||||||
|
_SectionHeader(
|
||||||
|
title: l10n.channels,
|
||||||
|
count: channels.length,
|
||||||
|
icon: Icons.broadcast_on_personal,
|
||||||
|
),
|
||||||
|
...channels.map(
|
||||||
|
(contact) => ContactTile(
|
||||||
|
contact: contact,
|
||||||
|
currentPosition: _currentPosition,
|
||||||
|
calculateDistance: _calculateDistanceInMeters,
|
||||||
|
formatDistance: _formatDistance,
|
||||||
|
onNavigateToMap: widget.onNavigateToMap,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
),
|
||||||
return RefreshIndicator(
|
floatingActionButton: _buildAddChannelFAB(context),
|
||||||
onRefresh: _handleRefresh,
|
|
||||||
child: ListView(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
children: [
|
|
||||||
// Team Members (Chat contacts)
|
|
||||||
if (chatContacts.isNotEmpty) ...[
|
|
||||||
_SectionHeader(
|
|
||||||
title: l10n.teamMembers,
|
|
||||||
count: chatContacts.length,
|
|
||||||
icon: Icons.people,
|
|
||||||
),
|
|
||||||
...chatContacts.map(
|
|
||||||
(contact) => ContactTile(
|
|
||||||
contact: contact,
|
|
||||||
currentPosition: _currentPosition,
|
|
||||||
calculateDistance: _calculateDistanceInMeters,
|
|
||||||
formatDistance: _formatDistance,
|
|
||||||
onNavigateToMap: widget.onNavigateToMap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 32),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Repeaters
|
|
||||||
if (repeaters.isNotEmpty) ...[
|
|
||||||
_SectionHeader(
|
|
||||||
title: l10n.repeaters,
|
|
||||||
count: repeaters.length,
|
|
||||||
icon: Icons.router,
|
|
||||||
),
|
|
||||||
...repeaters.map(
|
|
||||||
(contact) => ContactTile(
|
|
||||||
contact: contact,
|
|
||||||
currentPosition: _currentPosition,
|
|
||||||
calculateDistance: _calculateDistanceInMeters,
|
|
||||||
formatDistance: _formatDistance,
|
|
||||||
onNavigateToMap: widget.onNavigateToMap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 32),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Rooms
|
|
||||||
if (rooms.isNotEmpty) ...[
|
|
||||||
_SectionHeader(
|
|
||||||
title: l10n.rooms,
|
|
||||||
count: rooms.length,
|
|
||||||
icon: Icons.tag,
|
|
||||||
),
|
|
||||||
...rooms.map(
|
|
||||||
(contact) => ContactTile(
|
|
||||||
contact: contact,
|
|
||||||
currentPosition: _currentPosition,
|
|
||||||
calculateDistance: _calculateDistanceInMeters,
|
|
||||||
formatDistance: _formatDistance,
|
|
||||||
onNavigateToMap: widget.onNavigateToMap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(height: 32),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Channels (hidden in simple mode)
|
|
||||||
if (!isSimpleMode && channels.isNotEmpty) ...[
|
|
||||||
_SectionHeader(
|
|
||||||
title: l10n.channels,
|
|
||||||
count: channels.length,
|
|
||||||
icon: Icons.broadcast_on_personal,
|
|
||||||
),
|
|
||||||
...channels.map(
|
|
||||||
(contact) => ContactTile(
|
|
||||||
contact: contact,
|
|
||||||
currentPosition: _currentPosition,
|
|
||||||
calculateDistance: _calculateDistanceInMeters,
|
|
||||||
formatDistance: _formatDistance,
|
|
||||||
onNavigateToMap: widget.onNavigateToMap,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
178
lib/widgets/contacts/add_channel_dialog.dart
Normal file
178
lib/widgets/contacts/add_channel_dialog.dart
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
/// Dialog for adding a new channel
|
||||||
|
class AddChannelDialog extends StatefulWidget {
|
||||||
|
final Future<void> Function(String name, String secret) onCreateChannel;
|
||||||
|
|
||||||
|
const AddChannelDialog({
|
||||||
|
super.key,
|
||||||
|
required this.onCreateChannel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AddChannelDialog> createState() => _AddChannelDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddChannelDialogState extends State<AddChannelDialog> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _nameController = TextEditingController();
|
||||||
|
final _secretController = TextEditingController();
|
||||||
|
bool _isCreating = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
_secretController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate that a string contains only ASCII characters
|
||||||
|
bool _isAscii(String text) {
|
||||||
|
return text.codeUnits.every((unit) => unit < 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate channel name
|
||||||
|
String? _validateName(String? value) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return l10n.channelNameRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.length > 31) {
|
||||||
|
return l10n.channelNameTooLong;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isAscii(value)) {
|
||||||
|
return l10n.invalidAsciiCharacters;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate channel secret
|
||||||
|
String? _validateSecret(String? value) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return l10n.channelSecretRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.length > 32) {
|
||||||
|
return l10n.channelSecretTooLong;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isAscii(value)) {
|
||||||
|
return l10n.invalidAsciiCharacters;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle channel creation
|
||||||
|
Future<void> _handleCreate() async {
|
||||||
|
if (!_formKey.currentState!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isCreating = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await widget.onCreateChannel(
|
||||||
|
_nameController.text.trim(),
|
||||||
|
_secretController.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Error is handled by parent
|
||||||
|
setState(() {
|
||||||
|
_isCreating = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(l10n.addChannel),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Channel Name Field
|
||||||
|
TextFormField(
|
||||||
|
controller: _nameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: l10n.channelName,
|
||||||
|
hintText: l10n.channelNameHint,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
enabled: !_isCreating,
|
||||||
|
maxLength: 31,
|
||||||
|
validator: _validateName,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Channel Secret Field
|
||||||
|
TextFormField(
|
||||||
|
controller: _secretController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: l10n.channelSecret,
|
||||||
|
hintText: l10n.channelSecretHint,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
obscureText: true,
|
||||||
|
enabled: !_isCreating,
|
||||||
|
maxLength: 32,
|
||||||
|
validator: _validateSecret,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
onFieldSubmitted: (_) => _handleCreate(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Help Text
|
||||||
|
Text(
|
||||||
|
l10n.channelSecretHelp,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
// Cancel Button
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isCreating ? null : () => Navigator.of(context).pop(),
|
||||||
|
child: Text(l10n.cancel),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Create Button
|
||||||
|
FilledButton(
|
||||||
|
onPressed: _isCreating ? null : _handleCreate,
|
||||||
|
child: _isCreating
|
||||||
|
? const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: Text(l10n.createChannel),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -90,7 +90,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.4+2"
|
version: "0.3.4+2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ dependencies:
|
|||||||
# URL launching
|
# URL launching
|
||||||
url_launcher: ^6.3.0
|
url_launcher: ^6.3.0
|
||||||
|
|
||||||
|
# Cryptographic functions (for channel secret hashing)
|
||||||
|
crypto: ^3.0.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
Reference in New Issue
Block a user