mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Add Flutter live traffic monitor
This commit is contained in:
@@ -170,6 +170,12 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
bool _telemetryEnabled = false;
|
bool _telemetryEnabled = false;
|
||||||
bool _repeatEnabled = false;
|
bool _repeatEnabled = false;
|
||||||
bool _showCustomRadioSettings = false;
|
bool _showCustomRadioSettings = false;
|
||||||
|
bool _isSavingPublicInfo = false;
|
||||||
|
bool _isSavingRadioSettings = false;
|
||||||
|
bool _publicInfoSaved = false;
|
||||||
|
bool _radioSettingsSaved = false;
|
||||||
|
String? _publicInfoError;
|
||||||
|
String? _radioSettingsError;
|
||||||
String _selectedBandwidth = '62.5 kHz';
|
String _selectedBandwidth = '62.5 kHz';
|
||||||
int _selectedSpreadingFactor = 8;
|
int _selectedSpreadingFactor = 8;
|
||||||
int _selectedCodingRate = 8;
|
int _selectedCodingRate = 8;
|
||||||
@@ -336,11 +342,35 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _markPublicInfoDirty() {
|
||||||
|
if (_publicInfoSaved || _publicInfoError != null) {
|
||||||
|
setState(() {
|
||||||
|
_publicInfoSaved = false;
|
||||||
|
_publicInfoError = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _markRadioSettingsDirty() {
|
||||||
|
if (_radioSettingsSaved || _radioSettingsError != null) {
|
||||||
|
setState(() {
|
||||||
|
_radioSettingsSaved = false;
|
||||||
|
_radioSettingsError = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _savePublicInfo() async {
|
Future<void> _savePublicInfo() async {
|
||||||
final connectionProvider = context.read<ConnectionProvider>();
|
final connectionProvider = context.read<ConnectionProvider>();
|
||||||
final deviceInfo = connectionProvider.deviceInfo;
|
final deviceInfo = connectionProvider.deviceInfo;
|
||||||
final validator = ValidationService();
|
final validator = ValidationService();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isSavingPublicInfo = true;
|
||||||
|
_publicInfoSaved = false;
|
||||||
|
_publicInfoError = null;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Save name
|
// Save name
|
||||||
if (_nameController.text.isNotEmpty) {
|
if (_nameController.text.isNotEmpty) {
|
||||||
@@ -353,12 +383,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
final latResult = validator.parseLatitude(_latController.text);
|
final latResult = validator.parseLatitude(_latController.text);
|
||||||
if (!latResult.isSuccess) {
|
if (!latResult.isSuccess) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
setState(() {
|
||||||
SnackBar(
|
_publicInfoError = latResult.errorMessage!;
|
||||||
content: Text(latResult.errorMessage!),
|
_isSavingPublicInfo = false;
|
||||||
backgroundColor: Colors.red,
|
});
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -366,12 +394,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
final lonResult = validator.parseLongitude(_lonController.text);
|
final lonResult = validator.parseLongitude(_lonController.text);
|
||||||
if (!lonResult.isSuccess) {
|
if (!lonResult.isSuccess) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
setState(() {
|
||||||
SnackBar(
|
_publicInfoError = lonResult.errorMessage!;
|
||||||
content: Text(lonResult.errorMessage!),
|
_isSavingPublicInfo = false;
|
||||||
backgroundColor: Colors.red,
|
});
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -405,23 +431,19 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
await connectionProvider.refreshDeviceInfo();
|
await connectionProvider.refreshDeviceInfo();
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
setState(() {
|
||||||
SnackBar(
|
_isSavingPublicInfo = false;
|
||||||
content: Text(AppLocalizations.of(context)!.save),
|
_publicInfoSaved = true;
|
||||||
backgroundColor: Colors.green,
|
});
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
setState(() {
|
||||||
SnackBar(
|
_isSavingPublicInfo = false;
|
||||||
content: Text(
|
_publicInfoError = AppLocalizations.of(
|
||||||
AppLocalizations.of(context)!.failedToSave(e.toString()),
|
context,
|
||||||
),
|
)!.failedToSave(e.toString());
|
||||||
backgroundColor: Colors.red,
|
});
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -431,17 +453,21 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
final validator = ValidationService();
|
final validator = ValidationService();
|
||||||
final deviceInfo = connectionProvider.deviceInfo;
|
final deviceInfo = connectionProvider.deviceInfo;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isSavingRadioSettings = true;
|
||||||
|
_radioSettingsSaved = false;
|
||||||
|
_radioSettingsError = null;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Parse and validate frequency
|
// Parse and validate frequency
|
||||||
final freqResult = validator.parseFrequency(_freqController.text);
|
final freqResult = validator.parseFrequency(_freqController.text);
|
||||||
if (!freqResult.isSuccess) {
|
if (!freqResult.isSuccess) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
setState(() {
|
||||||
SnackBar(
|
_radioSettingsError = freqResult.errorMessage!;
|
||||||
content: Text(freqResult.errorMessage!),
|
_isSavingRadioSettings = false;
|
||||||
backgroundColor: Colors.red,
|
});
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -453,12 +479,10 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
);
|
);
|
||||||
if (!txPowerResult.isSuccess) {
|
if (!txPowerResult.isSuccess) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
setState(() {
|
||||||
SnackBar(
|
_radioSettingsError = txPowerResult.errorMessage!;
|
||||||
content: Text(txPowerResult.errorMessage!),
|
_isSavingRadioSettings = false;
|
||||||
backgroundColor: Colors.red,
|
});
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -481,23 +505,19 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
await connectionProvider.refreshDeviceInfo();
|
await connectionProvider.refreshDeviceInfo();
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
setState(() {
|
||||||
SnackBar(
|
_isSavingRadioSettings = false;
|
||||||
content: Text(AppLocalizations.of(context)!.save),
|
_radioSettingsSaved = true;
|
||||||
backgroundColor: Colors.green,
|
});
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
setState(() {
|
||||||
SnackBar(
|
_isSavingRadioSettings = false;
|
||||||
content: Text(
|
_radioSettingsError = AppLocalizations.of(
|
||||||
AppLocalizations.of(context)!.failedToSave(e.toString()),
|
context,
|
||||||
),
|
)!.failedToSave(e.toString());
|
||||||
backgroundColor: Colors.red,
|
});
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -698,9 +718,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
title: AppLocalizations.of(
|
title: AppLocalizations.of(
|
||||||
context,
|
context,
|
||||||
)!.telemetryAndLocationSharing,
|
)!.telemetryAndLocationSharing,
|
||||||
description: _telemetryEnabled
|
description: 'Share your location with nearby devices.',
|
||||||
? 'Your location is shared with nearby devices.'
|
|
||||||
: 'Your location is not being shared.',
|
|
||||||
accentColor: _telemetryEnabled
|
accentColor: _telemetryEnabled
|
||||||
? colorScheme.primary
|
? colorScheme.primary
|
||||||
: colorScheme.onSurfaceVariant,
|
: colorScheme.onSurfaceVariant,
|
||||||
@@ -709,6 +727,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_telemetryEnabled = value;
|
_telemetryEnabled = value;
|
||||||
|
_publicInfoSaved = false;
|
||||||
|
_publicInfoError = null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -716,6 +736,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _nameController,
|
controller: _nameController,
|
||||||
|
onChanged: (_) => _markPublicInfoDirty(),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Device name',
|
labelText: 'Device name',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
@@ -763,6 +784,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
child: _CompactCoordinateField(
|
child: _CompactCoordinateField(
|
||||||
controller: _latController,
|
controller: _latController,
|
||||||
label: 'Latitude',
|
label: 'Latitude',
|
||||||
|
onChanged: (_) => _markPublicInfoDirty(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
@@ -770,6 +792,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
child: _CompactCoordinateField(
|
child: _CompactCoordinateField(
|
||||||
controller: _lonController,
|
controller: _lonController,
|
||||||
label: 'Longitude',
|
label: 'Longitude',
|
||||||
|
onChanged: (_) => _markPublicInfoDirty(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
@@ -787,12 +810,22 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
|
if (_publicInfoError != null) ...[
|
||||||
|
Text(
|
||||||
|
_publicInfoError!,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: FilledButton.icon(
|
child: _SaveActionButton(
|
||||||
onPressed: _savePublicInfo,
|
onPressed: _isSavingPublicInfo ? null : _savePublicInfo,
|
||||||
icon: const Icon(Icons.save_outlined),
|
isSaving: _isSavingPublicInfo,
|
||||||
label: const Text('Save public info'),
|
isSaved: _publicInfoSaved,
|
||||||
|
label: 'Save public info',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -839,10 +872,13 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_selectedRadioPreset = null;
|
_selectedRadioPreset = null;
|
||||||
_showCustomRadioSettings = true;
|
_showCustomRadioSettings = true;
|
||||||
|
_radioSettingsSaved = false;
|
||||||
|
_radioSettingsError = null;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_applyRadioPreset(preset);
|
_applyRadioPreset(preset);
|
||||||
|
_markRadioSettingsDirty();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -900,6 +936,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
_selectedRadioPreset = null;
|
_selectedRadioPreset = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
_markRadioSettingsDirty();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -927,6 +964,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
_selectedBandwidth = newValue;
|
_selectedBandwidth = newValue;
|
||||||
_selectedRadioPreset = null;
|
_selectedRadioPreset = null;
|
||||||
});
|
});
|
||||||
|
_markRadioSettingsDirty();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -957,6 +995,7 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
_selectedSpreadingFactor = newValue;
|
_selectedSpreadingFactor = newValue;
|
||||||
_selectedRadioPreset = null;
|
_selectedRadioPreset = null;
|
||||||
});
|
});
|
||||||
|
_markRadioSettingsDirty();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -987,12 +1026,14 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
_selectedCodingRate = newValue;
|
_selectedCodingRate = newValue;
|
||||||
_selectedRadioPreset = null;
|
_selectedRadioPreset = null;
|
||||||
});
|
});
|
||||||
|
_markRadioSettingsDirty();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _txPowerController,
|
controller: _txPowerController,
|
||||||
|
onChanged: (_) => _markRadioSettingsDirty(),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(
|
labelText: AppLocalizations.of(
|
||||||
context,
|
context,
|
||||||
@@ -1027,18 +1068,32 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_repeatEnabled = value;
|
_repeatEnabled = value;
|
||||||
|
_radioSettingsSaved = false;
|
||||||
|
_radioSettingsError = null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
|
if (_radioSettingsError != null) ...[
|
||||||
|
Text(
|
||||||
|
_radioSettingsError!,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: FilledButton.icon(
|
child: _SaveActionButton(
|
||||||
onPressed: _saveRadioSettings,
|
onPressed: _isSavingRadioSettings
|
||||||
icon: const Icon(Icons.save_outlined),
|
? null
|
||||||
label: const Text('Save radio settings'),
|
: _saveRadioSettings,
|
||||||
|
isSaving: _isSavingRadioSettings,
|
||||||
|
isSaved: _radioSettingsSaved,
|
||||||
|
label: 'Save radio settings',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1381,16 +1436,19 @@ class _StorageStat extends StatelessWidget {
|
|||||||
class _CompactCoordinateField extends StatelessWidget {
|
class _CompactCoordinateField extends StatelessWidget {
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
final String label;
|
final String label;
|
||||||
|
final ValueChanged<String>? onChanged;
|
||||||
|
|
||||||
const _CompactCoordinateField({
|
const _CompactCoordinateField({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.label,
|
required this.label,
|
||||||
|
this.onChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
onChanged: onChanged,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: label,
|
labelText: label,
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(18)),
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(18)),
|
||||||
@@ -1512,6 +1570,53 @@ class _SelectedPresetCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SaveActionButton extends StatelessWidget {
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final bool isSaving;
|
||||||
|
final bool isSaved;
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
const _SaveActionButton({
|
||||||
|
required this.onPressed,
|
||||||
|
required this.isSaving,
|
||||||
|
required this.isSaved,
|
||||||
|
required this.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FilledButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 180),
|
||||||
|
child: isSaving
|
||||||
|
? Row(
|
||||||
|
key: const ValueKey('saving'),
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: const [
|
||||||
|
SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2.2),
|
||||||
|
),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Text('Saving...'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Row(
|
||||||
|
key: ValueKey(isSaved ? 'saved' : 'idle'),
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(isSaved ? Icons.check_rounded : Icons.save_outlined),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(isSaved ? 'Saved' : label),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _SettingHighlightCard extends StatelessWidget {
|
class _SettingHighlightCard extends StatelessWidget {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String title;
|
final String title;
|
||||||
|
|||||||
Reference in New Issue
Block a user