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.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() ??
[],
@@ -600,7 +600,7 @@ final class SeamlessFilteredTabEntitiesProvider
}
String _$seamlessFilteredTabEntitiesHash() =>
r'f2bf14737bc03f71fc61252359979b4e901e5df7';
r'590288855aa93162a8f70d37f2b3cd94eabc85e5';
final class SeamlessFilteredTabEntitiesFamily extends $Family
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/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,
),
);
@@ -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),
],
),
@@ -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<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/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(