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) {
tabId: tree.latestTabId, // Find the container ID for the latest tab
rootId: tree.rootTabId, final containerForTab = availableTabs.value
totalTabs: tree.totalTabs, .where((t) => t.tabId == tree.latestTabId)
), .firstOrNull
?.containerId;
return TabTreeEntity(
tabId: tree.latestTabId,
containerId: containerForTab,
rootId: tree.rootTabId,
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,52 +529,55 @@ class TabViewHeader extends HookConsumerWidget {
), ),
), ),
), ),
if (switch (tabsViewMode) { Consumer(
TabsViewMode.list || TabsViewMode.grid => true, builder: (context, ref, child) {
TabsViewMode.tree => false, final selectedContainer = ref.watch(
}) selectedContainerDataProvider.select(
Consumer( (value) => value.value,
builder: (context, ref, child) { ),
final selectedContainer = ref.watch( );
selectedContainerDataProvider.select(
(value) => value.value,
),
);
return ContainerChips( return ContainerChips(
showGroupSuggestions: true, showGroupSuggestions: switch (tabsViewMode) {
selectedContainer: selectedContainer, TabsViewMode.list || TabsViewMode.grid => true,
onSelected: (container) async { TabsViewMode.tree => false,
if (container != null) { },
final result = await ref enableDragAndDrop: switch (tabsViewMode) {
.read(selectedContainerProvider.notifier) TabsViewMode.list || TabsViewMode.grid => true,
.setContainerId(container.id); TabsViewMode.tree => false,
},
selectedContainer: selectedContainer,
onSelected: (container) async {
if (container != null) {
final result = await ref
.read(selectedContainerProvider.notifier)
.setContainerId(container.id);
if (context.mounted && if (context.mounted &&
result == SetContainerResult.successHasProxy) { result == SetContainerResult.successHasProxy) {
await ref await ref
.read(startProxyControllerProvider.notifier) .read(startProxyControllerProvider.notifier)
.maybeStartProxy(context); .maybeStartProxy(context);
}
} else {
ref
.read(selectedContainerProvider.notifier)
.clearContainer();
} }
}, } else {
onDeleted: (container) {
ref ref
.read(selectedContainerProvider.notifier) .read(selectedContainerProvider.notifier)
.clearContainer(); .clearContainer();
}, }
onLongPress: (container) async { },
await ContainerEditRoute( onDeleted: (container) {
containerData: jsonEncode(container.toJson()), ref
).push(context); .read(selectedContainerProvider.notifier)
}, .clearContainer();
); },
}, onLongPress: (container) async {
), await ContainerEditRoute(
containerData: jsonEncode(container.toJson()),
).push(context);
},
);
},
),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
), ),
@@ -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,121 +190,59 @@ class ContainerChips extends HookConsumerWidget {
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
child: SelectableChips( child:
enableDelete: false, SelectableChips<
itemId: (container) => container.id, ContainerDataWithCount,
itemBackgroundColor: (container) => ContainerData,
ContainerColors.forChip(container.color), String
selectedBorderColor: Theme.of(context).colorScheme.primary, >(
itemLabel: (container) => enableDelete: false,
ContainerTitle(container: container), itemId: (container) => container.id,
itemBadgeCount: (container) => itemBackgroundColor: (container) =>
containerBadgeCount?.call(container) ?? ContainerColors.forChip(container.color),
container.tabCount, selectedBorderColor: Theme.of(
itemWrap: (child, container) { context,
return TabDragContainerTarget( ).colorScheme.primary,
container: container, itemLabel: (container) =>
child: child, ContainerTitle(container: container),
); itemBadgeCount: (container) =>
}, containerBadgeCount?.call(container) ??
prefixListItems: [ container.tabCount,
if (showUnassignedChip) itemWrap: enableDragAndDrop
TabDragContainerTarget( ? (child, container) {
container: null, return TabDragContainerTarget(
child: Consumer( container: container,
builder: (context, ref, child) { child: 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,
);
},
); );
}, }
error: (error, stackTrace) { : null,
logger.e( prefixListItems: [
'Error suggesting containers', if (showUnassignedChip)
error: error, enableDragAndDrop
stackTrace: stackTrace, ? TabDragContainerTarget(
); container: null,
return const SizedBox.shrink(); child: _UnassignedContainerChip(
}, containerBadgeCount: () =>
loading: () { containerBadgeCount?.call(null),
return const FilterChip( selectedContainer: selectedContainer,
avatar: Icon(MdiIcons.autoFix), onSelected: onSelected,
label: Skeletonizer(child: Text('0')), ),
showCheckmark: false, )
onSelected: null, : _UnassignedContainerChip(
); containerBadgeCount: () =>
}, containerBadgeCount?.call(null),
); selectedContainer: selectedContainer,
}, onSelected: onSelected,
), ),
], if (showGroupSuggestions)
availableItems: availableContainers, const _ContainerSuggestionsChip(),
selectedItem: selectedContainer, ],
onSelected: onSelected, availableItems: availableContainers,
onDeleted: onDeleted, selectedItem: selectedContainer,
onLongPress: onLongPress, onSelected: onSelected,
), onDeleted: onDeleted,
onLongPress: onLongPress,
),
), ),
if (displayMenu) if (displayMenu)
IconButton( IconButton(