diff --git a/app/lib/features/geckoview/features/browser/presentation/dialogs/bookmark_all_dialog.dart b/app/lib/features/geckoview/features/browser/presentation/dialogs/bookmark_all_dialog.dart new file mode 100644 index 00000000..7b2b9fd4 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/dialogs/bookmark_all_dialog.dart @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024-2025 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'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; + +enum BookmarkAllChoice { fast, detailed } + +/// Dialog to select between fast (automatic) or detailed (one-by-one) bookmark import. +Future showBookmarkAllDialog(BuildContext context) { + return showDialog( + context: context, + builder: (context) => SimpleDialog( + title: const Text('Bookmark All Tabs'), + children: [ + ListTile( + leading: const Icon(MdiIcons.fastForward), + title: const Text('Fast'), + subtitle: const Text( + 'Automatically add all tabs to a selected folder', + ), + onTap: () { + Navigator.of(context).pop(BookmarkAllChoice.fast); + }, + ), + ListTile( + leading: const Icon(MdiIcons.stepForward), + title: const Text('Detailed'), + subtitle: const Text('Review and edit each bookmark individually'), + onTap: () { + Navigator.of(context).pop(BookmarkAllChoice.detailed); + }, + ), + ], + ), + ); +} diff --git a/app/lib/features/geckoview/features/browser/presentation/dialogs/select_folder_dialog.dart b/app/lib/features/geckoview/features/browser/presentation/dialogs/select_folder_dialog.dart new file mode 100644 index 00000000..a92bf8b5 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/dialogs/select_folder_dialog.dart @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2024-2025 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'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/geckoview/features/bookmarks/presentation/widgets/folder_tree_picker.dart'; + +/// Dialog to select a bookmark folder. +/// Returns the selected folder GUID or null if cancelled. +Future showSelectFolderDialog(BuildContext context) { + return showDialog( + context: context, + builder: (context) => const _SelectFolderDialog(), + ); +} + +class _SelectFolderDialog extends HookConsumerWidget { + const _SelectFolderDialog(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedFolderGuid = useState(BookmarkRoot.mobile.id); + + return AlertDialog( + title: const Text('Select folder'), + content: SizedBox( + width: double.maxFinite, + child: SingleChildScrollView( + child: FolderTreePicker( + selectedFolderGuid: selectedFolderGuid, + entryGuid: BookmarkRoot.root.id, + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.of(context).pop(selectedFolderGuid.value), + child: const Text('Select'), + ), + ], + ); + } +} diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart index e37a7ea8..1bc2991d 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart @@ -10,8 +10,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/geckoview/domain/providers.dart'; import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; +import 'package:weblibre/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_data.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/bookmark_all_dialog.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/select_folder_dialog.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/dialogs/clear_container_data_dialog.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/dialogs/close_all_private_tabs_dialog.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/dialogs/close_all_tabs_dialog.dart'; @@ -252,6 +255,9 @@ class TabViewHeader extends HookConsumerWidget { leadingIcon: const Icon(MdiIcons.bookmarkPlusOutline), child: const Text('Bookmark all'), onPressed: () async { + final choice = await showBookmarkAllDialog(context); + if (choice == null || !context.mounted) return; + final containerId = ref.read( selectedContainerProvider, ); @@ -260,16 +266,44 @@ class TabViewHeader extends HookConsumerWidget { .read(tabDataRepositoryProvider.notifier) .getContainerTabsData(containerId); - for (final tab in tabData) { + if (choice == BookmarkAllChoice.fast) { + if (!context.mounted) return; + final folderGuid = await showSelectFolderDialog( + context, + ); + if (folderGuid == null) return; + + final repo = ref.read( + bookmarksRepositoryProvider.notifier, + ); + for (final tab in tabData) { + if (tab.url != null) { + await repo.addBookmark( + parentGuid: folderGuid, + url: tab.url!, + title: tab.title ?? tab.url.toString(), + ); + } + } + if (context.mounted) { - await BookmarkEntryAddRoute( - bookmarkInfo: jsonEncode( - BookmarkInfo( - title: tab.title, - url: tab.url.toString(), - ).encode(), - ), - ).push(context); + ui_helper.showInfoMessage( + context, + '${tabData.length} bookmark(s) added', + ); + } + } else { + for (final tab in tabData) { + if (context.mounted) { + await BookmarkEntryAddRoute( + bookmarkInfo: jsonEncode( + BookmarkInfo( + title: tab.title, + url: tab.url.toString(), + ).encode(), + ), + ).push(context); + } } } },