diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 3fb9b95..a75f7db 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -237,12 +237,28 @@ class _SettingsScreenState extends State { 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 { ), ); } - - 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(); - 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 _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;