container filter for tree view

This commit is contained in:
Fabian Freund
2026-01-04 10:26:57 +01:00
parent a99051235b
commit 77657ffd2d
6 changed files with 213 additions and 162 deletions
@@ -253,11 +253,20 @@ EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
(value) => EquatableValue( (value) => EquatableValue(
value.value value.value
?.map( ?.map(
(tree) => TabTreeEntity( (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, tabId: tree.latestTabId,
containerId: containerForTab,
rootId: tree.rootTabId, rootId: tree.rootTabId,
totalTabs: tree.totalTabs, totalTabs: tree.totalTabs,
), );
},
) )
.toList() ?? .toList() ??
[], [],
@@ -600,7 +600,7 @@ final class SeamlessFilteredTabEntitiesProvider
} }
String _$seamlessFilteredTabEntitiesHash() => String _$seamlessFilteredTabEntitiesHash() =>
r'f2bf14737bc03f71fc61252359979b4e901e5df7'; r'590288855aa93162a8f70d37f2b3cd94eabc85e5';
final class SeamlessFilteredTabEntitiesFamily extends $Family final class SeamlessFilteredTabEntitiesFamily extends $Family
with with
@@ -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/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/container_filter.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.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.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
@@ -176,12 +177,14 @@ class ViewTabTreesWidget extends HookConsumerWidget {
builder: (context, ref, child) { builder: (context, ref, child) {
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final containerId = ref.watch(selectedContainerProvider);
final filteredTabEntities = ref.watch( final filteredTabEntities = ref.watch(
seamlessFilteredTabEntitiesProvider( seamlessFilteredTabEntitiesProvider(
searchPartition: TabSearchPartition.preview, searchPartition: TabSearchPartition.preview,
// ignore: document_ignores using fast equatable // ignore: document_ignores using fast equatable
// ignore: provider_parameters // ignore: provider_parameters
containerFilter: ContainerFilterDisabled(), containerFilter: ContainerFilterById(containerId: containerId),
groupTrees: true, groupTrees: true,
), ),
); );
@@ -529,10 +529,6 @@ class TabViewHeader extends HookConsumerWidget {
), ),
), ),
), ),
if (switch (tabsViewMode) {
TabsViewMode.list || TabsViewMode.grid => true,
TabsViewMode.tree => false,
})
Consumer( Consumer(
builder: (context, ref, child) { builder: (context, ref, child) {
final selectedContainer = ref.watch( final selectedContainer = ref.watch(
@@ -542,7 +538,14 @@ class TabViewHeader extends HookConsumerWidget {
); );
return ContainerChips( return ContainerChips(
showGroupSuggestions: true, showGroupSuggestions: switch (tabsViewMode) {
TabsViewMode.list || TabsViewMode.grid => true,
TabsViewMode.tree => false,
},
enableDragAndDrop: switch (tabsViewMode) {
TabsViewMode.list || TabsViewMode.grid => true,
TabsViewMode.tree => false,
},
selectedContainer: selectedContainer, selectedContainer: selectedContainer,
onSelected: (container) async { onSelected: (container) async {
if (container != null) { if (container != null) {
@@ -55,16 +55,19 @@ class TabTreeEntity extends TabEntity {
@override @override
final String tabId; final String tabId;
final String? containerId;
final String rootId; final String rootId;
final int totalTabs; final int totalTabs;
TabTreeEntity({ TabTreeEntity({
required this.tabId, required this.tabId,
required this.containerId,
required this.rootId, required this.rootId,
required this.totalTabs, required this.totalTabs,
}); });
@override @override
List<Object?> get hashParameters => [tabId, rootId, totalTabs]; List<Object?> get hashParameters => [tabId, containerId, rootId, totalTabs];
} }
@@ -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/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/widgets/selectable_chips.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 { class ContainerChips extends HookConsumerWidget {
final bool displayMenu; final bool displayMenu;
final bool showUnassignedChip; final bool showUnassignedChip;
final bool showGroupSuggestions; final bool showGroupSuggestions;
final bool enableDragAndDrop;
final ContainerData? selectedContainer; final ContainerData? selectedContainer;
final bool Function(ContainerDataWithCount)? containerFilter; final bool Function(ContainerDataWithCount)? containerFilter;
@@ -62,6 +156,7 @@ class ContainerChips extends HookConsumerWidget {
this.displayMenu = true, this.displayMenu = true,
this.showUnassignedChip = true, this.showUnassignedChip = true,
this.showGroupSuggestions = false, this.showGroupSuggestions = false,
this.enableDragAndDrop = true,
}); });
@override @override
@@ -95,114 +190,52 @@ class ContainerChips extends HookConsumerWidget {
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: SelectableChips( child:
SelectableChips<
ContainerDataWithCount,
ContainerData,
String
>(
enableDelete: false, enableDelete: false,
itemId: (container) => container.id, itemId: (container) => container.id,
itemBackgroundColor: (container) => itemBackgroundColor: (container) =>
ContainerColors.forChip(container.color), ContainerColors.forChip(container.color),
selectedBorderColor: Theme.of(context).colorScheme.primary, selectedBorderColor: Theme.of(
context,
).colorScheme.primary,
itemLabel: (container) => itemLabel: (container) =>
ContainerTitle(container: container), ContainerTitle(container: container),
itemBadgeCount: (container) => itemBadgeCount: (container) =>
containerBadgeCount?.call(container) ?? containerBadgeCount?.call(container) ??
container.tabCount, container.tabCount,
itemWrap: (child, container) { itemWrap: enableDragAndDrop
? (child, container) {
return TabDragContainerTarget( return TabDragContainerTarget(
container: container, container: container,
child: child, child: child,
); );
}, }
: null,
prefixListItems: [ prefixListItems: [
if (showUnassignedChip) if (showUnassignedChip)
TabDragContainerTarget( enableDragAndDrop
? TabDragContainerTarget(
container: null, container: null,
child: Consumer( child: _UnassignedContainerChip(
builder: (context, ref, child) { containerBadgeCount: () =>
final int tabCount = containerBadgeCount?.call(null),
containerBadgeCount?.call(null) ?? selectedContainer: selectedContainer,
ref.watch( onSelected: onSelected,
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);
}
},
);
},
), ),
)
: _UnassignedContainerChip(
containerBadgeCount: () =>
containerBadgeCount?.call(null),
selectedContainer: selectedContainer,
onSelected: onSelected,
), ),
if (showGroupSuggestions) if (showGroupSuggestions)
Consumer( const _ContainerSuggestionsChip(),
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,
);
},
);
},
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, availableItems: availableContainers,
selectedItem: selectedContainer, selectedItem: selectedContainer,