feat(#18): inline Privacy settings as apply-on-change section

Replace the Privacy save/cancel dialog with an inline _PrivacySection in the pane: advert-location + multi-ACKs toggles and the three telemetry-mode dropdowns now apply on change. All five commit through one setTelemetryModeBase call, so each control sends the full current state (coalesced via an apply guard) and reverts to the device's values on failure with an error snackbar -- folding in the #28 error handling for this pane. Removes the category->tile->dialog hop; drops _privacySettings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pull/36/head
Strycher 1 month ago
parent 4ba4d22a79
commit eb2888162e

@ -223,12 +223,28 @@ class _SettingsScreenState extends State<SettingsScreen> {
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ children: [
Card( Card(
child: ListTile( child: Column(
leading: const Icon(Icons.visibility_off_outlined), crossAxisAlignment: CrossAxisAlignment.start,
title: Text(l10n.settings_privacy), children: [
subtitle: Text(l10n.settings_privacySubtitle), Padding(
trailing: const Icon(Icons.chevron_right), padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
onTap: () => _privacySettings(context, connector), child: Text(
l10n.settings_privacy,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
_PrivacySection(
key: ValueKey(
'${connector.telemetryModeBase}-${connector.telemetryModeLoc}-'
'${connector.telemetryModeEnv}-${connector.advertLocationPolicy}-'
'${connector.multiAcks}',
),
connector: connector,
),
],
), ),
), ),
], ],
@ -975,129 +991,171 @@ class _SettingsScreenState extends State<SettingsScreen> {
} }
} }
void _privacySettings(BuildContext context, MeshCoreConnector connector) { /// Inline privacy controls for the Privacy pane.
final l10n = context.l10n; ///
/// Advert-location, multi-ACKs, and the three telemetry-access modes all commit
int telemetryMode = connector.telemetryModeBase; /// through a single [MeshCoreConnector.setTelemetryModeBase] call, so each
int telemetryLocMode = connector.telemetryModeLoc; /// control sends the full current state on change (coalesced) and reverts to the
int telemetryEnvMode = connector.telemetryModeEnv; /// device's values on failure. The host re-keys this widget on the connector's
bool advertLocPolicy = connector.advertLocationPolicy == 0 ? false : true; /// current values, so a device-reported change rebuilds it with fresh values.
int multiAcks = connector.multiAcks; class _PrivacySection extends StatefulWidget {
final MeshCoreConnector connector;
final telemModeBase = [
DropdownMenuItem(value: teleModeDeny, child: Text(l10n.settings_denyAll)), const _PrivacySection({super.key, required this.connector});
DropdownMenuItem(
value: teleModeAllowFlags, @override
child: Text(l10n.settings_allowByContact), State<_PrivacySection> createState() => _PrivacySectionState();
), }
DropdownMenuItem(
value: teleModeAllowAll, class _PrivacySectionState extends State<_PrivacySection> {
child: Text(l10n.settings_allowAll), late int _telemetryMode;
), late int _telemetryLocMode;
]; late int _telemetryEnvMode;
late bool _advertLocPolicy;
showDialog( late int _multiAcks;
context: context,
builder: (dialogContext) => StatefulBuilder( bool _applying = false;
builder: (context, setDialogState) => AlertDialog( bool _applyAgain = false;
title: Text(l10n.settings_privacy),
content: SingleChildScrollView( @override
child: Column( void initState() {
mainAxisSize: MainAxisSize.min, super.initState();
crossAxisAlignment: CrossAxisAlignment.start, _seedFromConnector();
children: [ }
Text(l10n.settings_privacySettingsDescription),
const SizedBox(height: 16), void _seedFromConnector() {
FeatureToggleRow( final c = widget.connector;
title: l10n.settings_advertLocation, _telemetryMode = c.telemetryModeBase;
subtitle: l10n.settings_advertLocationSubtitle, _telemetryLocMode = c.telemetryModeLoc;
value: advertLocPolicy, _telemetryEnvMode = c.telemetryModeEnv;
onChanged: (value) { _advertLocPolicy = c.advertLocationPolicy != 0;
setDialogState(() => advertLocPolicy = value); _multiAcks = c.multiAcks;
}, }
),
const SizedBox(height: 8), Future<void> _apply() async {
SwitchListTile( if (_applying) {
title: Text(l10n.settings_multiAck), _applyAgain = true;
value: multiAcks == 1, return;
onChanged: (value) { }
setDialogState(() => multiAcks = value ? 1 : 0); _applying = true;
}, try {
contentPadding: EdgeInsets.zero, do {
), _applyAgain = false;
const SizedBox(height: 16), await widget.connector.setTelemetryModeBase(
DropdownButtonFormField<int>( _telemetryMode,
initialValue: telemetryMode, _telemetryLocMode,
decoration: InputDecoration( _telemetryEnvMode,
labelText: l10n.settings_telemetryBaseMode, _advertLocPolicy ? 1 : 0,
border: const OutlineInputBorder(), _multiAcks,
), );
items: telemModeBase, await widget.connector.refreshDeviceInfo();
onChanged: (value) { } while (_applyAgain);
if (value != null) { } catch (e) {
setDialogState(() => telemetryMode = value); if (!mounted) return;
} setState(_seedFromConnector);
}, showDismissibleSnackBar(
), context,
const SizedBox(height: 16), content: Text(context.l10n.settings_error(e.toString())),
DropdownButtonFormField<int>( );
initialValue: telemetryLocMode, } finally {
decoration: InputDecoration( _applying = false;
labelText: l10n.settings_telemetryLocationMode, }
border: const OutlineInputBorder(), }
),
items: telemModeBase, Widget _telemetryDropdown({
onChanged: (value) { required String label,
if (value != null) { required int value,
setDialogState(() => telemetryLocMode = value); required ValueChanged<int> onChanged,
} }) {
}, final l10n = context.l10n;
), return Padding(
const SizedBox(height: 16), padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
DropdownButtonFormField<int>( child: DropdownButtonFormField<int>(
initialValue: telemetryEnvMode, key: ValueKey('$label-$value'),
decoration: InputDecoration( initialValue: value,
labelText: l10n.settings_telemetryEnvironmentMode, decoration: InputDecoration(
border: const OutlineInputBorder(), labelText: label,
), border: const OutlineInputBorder(),
items: telemModeBase,
onChanged: (value) {
if (value != null) {
setDialogState(() => telemetryEnvMode = value);
}
},
),
],
),
), ),
actions: [ items: [
TextButton( DropdownMenuItem(
onPressed: () => Navigator.pop(context), value: teleModeDeny,
child: Text(l10n.common_cancel), child: Text(l10n.settings_denyAll),
), ),
TextButton( DropdownMenuItem(
onPressed: () async { value: teleModeAllowFlags,
Navigator.pop(context); child: Text(l10n.settings_allowByContact),
await connector.setTelemetryModeBase( ),
telemetryMode, DropdownMenuItem(
telemetryLocMode, value: teleModeAllowAll,
telemetryEnvMode, child: Text(l10n.settings_allowAll),
advertLocPolicy ? 1 : 0,
multiAcks,
);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
showDismissibleSnackBar(
context,
content: Text(l10n.settings_telemetryModeUpdated),
);
},
child: Text(l10n.common_save),
), ),
], ],
onChanged: (v) {
if (v == null) return;
onChanged(v);
},
), ),
), );
); }
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Text(l10n.settings_privacySettingsDescription),
),
SwitchListTile(
secondary: const Icon(Icons.share_location_outlined),
title: Text(l10n.settings_advertLocation),
subtitle: Text(l10n.settings_advertLocationSubtitle),
value: _advertLocPolicy,
onChanged: (value) {
setState(() => _advertLocPolicy = value);
_apply();
},
),
SwitchListTile(
secondary: const Icon(Icons.done_all),
title: Text(l10n.settings_multiAck),
value: _multiAcks == 1,
onChanged: (value) {
setState(() => _multiAcks = value ? 1 : 0);
_apply();
},
),
_telemetryDropdown(
label: l10n.settings_telemetryBaseMode,
value: _telemetryMode,
onChanged: (v) {
setState(() => _telemetryMode = v);
_apply();
},
),
_telemetryDropdown(
label: l10n.settings_telemetryLocationMode,
value: _telemetryLocMode,
onChanged: (v) {
setState(() => _telemetryLocMode = v);
_apply();
},
),
_telemetryDropdown(
label: l10n.settings_telemetryEnvironmentMode,
value: _telemetryEnvMode,
onChanged: (v) {
setState(() => _telemetryEnvMode = v);
_apply();
},
),
const SizedBox(height: 8),
],
);
}
} }
/// Inline path-hash size selector for the connected device. /// Inline path-hash size selector for the connected device.

Loading…
Cancel
Save

Powered by TurnKey Linux.