Merge branch 'dev' into update-pacox-multibyte

This commit is contained in:
HDDen
2026-06-12 13:39:09 +03:00
95 changed files with 14166 additions and 6756 deletions
+96 -35
View File
@@ -53,6 +53,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Future<void> _loadVersionInfo() async {
final packageInfo = await PackageInfo.fromPlatform();
if (!mounted) return;
setState(() {
_appVersion = packageInfo.version;
});
@@ -213,12 +214,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (percent == null) {
icon = Icons.battery_unknown;
iconColor = Colors.grey;
iconColor = Theme.of(context).colorScheme.onSurfaceVariant;
valueColor = null;
} else if (percent <= 15) {
icon = Icons.battery_alert;
iconColor = Colors.orange;
valueColor = Colors.orange;
iconColor = Theme.of(context).colorScheme.tertiary;
valueColor = Theme.of(context).colorScheme.tertiary;
} else {
icon = Icons.battery_full;
iconColor = null;
@@ -316,18 +317,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
trailing: const Icon(Icons.chevron_right),
onTap: () => _editLocation(context, connector),
),
if (connector.currentCustomVars?.containsKey('gps') ?? false) ...[
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.gps_fixed),
title: Text(l10n.settings_locationGPSEnable),
subtitle: Text(l10n.settings_locationGPSEnableSubtitle),
value: connector.currentCustomVars?['gps'] == '1',
onChanged: (value) async {
await connector.setCustomVar(value ? 'gps:1' : 'gps:0');
},
),
],
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.group_add_outlined),
@@ -370,13 +359,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
),
ListTile(
leading: const Icon(Icons.delete_outline, color: Colors.red),
leading: Icon(
Icons.delete_outline,
color: Theme.of(context).colorScheme.error,
),
title: Text(l10n.settings_deleteAllPaths),
subtitle: Text(
l10n.settings_deleteAllPathsSubtitle,
style: TextStyle(color: Colors.red[700]),
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
onTap: () => connector.deleteAllPaths(),
onTap: () => _confirmDeleteAllPaths(context, connector),
),
const Divider(height: 1),
ListTile(
@@ -394,7 +386,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.restart_alt, color: Colors.orange),
leading: Icon(
Icons.restart_alt,
color: Theme.of(context).colorScheme.tertiary,
),
title: Text(l10n.settings_rebootDevice),
subtitle: Text(l10n.settings_rebootDeviceSubtitle),
onTap: () => _confirmReboot(context, connector),
@@ -662,6 +657,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
intervalController.text = currentInterval.toString();
String? intervalError;
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
@@ -697,9 +694,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 16),
TextField(
controller: intervalController,
onChanged: (_) {
if (intervalError != null) {
setDialogState(() => intervalError = null);
}
},
decoration: InputDecoration(
labelText: l10n.settings_locationIntervalSec,
border: const OutlineInputBorder(),
errorText: intervalError,
),
keyboardType: const TextInputType.numberWithOptions(
decimal: false,
@@ -730,24 +733,25 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
TextButton(
onPressed: () async {
Navigator.pop(context);
int? interval;
if (hasGPS) {
final intervalText = intervalController.text.trim();
if (intervalText.isEmpty) {
return;
if (intervalText.isNotEmpty) {
interval = int.tryParse(intervalText);
if (interval == null ||
interval < 60 ||
interval >= 86400) {
setDialogState(() {
intervalError = l10n.settings_locationIntervalInvalid;
});
return;
}
}
}
final interval = int.tryParse(intervalText);
if (interval == null || interval < 60 || interval >= 86400) {
if (!context.mounted) return;
showDismissibleSnackBar(
context,
content: Text(l10n.settings_locationIntervalInvalid),
);
return;
}
Navigator.pop(context);
if (interval != null) {
await connector.setCustomVar("gps_interval:$interval");
await connector.refreshDeviceInfo();
if (!context.mounted) return;
@@ -813,6 +817,36 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
void _confirmDeleteAllPaths(
BuildContext context,
MeshCoreConnector connector,
) {
final l10n = context.l10n;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.settings_deleteAllPaths),
content: Text(l10n.settings_deleteAllPathsSubtitle),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () {
Navigator.pop(context);
connector.deleteAllPaths();
},
child: Text(
l10n.common_deleteAll,
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
],
),
);
}
void _confirmReboot(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
showDialog(
@@ -832,7 +866,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
},
child: Text(
l10n.common_reboot,
style: const TextStyle(color: Colors.orange),
style: TextStyle(color: Theme.of(context).colorScheme.tertiary),
),
),
],
@@ -1223,6 +1257,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
bool _clientRepeat = false;
int? _selectedPresetIndex;
_RadioSettingsSnapshot? _lastNonRepeatSnapshot;
String? _frequencyError;
String? _txPowerError;
AppDebugLogService get _appLog =>
Provider.of<AppDebugLogService>(context, listen: false);
@@ -1489,7 +1525,24 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
void _handleManualSettingsChanged(String source) {
_logRadioSettingsState('Manual settings edit: $source');
setState(_syncPresetSelection);
setState(() {
_validateFields();
_syncPresetSelection();
});
}
void _validateFields() {
final l10n = context.l10n;
final freqMHz = double.tryParse(_frequencyController.text);
_frequencyError = (freqMHz == null || freqMHz < 300 || freqMHz > 2500)
? l10n.settings_frequencyInvalid
: null;
final maxTxPower = widget.connector.maxTxPower ?? 22;
final txPower = int.tryParse(_txPowerController.text);
_txPowerError = (txPower == null || txPower < 0 || txPower > maxTxPower)
? '${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'
: null;
}
void _handleClientRepeatChanged(bool enabled) {
@@ -1601,6 +1654,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
content: Text(l10n.settings_error(e.toString())),
);
}
if (!mounted) return;
Navigator.pop(context);
}
@@ -1676,6 +1730,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
labelText: l10n.settings_frequency,
border: const OutlineInputBorder(),
helperText: l10n.settings_frequencyHelper,
errorText: _frequencyError,
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
@@ -1759,6 +1814,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
helperText: widget.connector.maxTxPower != null
? '${l10n.settings_txPowerHelper} (max: ${widget.connector.maxTxPower} dBm)'
: l10n.settings_txPowerHelper,
errorText: _txPowerError,
),
keyboardType: TextInputType.number,
),
@@ -1780,7 +1836,12 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
FilledButton(onPressed: _saveSettings, child: Text(l10n.common_save)),
FilledButton(
onPressed: (_frequencyError != null || _txPowerError != null)
? null
: _saveSettings,
child: Text(l10n.common_save),
),
],
);
}