feat(#18): inline Contact auto-add toggles into Contacts pane

Replace the Contact-settings tile->modal dialog with an inline _AutoAddSection: five SwitchListTiles that apply on toggle (send config frame + refetch flags). Apply path has a coalescing guard (re-applies the latest state instead of overlapping sends) plus inline error handling (revert to device flags + snackbar). _PathHashSizeTile onChanged wrapped in try/catch + error snackbar.

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

@ -237,12 +237,28 @@ class _SettingsScreenState extends State<SettingsScreen> {
padding: const EdgeInsets.all(16),
children: [
Card(
child: ListTile(
leading: const Icon(Icons.group_add_outlined),
title: Text(l10n.settings_contactSettings),
subtitle: Text(l10n.settings_contactSettingsSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () => _editAutoAddConfig(context, connector),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
child: Text(
l10n.contactsSettings_autoAddTitle,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
_AutoAddSection(
key: ValueKey(
'${connector.autoAddUsers}-${connector.autoAddRepeaters}-'
'${connector.autoAddRoomServers}-${connector.autoAddSensors}-'
'${connector.autoAddOverwriteOldest}',
),
connector: connector,
),
],
),
),
],
@ -972,121 +988,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
);
}
void _editAutoAddConfig(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
bool autoAddChat = false;
bool autoAddRepeater = false;
bool autoAddRoomServer = false;
bool autoAddSensor = false;
bool overwriteOldest = false;
final connector = context.read<MeshCoreConnector>();
autoAddChat = connector.autoAddUsers ?? false;
autoAddRepeater = connector.autoAddRepeaters ?? false;
autoAddRoomServer = connector.autoAddRoomServers ?? false;
autoAddSensor = connector.autoAddSensors ?? false;
overwriteOldest = connector.autoAddOverwriteOldest ?? false;
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(l10n.contactsSettings_autoAddTitle),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FeatureToggleRow(
title: l10n.contactsSettings_autoAddUsersTitle,
subtitle: l10n.contactsSettings_autoAddUsersSubtitle,
value: autoAddChat,
onChanged: (value) {
setDialogState(() => autoAddChat = value);
},
),
SizedBox(height: 8),
FeatureToggleRow(
title: l10n.contactsSettings_autoAddRepeatersTitle,
subtitle: l10n.contactsSettings_autoAddRepeatersSubtitle,
value: autoAddRepeater,
onChanged: (value) {
setDialogState(() => autoAddRepeater = value);
},
),
SizedBox(height: 8),
FeatureToggleRow(
title: l10n.contactsSettings_autoAddRoomServersTitle,
subtitle: l10n.contactsSettings_autoAddRoomServersSubtitle,
value: autoAddRoomServer,
onChanged: (value) {
setDialogState(() => autoAddRoomServer = value);
},
),
SizedBox(height: 8),
FeatureToggleRow(
title: l10n.contactsSettings_autoAddSensorsTitle,
subtitle: l10n.contactsSettings_autoAddSensorsSubtitle,
value: autoAddSensor,
onChanged: (value) {
setDialogState(() => autoAddSensor = value);
},
),
Divider(height: 4),
FeatureToggleRow(
title: l10n.contactsSettings_overwriteOldestTitle,
subtitle: l10n.contactsSettings_overwriteOldestSubtitle,
value: overwriteOldest,
onChanged: (value) {
setDialogState(() => overwriteOldest = value);
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () {
_sendSettings(
connector,
autoAddChat,
autoAddRepeater,
autoAddRoomServer,
autoAddSensor,
overwriteOldest,
);
Navigator.pop(context);
},
child: Text(l10n.common_save),
),
],
),
),
);
}
void _sendSettings(
MeshCoreConnector connector,
bool autoAddChat,
bool autoAddRepeater,
bool autoAddRoomServer,
bool autoAddSensor,
bool overwriteOldest,
) async {
final frame = buildSetAutoAddConfigFrame(
autoAddChat: autoAddChat,
autoAddRepeater: autoAddRepeater,
autoAddRoomServer: autoAddRoomServer,
autoAddSensor: autoAddSensor,
overwriteOldest: overwriteOldest,
);
await connector.sendFrame(frame);
await connector.sendFrame(buildGetAutoAddFlagsFrame());
}
}
void _privacySettings(BuildContext context, MeshCoreConnector connector) {
@ -1261,15 +1162,23 @@ class _PathHashSizeTileState extends State<_PathHashSizeTile> {
onChanged: connector.isConnected
? (value) async {
if (value == null) return;
await connector.setPathHashMode(value);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
showDismissibleSnackBar(
context,
content: Text(
'${l10n.repeater_pathHashMode}: ${value + 1}-byte',
),
);
try {
await connector.setPathHashMode(value);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
showDismissibleSnackBar(
context,
content: Text(
'${l10n.repeater_pathHashMode}: ${value + 1}-byte',
),
);
} catch (e) {
if (!context.mounted) return;
showDismissibleSnackBar(
context,
content: Text(l10n.settings_error(e.toString())),
);
}
}
: null,
),
@ -1277,6 +1186,144 @@ class _PathHashSizeTileState extends State<_PathHashSizeTile> {
}
}
/// Inline auto-add controls for the Contacts pane.
///
/// Replaces the former modal dialog: toggles apply immediately (send + refetch
/// flags). The host re-keys this widget on the connector's current flags, so a
/// device-reported change rebuilds it with fresh values.
class _AutoAddSection extends StatefulWidget {
final MeshCoreConnector connector;
const _AutoAddSection({super.key, required this.connector});
@override
State<_AutoAddSection> createState() => _AutoAddSectionState();
}
class _AutoAddSectionState extends State<_AutoAddSection> {
late bool _chat;
late bool _repeater;
late bool _roomServer;
late bool _sensor;
late bool _overwriteOldest;
bool _applying = false;
bool _applyAgain = false;
@override
void initState() {
super.initState();
final c = widget.connector;
_chat = c.autoAddUsers ?? false;
_repeater = c.autoAddRepeaters ?? false;
_roomServer = c.autoAddRoomServers ?? false;
_sensor = c.autoAddSensors ?? false;
_overwriteOldest = c.autoAddOverwriteOldest ?? false;
}
Future<void> _apply() async {
// Coalesce rapid toggles: if a send is already in flight, flag a re-apply
// with the latest state for when it finishes instead of overlapping sends.
if (_applying) {
_applyAgain = true;
return;
}
_applying = true;
try {
do {
_applyAgain = false;
await widget.connector.sendFrame(
buildSetAutoAddConfigFrame(
autoAddChat: _chat,
autoAddRepeater: _repeater,
autoAddRoomServer: _roomServer,
autoAddSensor: _sensor,
overwriteOldest: _overwriteOldest,
),
);
await widget.connector.sendFrame(buildGetAutoAddFlagsFrame());
} while (_applyAgain);
} catch (e) {
if (!mounted) return;
// Revert the optimistic toggles to the device's last-known flags.
final c = widget.connector;
setState(() {
_chat = c.autoAddUsers ?? false;
_repeater = c.autoAddRepeaters ?? false;
_roomServer = c.autoAddRoomServers ?? false;
_sensor = c.autoAddSensors ?? false;
_overwriteOldest = c.autoAddOverwriteOldest ?? false;
});
showDismissibleSnackBar(
context,
content: Text(context.l10n.settings_error(e.toString())),
);
} finally {
_applying = false;
}
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Column(
children: [
SwitchListTile(
secondary: const Icon(Icons.person_add_outlined),
title: Text(l10n.contactsSettings_autoAddUsersTitle),
subtitle: Text(l10n.contactsSettings_autoAddUsersSubtitle),
value: _chat,
onChanged: (value) {
setState(() => _chat = value);
_apply();
},
),
SwitchListTile(
secondary: const Icon(Icons.router_outlined),
title: Text(l10n.contactsSettings_autoAddRepeatersTitle),
subtitle: Text(l10n.contactsSettings_autoAddRepeatersSubtitle),
value: _repeater,
onChanged: (value) {
setState(() => _repeater = value);
_apply();
},
),
SwitchListTile(
secondary: const Icon(Icons.meeting_room_outlined),
title: Text(l10n.contactsSettings_autoAddRoomServersTitle),
subtitle: Text(l10n.contactsSettings_autoAddRoomServersSubtitle),
value: _roomServer,
onChanged: (value) {
setState(() => _roomServer = value);
_apply();
},
),
SwitchListTile(
secondary: const Icon(Icons.sensors_outlined),
title: Text(l10n.contactsSettings_autoAddSensorsTitle),
subtitle: Text(l10n.contactsSettings_autoAddSensorsSubtitle),
value: _sensor,
onChanged: (value) {
setState(() => _sensor = value);
_apply();
},
),
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.autorenew),
title: Text(l10n.contactsSettings_overwriteOldestTitle),
subtitle: Text(l10n.contactsSettings_overwriteOldestSubtitle),
value: _overwriteOldest,
onChanged: (value) {
setState(() => _overwriteOldest = value);
_apply();
},
),
],
);
}
}
class _RadioSettingsDialog extends StatefulWidget {
final MeshCoreConnector connector;

Loading…
Cancel
Save

Powered by TurnKey Linux.