mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Add permanent device deletion and refine frontend UI/snackbars
Backend:
- Add db::devices::delete to erase a device and its events atomically
- Expose DELETE /api/devices/{mac}/permanently, wired to OpenAPI
- Cover the new db method and endpoint with tests
Frontend:
- Delete action for not-registered devices (detail screen + list row)
and an opt-in "permanently delete" checkbox in the Forget dialog
- Navigate to the devices list after deleting from the detail screen
- Refine button emphasis to M3: single filled primary, error-colored
text buttons for destructive actions, Test demoted to filled-tonal
- Flash the backend-config Test button red on a failed connection test
- Render snackbars through a top-level ScaffoldMessenger host so they
show above dialogs; keep the built-in SnackBar (with an Overlay host)
Docs:
- CLAUDE.md: rustfmt edition 2024, don't revert formatter-only changes,
prefer built-in Flutter components, follow existing patterns + M3
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2541d0989c
commit
587e097291
@@ -6,20 +6,111 @@ import '../utils/oott_api.dart';
|
||||
import '../utils/ui_snackbars.dart';
|
||||
import '../theme/dimens.dart';
|
||||
|
||||
/// Confirms forgetting (and optionally permanently deleting) a registered
|
||||
/// device.
|
||||
///
|
||||
/// On a successful permanent deletion [onDeleted] is invoked when provided
|
||||
/// (e.g. to navigate away from a now-stale detail page); otherwise [onRefresh]
|
||||
/// is used. A plain unregister always calls [onRefresh].
|
||||
Future<void> confirmForgetDevice(
|
||||
BuildContext context,
|
||||
Device device,
|
||||
VoidCallback onRefresh,
|
||||
) async {
|
||||
VoidCallback onRefresh, {
|
||||
VoidCallback? onDeleted,
|
||||
}) async {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
bool alsoDelete = false;
|
||||
|
||||
final action = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: const Text('Forget Device'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'This device will be unregistered and will no longer be linked '
|
||||
'to ${device.owner}. Are you sure?',
|
||||
),
|
||||
const SizedBox(height: Insets.md),
|
||||
CheckboxListTile(
|
||||
value: alsoDelete,
|
||||
onChanged: (value) =>
|
||||
setDialogState(() => alsoDelete = value ?? false),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: const Text('Permanently delete this device'),
|
||||
),
|
||||
if (alsoDelete) const _PermanentDeletionWarning(),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(foregroundColor: colorScheme.error),
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(alsoDelete ? 'Delete' : 'Forget'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (action != true || !context.mounted) return;
|
||||
|
||||
try {
|
||||
if (alsoDelete) {
|
||||
await BackendAPI.instance.deleteDevice(device.macAddress);
|
||||
if (!context.mounted) return;
|
||||
UISnackbars.showSuccess(context, 'Device deleted');
|
||||
(onDeleted ?? onRefresh)();
|
||||
} else {
|
||||
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,
|
||||
alsoDelete
|
||||
? 'Failed to delete device. ${dioErrorToUserMessage(e)}'
|
||||
: 'Failed to forget device. ${dioErrorToUserMessage(e)}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Confirms and performs the permanent deletion of a not-registered device,
|
||||
/// erasing the device record and all of its event history.
|
||||
///
|
||||
/// On success [onDeleted] is invoked when provided (e.g. to navigate away from
|
||||
/// a now-stale detail page); otherwise [onRefresh] is used.
|
||||
Future<void> confirmDeleteDevice(
|
||||
BuildContext context,
|
||||
Device device,
|
||||
VoidCallback onRefresh, {
|
||||
VoidCallback? onDeleted,
|
||||
}) async {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
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?',
|
||||
title: const Text('Delete Device'),
|
||||
content: const Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Are you sure you want to delete this device?'),
|
||||
SizedBox(height: Insets.md),
|
||||
_PermanentDeletionWarning(),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -27,8 +118,9 @@ Future<void> confirmForgetDevice(
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(foregroundColor: colorScheme.error),
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text('Forget', style: TextStyle(color: colorScheme.error)),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -37,15 +129,46 @@ Future<void> confirmForgetDevice(
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
|
||||
try {
|
||||
await BackendAPI.instance.forgetDevice(device.macAddress);
|
||||
await BackendAPI.instance.deleteDevice(device.macAddress);
|
||||
if (!context.mounted) return;
|
||||
UISnackbars.showSuccess(context, 'Device forgotten');
|
||||
onRefresh();
|
||||
UISnackbars.showSuccess(context, 'Device deleted');
|
||||
(onDeleted ?? onRefresh)();
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
UISnackbars.showError(
|
||||
context,
|
||||
'Failed to forget device. ${dioErrorToUserMessage(e)}',
|
||||
'Failed to delete device. ${dioErrorToUserMessage(e)}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Embedded warning shown when a permanent deletion is about to happen.
|
||||
class _PermanentDeletionWarning extends StatelessWidget {
|
||||
const _PermanentDeletionWarning();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(Insets.md),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(Insets.sm),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.warning_amber, color: colorScheme.onErrorContainer),
|
||||
const SizedBox(width: Insets.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'This permanently erases the device and its entire event '
|
||||
'history. This action cannot be undone.',
|
||||
style: TextStyle(color: colorScheme.onErrorContainer),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -136,7 +259,7 @@ Future<void> showEditDeviceDialog(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(onPressed: save, child: const Text('Save')),
|
||||
FilledButton(onPressed: save, child: const Text('Save')),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -240,7 +363,7 @@ Future<void> showRegisterDeviceDialog(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(onPressed: save, child: const Text('Save')),
|
||||
FilledButton(onPressed: save, child: const Text('Register')),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user