From 77657ffd2da909916726799e5f3dfae200f8d1dc Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Sun, 4 Jan 2026 10:26:57 +0100 Subject: [PATCH] container filter for tree view --- .../features/browser/domain/providers.dart | 19 +- .../features/browser/domain/providers.g.dart | 2 +- .../widgets/tab_view/tab_tree_view.dart | 5 +- .../widgets/tab_view/tab_view_header.dart | 83 +++--- .../tabs/data/entities/tab_entity.dart | 5 +- .../presentation/widgets/container_chips.dart | 261 ++++++++++-------- 6 files changed, 213 insertions(+), 162 deletions(-) diff --git a/app/lib/features/geckoview/features/browser/domain/providers.dart b/app/lib/features/geckoview/features/browser/domain/providers.dart index 349db2d0..604849ca 100644 --- a/app/lib/features/geckoview/features/browser/domain/providers.dart +++ b/app/lib/features/geckoview/features/browser/domain/providers.dart @@ -253,11 +253,20 @@ EquatableValue> seamlessFilteredTabEntities( (value) => EquatableValue( value.value ?.map( - (tree) => TabTreeEntity( - tabId: tree.latestTabId, - rootId: tree.rootTabId, - totalTabs: tree.totalTabs, - ), + (tree) { + // Find the container ID for the latest tab + final containerForTab = availableTabs.value + .where((t) => t.tabId == tree.latestTabId) + .firstOrNull + ?.containerId; + + return TabTreeEntity( + tabId: tree.latestTabId, + containerId: containerForTab, + rootId: tree.rootTabId, + totalTabs: tree.totalTabs, + ); + }, ) .toList() ?? [], diff --git a/app/lib/features/geckoview/features/browser/domain/providers.g.dart b/app/lib/features/geckoview/features/browser/domain/providers.g.dart index 10b2ae71..f77d3fe5 100644 --- a/app/lib/features/geckoview/features/browser/domain/providers.g.dart +++ b/app/lib/features/geckoview/features/browser/domain/providers.g.dart @@ -600,7 +600,7 @@ final class SeamlessFilteredTabEntitiesProvider } String _$seamlessFilteredTabEntitiesHash() => - r'f2bf14737bc03f71fc61252359979b4e901e5df7'; + r'590288855aa93162a8f70d37f2b3cd94eabc85e5'; final class SeamlessFilteredTabEntitiesFamily extends $Family with diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart index df4af09b..9e43d6ce 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart @@ -15,6 +15,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget import 'package:weblibre/features/geckoview/features/browser/utils/grid_calculations.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; @@ -176,12 +177,14 @@ class ViewTabTreesWidget extends HookConsumerWidget { builder: (context, ref, child) { final screenWidth = MediaQuery.of(context).size.width; + final containerId = ref.watch(selectedContainerProvider); + final filteredTabEntities = ref.watch( seamlessFilteredTabEntitiesProvider( searchPartition: TabSearchPartition.preview, // ignore: document_ignores using fast equatable // ignore: provider_parameters - containerFilter: ContainerFilterDisabled(), + containerFilter: ContainerFilterById(containerId: containerId), groupTrees: true, ), ); 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 b21f1109..a1ed7609 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 @@ -529,52 +529,55 @@ class TabViewHeader extends HookConsumerWidget { ), ), ), - if (switch (tabsViewMode) { - TabsViewMode.list || TabsViewMode.grid => true, - TabsViewMode.tree => false, - }) - Consumer( - builder: (context, ref, child) { - final selectedContainer = ref.watch( - selectedContainerDataProvider.select( - (value) => value.value, - ), - ); + Consumer( + builder: (context, ref, child) { + final selectedContainer = ref.watch( + selectedContainerDataProvider.select( + (value) => value.value, + ), + ); - return ContainerChips( - showGroupSuggestions: true, - selectedContainer: selectedContainer, - onSelected: (container) async { - if (container != null) { - final result = await ref - .read(selectedContainerProvider.notifier) - .setContainerId(container.id); + return ContainerChips( + showGroupSuggestions: switch (tabsViewMode) { + TabsViewMode.list || TabsViewMode.grid => true, + TabsViewMode.tree => false, + }, + enableDragAndDrop: switch (tabsViewMode) { + TabsViewMode.list || TabsViewMode.grid => true, + TabsViewMode.tree => false, + }, + selectedContainer: selectedContainer, + onSelected: (container) async { + if (container != null) { + final result = await ref + .read(selectedContainerProvider.notifier) + .setContainerId(container.id); - if (context.mounted && - result == SetContainerResult.successHasProxy) { - await ref - .read(startProxyControllerProvider.notifier) - .maybeStartProxy(context); - } - } else { - ref - .read(selectedContainerProvider.notifier) - .clearContainer(); + if (context.mounted && + result == SetContainerResult.successHasProxy) { + await ref + .read(startProxyControllerProvider.notifier) + .maybeStartProxy(context); } - }, - onDeleted: (container) { + } else { ref .read(selectedContainerProvider.notifier) .clearContainer(); - }, - onLongPress: (container) async { - await ContainerEditRoute( - containerData: jsonEncode(container.toJson()), - ).push(context); - }, - ); - }, - ), + } + }, + onDeleted: (container) { + ref + .read(selectedContainerProvider.notifier) + .clearContainer(); + }, + onLongPress: (container) async { + await ContainerEditRoute( + containerData: jsonEncode(container.toJson()), + ).push(context); + }, + ); + }, + ), const SizedBox(height: 8), ], ), diff --git a/app/lib/features/geckoview/features/tabs/data/entities/tab_entity.dart b/app/lib/features/geckoview/features/tabs/data/entities/tab_entity.dart index 5e13f609..ed4da444 100644 --- a/app/lib/features/geckoview/features/tabs/data/entities/tab_entity.dart +++ b/app/lib/features/geckoview/features/tabs/data/entities/tab_entity.dart @@ -55,16 +55,19 @@ class TabTreeEntity extends TabEntity { @override final String tabId; + final String? containerId; + final String rootId; final int totalTabs; TabTreeEntity({ required this.tabId, + required this.containerId, required this.rootId, required this.totalTabs, }); @override - List get hashParameters => [tabId, rootId, totalTabs]; + List get hashParameters => [tabId, containerId, rootId, totalTabs]; } diff --git a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart index eb0af0b6..e64ebb7a 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart @@ -37,10 +37,104 @@ import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/widgets/selectable_chips.dart'; +class _UnassignedContainerChip extends ConsumerWidget { + final int? Function()? containerBadgeCount; + final ContainerData? selectedContainer; + final void Function(ContainerDataWithCount?)? onSelected; + + const _UnassignedContainerChip({ + required this.containerBadgeCount, + required this.selectedContainer, + required this.onSelected, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final int tabCount = + containerBadgeCount?.call() ?? + ref.watch( + containerTabCountProvider( + // ignore: provider_parameters + ContainerFilterById(containerId: null), + ).select((value) => value.value ?? 0), + ); + + return FilterChip( + avatar: const Icon(MdiIcons.folderHidden), + labelPadding: (tabCount > 0) ? null : const EdgeInsets.only(right: 2.0), + label: (tabCount > 0) + ? Text(tabCount.toString()) + : const SizedBox.shrink(), + selected: selectedContainer == null, + showCheckmark: false, + onSelected: (value) { + if (value) { + onSelected?.call(null); + } + }, + ); + } +} + +class _ContainerSuggestionsChip extends ConsumerWidget { + const _ContainerSuggestionsChip(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabSuggestionsEnabled = ref.watch(tabSuggestionsControllerProvider); + final enableAiFeatures = ref.watch( + generalSettingsWithDefaultsProvider.select( + (settings) => settings.enableLocalAiFeatures, + ), + ); + + if (!enableAiFeatures || !tabSuggestionsEnabled) { + return const SizedBox.shrink(); + } + + final suggestions = ref.watch(suggestClustersProvider); + + return suggestions.when( + skipLoadingOnReload: true, + data: (data) { + if (data.isEmpty) { + return const SizedBox.shrink(); + } + + return FilterChip( + avatar: const Icon(MdiIcons.autoFix), + label: Text(data!.length.toString()), + showCheckmark: false, + onSelected: (_) async { + await const ContainerDraftRoute().push(context); + }, + ); + }, + error: (error, stackTrace) { + logger.e( + 'Error suggesting containers', + error: error, + stackTrace: stackTrace, + ); + return const SizedBox.shrink(); + }, + loading: () { + return const FilterChip( + avatar: Icon(MdiIcons.autoFix), + label: Skeletonizer(child: Text('0')), + showCheckmark: false, + onSelected: null, + ); + }, + ); + } +} + class ContainerChips extends HookConsumerWidget { final bool displayMenu; final bool showUnassignedChip; final bool showGroupSuggestions; + final bool enableDragAndDrop; final ContainerData? selectedContainer; final bool Function(ContainerDataWithCount)? containerFilter; @@ -62,6 +156,7 @@ class ContainerChips extends HookConsumerWidget { this.displayMenu = true, this.showUnassignedChip = true, this.showGroupSuggestions = false, + this.enableDragAndDrop = true, }); @override @@ -95,121 +190,59 @@ class ContainerChips extends HookConsumerWidget { child: Row( children: [ Expanded( - child: SelectableChips( - enableDelete: false, - itemId: (container) => container.id, - itemBackgroundColor: (container) => - ContainerColors.forChip(container.color), - selectedBorderColor: Theme.of(context).colorScheme.primary, - itemLabel: (container) => - ContainerTitle(container: container), - itemBadgeCount: (container) => - containerBadgeCount?.call(container) ?? - container.tabCount, - itemWrap: (child, container) { - return TabDragContainerTarget( - container: container, - child: child, - ); - }, - prefixListItems: [ - if (showUnassignedChip) - TabDragContainerTarget( - container: null, - child: Consumer( - builder: (context, ref, child) { - final int tabCount = - containerBadgeCount?.call(null) ?? - ref.watch( - containerTabCountProvider( - // ignore: provider_parameters - ContainerFilterById(containerId: null), - ).select((value) => value.value ?? 0), - ); - - return FilterChip( - avatar: const Icon(MdiIcons.folderHidden), - labelPadding: (tabCount > 0) - ? null - : const EdgeInsets.only(right: 2.0), - label: (tabCount > 0) - ? Text(tabCount.toString()) - : const SizedBox.shrink(), - selected: selectedContainer == null, - showCheckmark: false, - onSelected: (value) { - if (value) { - onSelected?.call(null); - } - }, - ); - }, - ), - ), - if (showGroupSuggestions) - Consumer( - builder: (context, ref, child) { - final tabSuggestionsEnabled = ref.watch( - tabSuggestionsControllerProvider, - ); - final enableAiFeatures = ref.watch( - generalSettingsWithDefaultsProvider.select( - (settings) => settings.enableLocalAiFeatures, - ), - ); - - if (!enableAiFeatures || !tabSuggestionsEnabled) { - return const SizedBox.shrink(); - } - - final suggestions = ref.watch( - suggestClustersProvider, - ); - - return suggestions.when( - skipLoadingOnReload: true, - data: (data) { - if (data.isEmpty) { - return const SizedBox.shrink(); - } - - return FilterChip( - avatar: const Icon(MdiIcons.autoFix), - label: Text(data!.length.toString()), - showCheckmark: false, - onSelected: (_) async { - await const ContainerDraftRoute().push( - context, - ); - }, + child: + SelectableChips< + ContainerDataWithCount, + ContainerData, + String + >( + enableDelete: false, + itemId: (container) => container.id, + itemBackgroundColor: (container) => + ContainerColors.forChip(container.color), + selectedBorderColor: Theme.of( + context, + ).colorScheme.primary, + itemLabel: (container) => + ContainerTitle(container: container), + itemBadgeCount: (container) => + containerBadgeCount?.call(container) ?? + container.tabCount, + itemWrap: enableDragAndDrop + ? (child, container) { + return TabDragContainerTarget( + container: container, + child: child, ); - }, - error: (error, stackTrace) { - logger.e( - 'Error suggesting containers', - error: error, - stackTrace: stackTrace, - ); - return const SizedBox.shrink(); - }, - loading: () { - return const FilterChip( - avatar: Icon(MdiIcons.autoFix), - label: Skeletonizer(child: Text('0')), - showCheckmark: false, - onSelected: null, - ); - }, - ); - }, - ), - ], - availableItems: availableContainers, - selectedItem: selectedContainer, - onSelected: onSelected, - onDeleted: onDeleted, - onLongPress: onLongPress, - ), + } + : null, + prefixListItems: [ + if (showUnassignedChip) + enableDragAndDrop + ? TabDragContainerTarget( + container: null, + child: _UnassignedContainerChip( + containerBadgeCount: () => + containerBadgeCount?.call(null), + selectedContainer: selectedContainer, + onSelected: onSelected, + ), + ) + : _UnassignedContainerChip( + containerBadgeCount: () => + containerBadgeCount?.call(null), + selectedContainer: selectedContainer, + onSelected: onSelected, + ), + if (showGroupSuggestions) + const _ContainerSuggestionsChip(), + ], + availableItems: availableContainers, + selectedItem: selectedContainer, + onSelected: onSelected, + onDeleted: onDeleted, + onLongPress: onLongPress, + ), ), if (displayMenu) IconButton(