diff --git a/app/lib/features/geckoview/domain/providers.g.dart b/app/lib/features/geckoview/domain/providers.g.dart index 632f5536..0334deb8 100644 --- a/app/lib/features/geckoview/domain/providers.g.dart +++ b/app/lib/features/geckoview/domain/providers.g.dart @@ -7,7 +7,7 @@ part of 'providers.dart'; // ************************************************************************** String _$selectionActionServiceHash() => - r'9393acfbc2cc0864d9e9b116e65e4c2713da2ab1'; + r'c45737a0a403bff1bdb1931b89701300f8dc3726'; /// See also [selectionActionService]. @ProviderFor(selectionActionService) diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart index ee86baa3..9adf630f 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart @@ -76,11 +76,11 @@ class _TabSheetHeader extends HookConsumerWidget { final container = ref.read(selectedContainerProvider); await ref .read(tabDataRepositoryProvider.notifier) - .closeAllTabs(container); + .closeAllTabsByContainer(container); onClose(); }, - icon: const Icon(Icons.delete), + icon: const Icon(MdiIcons.closeBoxMultiple), label: const Text('Close All'), ), ], diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_preview.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_preview.dart index f4f7c211..2da9f2c1 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_preview.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_preview.dart @@ -1,18 +1,23 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:nullability/nullability.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; +import 'package:weblibre/presentation/hooks/menu_controller.dart'; -class TabPreview extends StatelessWidget { +class TabPreview extends HookWidget { final TabState tab; final bool isActive; final VoidCallback? onTap; final VoidCallback? onDoubleTap; final VoidCallback? onDelete; + final void Function(String host)? onDeleteAll; const TabPreview({ required this.tab, @@ -20,12 +25,15 @@ class TabPreview extends StatelessWidget { this.onTap, this.onDoubleTap, this.onDelete, + this.onDeleteAll, super.key, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; + final extendedDeleteMenuController = useMenuController(); + return Container( decoration: BoxDecoration( border: Border.all( @@ -62,13 +70,37 @@ class TabPreview extends StatelessWidget { ), ), ), - IconButton( - visualDensity: const VisualDensity( - horizontal: -4.0, - vertical: -4.0, + MenuAnchor( + controller: extendedDeleteMenuController, + builder: (context, controller, child) { + return child!; + }, + menuChildren: [ + MenuItemButton( + onPressed: onDeleteAll.mapNotNull( + (p0) => () { + p0(tab.url.host); + }, + ), + leadingIcon: const Icon(MdiIcons.closeBoxMultiple), + child: Text('Close all from ${tab.url.host}'), + ), + ], + child: IconButton( + visualDensity: const VisualDensity( + horizontal: -4.0, + vertical: -4.0, + ), + onPressed: onDelete, + onLongPress: () { + if (extendedDeleteMenuController.isOpen) { + extendedDeleteMenuController.close(); + } else { + extendedDeleteMenuController.open(); + } + }, + icon: const Icon(Icons.close), ), - onPressed: onDelete, - icon: const Icon(Icons.close), ), ], ), @@ -166,6 +198,15 @@ class TabPreviewDraggable extends HookConsumerWidget { onClose(); } }, + onDeleteAll: (host) async { + final containerId = await ref + .read(tabDataRepositoryProvider.notifier) + .containerTabId(tab.id); + + await ref + .read(tabDataRepositoryProvider.notifier) + .closeAllTabsByHost(containerId, host); + }, // onDoubleTap: () { // ref.read(overlayDialogControllerProvider.notifier).show( // TabActionDialog( diff --git a/app/lib/features/geckoview/features/tabs/data/database/daos/container.dart b/app/lib/features/geckoview/features/tabs/data/database/daos/container.dart index e2aa0ae0..2f647e45 100644 --- a/app/lib/features/geckoview/features/tabs/data/database/daos/container.dart +++ b/app/lib/features/geckoview/features/tabs/data/database/daos/container.dart @@ -66,6 +66,20 @@ class ContainerDao extends DatabaseAccessor return query.map((row) => row.read(db.tab.id)!); } + Selectable getContainerTabsData(String? containerId) { + final query = + db.tab.select() + ..where( + (t) => + (containerId != null) + ? t.containerId.equals(containerId) + : t.containerId.isNull(), + ) + ..orderBy([(t) => OrderingTerm.asc(t.orderKey)]); + + return query; + } + SingleSelectable generateLeadingOrderKey( String? containerId, { int bucket = 0, diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/container.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/container.dart index d03c2dbb..34712058 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/container.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/container.dart @@ -43,7 +43,9 @@ class ContainerRepository extends _$ContainerRepository { } Future deleteContainer(String id) async { - await ref.read(tabDataRepositoryProvider.notifier).closeAllTabs(id); + await ref + .read(tabDataRepositoryProvider.notifier) + .closeAllTabsByContainer(id); return ref.read(tabDatabaseProvider).containerDao.deleteContainer(id); } diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart index d838e411..5cbcbc0e 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart @@ -7,7 +7,7 @@ part of 'container.dart'; // ************************************************************************** String _$containerRepositoryHash() => - r'9fff10b7896ea8865282bc315df21abde18b9d69'; + r'5344ecde33c1c80c1fa2bc8383faca181da91593'; /// See also [ContainerRepository]. @ProviderFor(ContainerRepository) diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart index 14b28ed2..c57ec469 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart @@ -20,7 +20,7 @@ class TabDataRepository extends _$TabDataRepository { .assignOrderKey(tabId, orderKey: orderKey); } - Future closeAllTabs(String? containerId) async { + Future closeAllTabsByContainer(String? containerId) async { final tabIds = await ref .read(tabDatabaseProvider) @@ -33,6 +33,25 @@ class TabDataRepository extends _$TabDataRepository { } } + Future closeAllTabsByHost(String? containerId, String host) async { + final tabs = + await ref + .read(tabDatabaseProvider) + .containerDao + .getContainerTabsData(containerId) + .get(); + + final filtered = + tabs + .where((tab) => tab.url?.host == host) + .map((tab) => tab.id) + .toList(); + + if (filtered.isNotEmpty) { + await ref.read(tabRepositoryProvider.notifier).closeTabs(filtered); + } + } + Future containerTabId(String tabId) { return ref .read(tabDatabaseProvider) diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart index 74317ea4..ac626651 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart @@ -6,7 +6,7 @@ part of 'tab.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabDataRepositoryHash() => r'b5193e58aa331c7d0dc374e01032fe3a44955c99'; +String _$tabDataRepositoryHash() => r'0de1de9f948e38cc0a6d2fc18a06b1327d41c7ca'; /// See also [TabDataRepository]. @ProviderFor(TabDataRepository) diff --git a/app/lib/features/web_feed/data/providers.dart b/app/lib/features/web_feed/data/providers.dart index f3c92f36..b5505864 100644 --- a/app/lib/features/web_feed/data/providers.dart +++ b/app/lib/features/web_feed/data/providers.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -8,6 +6,7 @@ import 'package:path_provider/path_provider.dart' as path_provider; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; +import 'package:universal_io/io.dart'; import 'package:weblibre/features/web_feed/data/database/database.dart'; part 'providers.g.dart';