diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/presentation/dialogs/discard_changes_dialog.dart b/apps/weblibre/lib/features/geckoview/features/tabs/presentation/dialogs/discard_changes_dialog.dart new file mode 100644 index 00000000..2c9a090d --- /dev/null +++ b/apps/weblibre/lib/features/geckoview/features/tabs/presentation/dialogs/discard_changes_dialog.dart @@ -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 . + */ + +import 'package:flutter/material.dart'; + +enum DiscardChangesChoice { discard, save } + +Future showDiscardChangesDialog(BuildContext context) { + return showDialog( + 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: [ + 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'), + ), + ], + ); + }, + ); +} diff --git a/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart b/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart index a7c4a5e3..00bfc1af 100644 --- a/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart +++ b/apps/weblibre/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart @@ -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/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/discard_changes_dialog.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/utils/container_colors.dart'; @@ -82,207 +83,242 @@ class ContainerEditScreen extends HookConsumerWidget { final textController = useTextEditingController( text: initialContainer.name, ); + useListenable(textController); - return Scaffold( - appBar: AppBar( - title: Text(switch (_mode) { - _DialogMode.create => 'New Container', - _DialogMode.edit => 'Edit Container', - }), - actions: [ - IconButton( - onPressed: () async { - final name = textController.text.trim(); - final container = initialContainer.copyWith( - name: name.isNotEmpty ? name : null, - color: selectedColor.value, - metadata: initialContainer.metadata.copyWith( - contextualIdentity: contextualIdentity.value, - useProxy: useProxy.value && contextualIdentity.value != null, - clearDataOnExit: - clearDataOnExit.value && contextualIdentity.value != null, - assignedSites: assignedSites.value, - ), - ); + ContainerData buildContainer() { + final name = textController.text.trim(); + return initialContainer.copyWith( + name: name.isNotEmpty ? name : null, + color: selectedColor.value, + metadata: initialContainer.metadata.copyWith( + contextualIdentity: contextualIdentity.value, + useProxy: useProxy.value && contextualIdentity.value != null, + clearDataOnExit: + clearDataOnExit.value && contextualIdentity.value != null, + assignedSites: assignedSites.value, + ), + ); + } - switch (_mode) { - case _DialogMode.create: - await ref - .read(containerRepositoryProvider.notifier) - .addContainer(container); - case _DialogMode.edit: - await ref - .read(containerRepositoryProvider.notifier) - .replaceContainer(container); - } + Future saveContainer() async { + final container = buildContainer(); + switch (_mode) { + case _DialogMode.create: + await ref + .read(containerRepositoryProvider.notifier) + .addContainer(container); + case _DialogMode.edit: + await ref + .read(containerRepositoryProvider.notifier) + .replaceContainer(container); + } + return container; + } - if (context.mounted) { - context.pop(container); - } - }, - icon: const Icon(Icons.check), - ), - ], - ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - Expanded( - child: ListView( - children: [ - TextField( - decoration: InputDecoration( - prefixIcon: Padding( - padding: const EdgeInsets.all(10.0), - child: AnimatedContainer( - duration: disableAnimations - ? Duration.zero - : const Duration(milliseconds: 300), - height: 24, - width: 24, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: ContainerColors.preview( - selectedColor.value, + 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) { + context.pop(container); + } + }, + icon: const Icon(Icons.check), + ), + ], + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + Expanded( + child: ListView( + children: [ + TextField( + decoration: InputDecoration( + prefixIcon: Padding( + padding: const EdgeInsets.all(10.0), + child: AnimatedContainer( + duration: disableAnimations + ? Duration.zero + : const Duration(milliseconds: 300), + height: 24, + width: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: ContainerColors.preview( + selectedColor.value, + ), ), ), ), - ), - label: const Text('Name'), - suffixIcon: _buildMagicWandButton( - context, - ref, - textController, - ), - ), - controller: textController, - ), - TextButton.icon( - label: const Text('Select Color'), - icon: const Icon(Icons.colorize), - onPressed: () async { - final color = await showDialog( - context: context, - builder: (context) => - ColorPickerDialog(selectedColor.value), - ); - - if (color != null) { - selectedColor.value = color; - } - }, - ), - SwitchListTile.adaptive( - value: contextualIdentity.value != null, - title: const Text('Cookie Isolation'), - secondary: const Icon(MdiIcons.cookieLock), - contentPadding: EdgeInsets.zero, - onChanged: (_mode == _DialogMode.create) - ? (value) { - contextualIdentity.value = value - ? initialContainer - .metadata - .contextualIdentity ?? - uuid.v4() - : null; - - if (!value && useProxy.value) { - useProxy.value = false; - } - } - : null, - ), - SwitchListTile.adaptive( - value: useProxy.value, - title: const Text('Use Torâ„¢ Proxy'), - secondary: const Icon(TorIcons.onionAlt), - contentPadding: EdgeInsets.zero, - onChanged: switch (_mode) { - _DialogMode.create => (value) { - if (value && contextualIdentity.value == null) { - contextualIdentity.value = - initialContainer.metadata.contextualIdentity ?? - uuid.v4(); - } - - useProxy.value = value; - }, - _DialogMode.edit => - (contextualIdentity.value != null) - ? (value) { - useProxy.value = value; - } - : null, - }, - ), - SwitchListTile.adaptive( - value: clearDataOnExit.value, - title: const Text('Clear Data on Exit'), - subtitle: const Text( - 'Clear cookies and site data when app closes', - ), - secondary: const Icon(MdiIcons.databaseRemove), - contentPadding: EdgeInsets.zero, - onChanged: (contextualIdentity.value != null) - ? (value) { - clearDataOnExit.value = value; - } - : null, - ), - ListTile( - leading: const Icon(Icons.web), - title: const Text('Assigned Sites'), - trailing: const Icon(Icons.chevron_right), - contentPadding: EdgeInsets.zero, - onTap: () async { - final result = await showDialog>( - context: context, - builder: (context) => ContainerSitesScreen( - initialSites: assignedSites.value?.toSet() ?? {}, + label: const Text('Name'), + suffixIcon: _buildMagicWandButton( + context, + ref, + textController, ), - ); - - if (result.isEmpty) { - assignedSites.value = null; - } else { - assignedSites.value = result!.toList(); - } - }, - ), - ], - ), - ), - if (_mode == _DialogMode.edit) - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( - side: BorderSide( - color: Theme.of(context).colorScheme.error, + ), + controller: textController, ), - foregroundColor: Theme.of(context).colorScheme.error, - iconColor: Theme.of(context).colorScheme.error, - ), - label: const Text('Delete'), - icon: const Icon(Icons.delete), - onPressed: () async { - final result = await showDeleteContainerDialog(context); + TextButton.icon( + label: const Text('Select Color'), + icon: const Icon(Icons.colorize), + onPressed: () async { + final color = await showDialog( + context: context, + builder: (context) => + ColorPickerDialog(selectedColor.value), + ); - if (result == true) { - await ref - .read(containerRepositoryProvider.notifier) - .deleteContainer(initialContainer.id); + if (color != null) { + selectedColor.value = color; + } + }, + ), + SwitchListTile.adaptive( + value: contextualIdentity.value != null, + title: const Text('Cookie Isolation'), + secondary: const Icon(MdiIcons.cookieLock), + contentPadding: EdgeInsets.zero, + onChanged: (_mode == _DialogMode.create) + ? (value) { + contextualIdentity.value = value + ? initialContainer + .metadata + .contextualIdentity ?? + uuid.v4() + : null; - if (context.mounted) { - context.pop(); - } - } - }, + if (!value && useProxy.value) { + useProxy.value = false; + } + } + : null, + ), + SwitchListTile.adaptive( + value: useProxy.value, + title: const Text('Use Torâ„¢ Proxy'), + secondary: const Icon(TorIcons.onionAlt), + contentPadding: EdgeInsets.zero, + onChanged: switch (_mode) { + _DialogMode.create => (value) { + if (value && contextualIdentity.value == null) { + contextualIdentity.value = + initialContainer + .metadata + .contextualIdentity ?? + uuid.v4(); + } + + useProxy.value = value; + }, + _DialogMode.edit => + (contextualIdentity.value != null) + ? (value) { + useProxy.value = value; + } + : null, + }, + ), + SwitchListTile.adaptive( + value: clearDataOnExit.value, + title: const Text('Clear Data on Exit'), + subtitle: const Text( + 'Clear cookies and site data when app closes', + ), + secondary: const Icon(MdiIcons.databaseRemove), + contentPadding: EdgeInsets.zero, + onChanged: (contextualIdentity.value != null) + ? (value) { + clearDataOnExit.value = value; + } + : null, + ), + ListTile( + leading: const Icon(Icons.web), + title: const Text('Assigned Sites'), + trailing: const Icon(Icons.chevron_right), + contentPadding: EdgeInsets.zero, + onTap: () async { + final result = await showDialog>( + context: context, + builder: (context) => ContainerSitesScreen( + initialSites: assignedSites.value?.toSet() ?? {}, + ), + ); + + if (result.isEmpty) { + assignedSites.value = null; + } else { + assignedSites.value = result!.toList(); + } + }, + ), + ], ), ), - ], + if (_mode == _DialogMode.edit) + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + side: BorderSide( + color: Theme.of(context).colorScheme.error, + ), + foregroundColor: Theme.of(context).colorScheme.error, + iconColor: Theme.of(context).colorScheme.error, + ), + label: const Text('Delete'), + icon: const Icon(Icons.delete), + onPressed: () async { + final result = await showDeleteContainerDialog(context); + + if (result == true) { + await ref + .read(containerRepositoryProvider.notifier) + .deleteContainer(initialContainer.id); + + if (context.mounted) { + context.pop(); + } + } + }, + ), + ), + ], + ), ), ), ),