container unsaved changes warning

This commit is contained in:
Fabian Freund
2026-04-20 14:23:23 +02:00
parent 14489cb375
commit a68c301f74
2 changed files with 283 additions and 189 deletions
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/material.dart';
enum DiscardChangesChoice { discard, save }
Future<DiscardChangesChoice?> showDiscardChangesDialog(BuildContext context) {
return showDialog<DiscardChangesChoice?>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
icon: const Icon(Icons.warning),
title: const Text('Unsaved Changes'),
content: const Text(
'You have unsaved changes. Do you want to save them before leaving?',
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.pop(context, DiscardChangesChoice.discard);
},
child: const Text('Discard'),
),
TextButton(
onPressed: () {
Navigator.pop(context, DiscardChangesChoice.save);
},
child: const Text('Save'),
),
],
);
},
);
}
@@ -29,6 +29,7 @@ import 'package:weblibre/features/geckoview/features/tabs/data/models/container_
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/controllers/container_topic.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/controllers/container_topic.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/dialogs/delete_container_dialog.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/dialogs/delete_container_dialog.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/dialogs/discard_changes_dialog.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_sites.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_sites.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart';
import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart'; import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart';
@@ -82,18 +83,11 @@ class ContainerEditScreen extends HookConsumerWidget {
final textController = useTextEditingController( final textController = useTextEditingController(
text: initialContainer.name, text: initialContainer.name,
); );
useListenable(textController);
return Scaffold( ContainerData buildContainer() {
appBar: AppBar(
title: Text(switch (_mode) {
_DialogMode.create => 'New Container',
_DialogMode.edit => 'Edit Container',
}),
actions: [
IconButton(
onPressed: () async {
final name = textController.text.trim(); final name = textController.text.trim();
final container = initialContainer.copyWith( return initialContainer.copyWith(
name: name.isNotEmpty ? name : null, name: name.isNotEmpty ? name : null,
color: selectedColor.value, color: selectedColor.value,
metadata: initialContainer.metadata.copyWith( metadata: initialContainer.metadata.copyWith(
@@ -104,7 +98,10 @@ class ContainerEditScreen extends HookConsumerWidget {
assignedSites: assignedSites.value, assignedSites: assignedSites.value,
), ),
); );
}
Future<ContainerData> saveContainer() async {
final container = buildContainer();
switch (_mode) { switch (_mode) {
case _DialogMode.create: case _DialogMode.create:
await ref await ref
@@ -115,7 +112,43 @@ class ContainerEditScreen extends HookConsumerWidget {
.read(containerRepositoryProvider.notifier) .read(containerRepositoryProvider.notifier)
.replaceContainer(container); .replaceContainer(container);
} }
return container;
}
final container = buildContainer();
//Empty copy to create comparable container with same type
final comparison = initialContainer.copyWith();
return PopScope(
canPop: container == comparison,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final choice = await showDiscardChangesDialog(context);
if (choice == null) return;
switch (choice) {
case DiscardChangesChoice.discard:
if (context.mounted) {
context.pop();
}
case DiscardChangesChoice.save:
final container = await saveContainer();
if (context.mounted) {
context.pop(container);
}
}
},
child: Scaffold(
appBar: AppBar(
title: Text(switch (_mode) {
_DialogMode.create => 'New Container',
_DialogMode.edit => 'Edit Container',
}),
actions: [
IconButton(
onPressed: () async {
final container = await saveContainer();
if (context.mounted) { if (context.mounted) {
context.pop(container); context.pop(container);
} }
@@ -203,7 +236,9 @@ class ContainerEditScreen extends HookConsumerWidget {
_DialogMode.create => (value) { _DialogMode.create => (value) {
if (value && contextualIdentity.value == null) { if (value && contextualIdentity.value == null) {
contextualIdentity.value = contextualIdentity.value =
initialContainer.metadata.contextualIdentity ?? initialContainer
.metadata
.contextualIdentity ??
uuid.v4(); uuid.v4();
} }
@@ -286,6 +321,7 @@ class ContainerEditScreen extends HookConsumerWidget {
), ),
), ),
), ),
),
); );
} }