From b15cb46c4333fd1cc423366b904b393fe5b4b79a Mon Sep 17 00:00:00 2001 From: rzuasti Date: Wed, 27 May 2026 19:13:50 -0400 Subject: [PATCH] Extract device forget/register dialogs into shared device_actions utility Co-Authored-By: Claude Sonnet 4.6 --- TODO.md | 4 +- frontend/lib/devices/device_actions.dart | 129 +++++++++++++++++++++++ frontend/lib/devices/device_detail.dart | 122 +-------------------- frontend/lib/devices/device_list.dart | 121 +-------------------- 4 files changed, 138 insertions(+), 238 deletions(-) create mode 100644 frontend/lib/devices/device_actions.dart diff --git a/TODO.md b/TODO.md index f97079e..f41b294 100644 --- a/TODO.md +++ b/TODO.md @@ -14,7 +14,9 @@ - [x] Register a device - [x] Forget a device - [x] Extract the snack bar confirmations as a utility widget so it can be reused -- [ ] In the notifications list add an action to register the device if new +- [x] Add a detail device page with access from notifications and devices lists +- [x] Extract the device type as an enum (idem Notification) and with Icon getter too +- [x] Extract the confirmForget and showRegisterDialog methods from both device pages - [ ] In the devices list add filters by owner and device type - [ ] View detailed log of device activity (based on event log in the backend) - [ ] Scan process monitor and summary page diff --git a/frontend/lib/devices/device_actions.dart b/frontend/lib/devices/device_actions.dart new file mode 100644 index 0000000..8ecb072 --- /dev/null +++ b/frontend/lib/devices/device_actions.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; + +import '../model/device.dart'; +import '../model/device_type.dart'; +import '../utils/oott_api.dart'; +import '../utils/ui_snackbars.dart'; + +Future confirmForgetDevice( + BuildContext context, + Device device, + VoidCallback onRefresh, +) async { + final colorScheme = Theme.of(context).colorScheme; + + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Forget Device'), + content: Text( + 'This device will be unregistered and will no longer be linked to ' + '${device.owner}. Are you sure?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text('Forget', style: TextStyle(color: colorScheme.error)), + ), + ], + ), + ); + + if (confirmed != true || !context.mounted) return; + + try { + await BackendAPI.instance.forgetDevice(device.macAddress); + if (!context.mounted) return; + UISnackbars.showSuccess(context, 'Device forgotten'); + onRefresh(); + } catch (e) { + if (!context.mounted) return; + UISnackbars.showError(context, 'Failed to forget device: $e'); + } +} + +Future showRegisterDeviceDialog( + BuildContext context, + Device device, + VoidCallback onRefresh, +) async { + final formKey = GlobalKey(); + String owner = ''; + DeviceType deviceType = device.deviceType; + + final saved = await showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: const Text('Register Device'), + content: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: const InputDecoration(labelText: 'Owner'), + validator: (value) => value == null || value.trim().isEmpty + ? 'Owner is required' + : null, + onSaved: (value) => owner = value?.trim() ?? '', + ), + const SizedBox(height: 16), + InputDecorator( + decoration: const InputDecoration(labelText: 'Device Type'), + child: DropdownButton( + value: deviceType, + isExpanded: true, + underline: const SizedBox(), + items: DeviceType.values + .map( + (t) => + DropdownMenuItem(value: t, child: Text(t.label)), + ) + .toList(), + onChanged: (value) => + setDialogState(() => deviceType = value ?? deviceType), + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + if (formKey.currentState?.validate() ?? false) { + formKey.currentState?.save(); + Navigator.of(context).pop(true); + } + }, + child: const Text('Save'), + ), + ], + ), + ), + ); + + if (saved != true || !context.mounted) return; + + try { + await BackendAPI.instance.registerDevice( + device.macAddress, + owner, + deviceType.name, + ); + if (!context.mounted) return; + UISnackbars.showSuccess(context, 'Device registered'); + onRefresh(); + } catch (e) { + if (!context.mounted) return; + UISnackbars.showError(context, 'Failed to register device: $e'); + } +} diff --git a/frontend/lib/devices/device_detail.dart b/frontend/lib/devices/device_detail.dart index 2e15ef0..dd4433e 100644 --- a/frontend/lib/devices/device_detail.dart +++ b/frontend/lib/devices/device_detail.dart @@ -2,11 +2,10 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../model/device.dart'; -import '../model/device_type.dart'; import '../utils/friendly_date_formatter.dart'; import '../utils/oott_api.dart'; -import '../utils/ui_snackbars.dart'; import '../widgets/status_badge.dart'; +import 'device_actions.dart'; class DeviceDetail extends StatefulWidget { final String macAddress; @@ -49,121 +48,6 @@ class _DeviceDetailState extends State { } } - Future _confirmForget(Device device) async { - final colorScheme = Theme.of(context).colorScheme; - - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Forget Device'), - content: Text( - 'This device will be unregistered and will no longer be linked to ' - '${device.owner}. Are you sure?', - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text('Forget', style: TextStyle(color: colorScheme.error)), - ), - ], - ), - ); - - if (confirmed != true || !mounted) return; - - try { - await BackendAPI.instance.forgetDevice(device.macAddress); - if (!mounted) return; - UISnackbars.showSuccess(context, 'Device forgotten'); - _loadDevice(); - } catch (e) { - if (!mounted) return; - UISnackbars.showError(context, 'Failed to forget device: $e'); - } - } - - Future _showRegisterDialog(Device device) async { - final formKey = GlobalKey(); - String owner = ''; - DeviceType deviceType = device.deviceType; - - final saved = await showDialog( - context: context, - builder: (context) => StatefulBuilder( - builder: (context, setDialogState) => AlertDialog( - title: const Text('Register Device'), - content: Form( - key: formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - decoration: const InputDecoration(labelText: 'Owner'), - validator: (value) => value == null || value.trim().isEmpty - ? 'Owner is required' - : null, - onSaved: (value) => owner = value?.trim() ?? '', - ), - const SizedBox(height: 16), - InputDecorator( - decoration: const InputDecoration(labelText: 'Device Type'), - child: DropdownButton( - value: deviceType, - isExpanded: true, - underline: const SizedBox(), - items: DeviceType.values - .map( - (t) => - DropdownMenuItem(value: t, child: Text(t.label)), - ) - .toList(), - onChanged: (value) => - setDialogState(() => deviceType = value ?? deviceType), - ), - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - if (formKey.currentState?.validate() ?? false) { - formKey.currentState?.save(); - Navigator.of(context).pop(true); - } - }, - child: const Text('Save'), - ), - ], - ), - ), - ); - - if (saved != true || !mounted) return; - - try { - await BackendAPI.instance.registerDevice( - device.macAddress, - owner, - deviceType.name, - ); - if (!mounted) return; - UISnackbars.showSuccess(context, 'Device registered'); - _loadDevice(); - } catch (e) { - if (!mounted) return; - UISnackbars.showError(context, 'Failed to register device: $e'); - } - } - @override Widget build(BuildContext context) { final device = _device; @@ -178,9 +62,9 @@ class _DeviceDetailState extends State { icon: const Icon(Icons.more_vert), onSelected: (value) async { if (value == 'forget') { - await _confirmForget(device); + await confirmForgetDevice(context, device, _loadDevice); } else if (value == 'register') { - await _showRegisterDialog(device); + await showRegisterDeviceDialog(context, device, _loadDevice); } }, itemBuilder: (context) => [ diff --git a/frontend/lib/devices/device_list.dart b/frontend/lib/devices/device_list.dart index 790110e..98ab0ab 100644 --- a/frontend/lib/devices/device_list.dart +++ b/frontend/lib/devices/device_list.dart @@ -5,8 +5,8 @@ import '../model/device.dart'; import '../model/device_type.dart'; import '../utils/friendly_date_formatter.dart'; import '../utils/oott_api.dart'; -import '../utils/ui_snackbars.dart'; import '../widgets/status_badge.dart'; +import 'device_actions.dart'; enum _DeviceFilter { newDevices, registered, all } @@ -61,121 +61,6 @@ class _DeviceListState extends State { } } - Future _confirmForget(Device device) async { - final colorScheme = Theme.of(context).colorScheme; - - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Forget Device'), - content: Text( - 'This device will be unregistered and will no longer be linked to ' - '${device.owner}. Are you sure?', - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text('Forget', style: TextStyle(color: colorScheme.error)), - ), - ], - ), - ); - - if (confirmed != true || !mounted) return; - - try { - await BackendAPI.instance.forgetDevice(device.macAddress); - if (!mounted) return; - UISnackbars.showSuccess(context, 'Device forgotten'); - _loadDevices(); - } catch (e) { - if (!mounted) return; - UISnackbars.showError(context, 'Failed to forget device: $e'); - } - } - - Future _showRegisterDialog(Device device) async { - final formKey = GlobalKey(); - String owner = ''; - DeviceType deviceType = device.deviceType; - - final saved = await showDialog( - context: context, - builder: (context) => StatefulBuilder( - builder: (context, setDialogState) => AlertDialog( - title: const Text('Register Device'), - content: Form( - key: formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - decoration: const InputDecoration(labelText: 'Owner'), - validator: (value) => value == null || value.trim().isEmpty - ? 'Owner is required' - : null, - onSaved: (value) => owner = value?.trim() ?? '', - ), - const SizedBox(height: 16), - InputDecorator( - decoration: const InputDecoration(labelText: 'Device Type'), - child: DropdownButton( - value: deviceType, - isExpanded: true, - underline: const SizedBox(), - items: DeviceType.values - .map( - (t) => - DropdownMenuItem(value: t, child: Text(t.label)), - ) - .toList(), - onChanged: (value) => - setDialogState(() => deviceType = value ?? deviceType), - ), - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - if (formKey.currentState?.validate() ?? false) { - formKey.currentState?.save(); - Navigator.of(context).pop(true); - } - }, - child: const Text('Save'), - ), - ], - ), - ), - ); - - if (saved != true || !mounted) return; - - try { - await BackendAPI.instance.registerDevice( - device.macAddress, - owner, - deviceType.name, - ); - if (!mounted) return; - UISnackbars.showSuccess(context, 'Device registered'); - _loadDevices(); - } catch (e) { - if (!mounted) return; - UISnackbars.showError(context, 'Failed to register device: $e'); - } - } - String _emptyMessage() { switch (_filter) { case _DeviceFilter.newDevices: @@ -285,9 +170,9 @@ class _DeviceListState extends State { '/devices/${device.macAddress}', ); } else if (value == 'forget') { - await _confirmForget(device); + await confirmForgetDevice(context, device, _loadDevices); } else if (value == 'register') { - await _showRegisterDialog(device); + await showRegisterDeviceDialog(context, device, _loadDevices); } }, itemBuilder: (context) => [