Hide Empty Root Folders by default

This commit is contained in:
Fabian Freund
2026-01-12 18:48:35 +01:00
parent 4b33ae67c5
commit 5e446f1b92
3 changed files with 89 additions and 26 deletions
@@ -27,6 +27,13 @@ import 'package:weblibre/features/geckoview/features/bookmarks/domain/repositori
part 'bookmarks.g.dart'; part 'bookmarks.g.dart';
/// Check if a root folder is effectively empty (has no non-root children)
bool _isEmptyRootFolder(BookmarkFolder folder) {
if (folder.children == null) return true;
// A root folder is empty if it has no children, or only contains other root folders
return folder.children!.every((child) => bookmarkRootIds.contains(child.guid));
}
T? _selectChildRecursive<T extends BookmarkItem>( T? _selectChildRecursive<T extends BookmarkItem>(
List<BookmarkItem> children, List<BookmarkItem> children,
String guid, String guid,
@@ -117,7 +124,11 @@ class BookmarksSearch extends _$BookmarksSearch {
} }
@Riverpod() @Riverpod()
AsyncValue<T?> bookmarks<T extends BookmarkItem>(Ref ref, String entryGuid) { AsyncValue<T?> bookmarks<T extends BookmarkItem>(
Ref ref,
String entryGuid, {
bool hideEmptyRoots = false,
}) {
final bookmarksAsync = ref.watch(bookmarksRepositoryProvider); final bookmarksAsync = ref.watch(bookmarksRepositoryProvider);
return bookmarksAsync.whenData((bookmarkNode) { return bookmarksAsync.whenData((bookmarkNode) {
@@ -134,7 +145,22 @@ AsyncValue<T?> bookmarks<T extends BookmarkItem>(Ref ref, String entryGuid) {
} }
if (selectedNode != null) { if (selectedNode != null) {
return _cloneAndFilterChildrenType<T>(selectedNode); var result = _cloneAndFilterChildrenType<T>(selectedNode);
// Filter empty root folders when viewing root level (excluding WebLibre root)
if (hideEmptyRoots &&
entryGuid == BookmarkRoot.root.id &&
result is BookmarkFolder) {
final filteredChildren = result.children
?.where((child) =>
child is! BookmarkFolder ||
child.guid == BookmarkRoot.mobile.id ||
!_isEmptyRootFolder(child))
.toList();
result = result.copyWith.children(filteredChildren) as T;
}
return result;
} }
return null; return null;
@@ -161,8 +187,8 @@ class SeamlessBookmarks extends _$SeamlessBookmarks {
} }
@override @override
AsyncValue<BookmarkItem?> build(String entryGuid) { AsyncValue<BookmarkItem?> build(String entryGuid, {bool hideEmptyRoots = false}) {
final bookmarks = ref.watch(bookmarksProvider<BookmarkItem>(entryGuid)); final bookmarks = ref.watch(bookmarksProvider<BookmarkItem>(entryGuid, hideEmptyRoots: hideEmptyRoots));
if (_hasSearch) { if (_hasSearch) {
final filterGuids = ref.watch(bookmarksSearchProvider); final filterGuids = ref.watch(bookmarksSearchProvider);
@@ -61,7 +61,7 @@ final class BookmarksProvider<T extends BookmarkItem>
with $Provider<AsyncValue<T?>> { with $Provider<AsyncValue<T?>> {
BookmarksProvider._({ BookmarksProvider._({
required BookmarksFamily super.from, required BookmarksFamily super.from,
required String super.argument, required (String, {bool hideEmptyRoots}) super.argument,
}) : super( }) : super(
retry: null, retry: null,
name: r'bookmarksProvider', name: r'bookmarksProvider',
@@ -77,7 +77,7 @@ final class BookmarksProvider<T extends BookmarkItem>
String toString() { String toString() {
return r'bookmarksProvider' return r'bookmarksProvider'
'<${T}>' '<${T}>'
'($argument)'; '$argument';
} }
@$internal @$internal
@@ -87,8 +87,12 @@ final class BookmarksProvider<T extends BookmarkItem>
@override @override
AsyncValue<T?> create(Ref ref) { AsyncValue<T?> create(Ref ref) {
final argument = this.argument as String; final argument = this.argument as (String, {bool hideEmptyRoots});
return bookmarks<T>(ref, argument); return bookmarks<T>(
ref,
argument.$1,
hideEmptyRoots: argument.hideEmptyRoots,
);
} }
$R _captureGenerics<$R>($R Function<T extends BookmarkItem>() cb) { $R _captureGenerics<$R>($R Function<T extends BookmarkItem>() cb) {
@@ -116,7 +120,7 @@ final class BookmarksProvider<T extends BookmarkItem>
} }
} }
String _$bookmarksHash() => r'dfa19aea04f352b8a6cefe35a0b283c9fddb214f'; String _$bookmarksHash() => r'72b54c4ff18cfdb60824a57607628be6b61d25d4';
final class BookmarksFamily extends $Family { final class BookmarksFamily extends $Family {
BookmarksFamily._() BookmarksFamily._()
@@ -128,15 +132,23 @@ final class BookmarksFamily extends $Family {
isAutoDispose: true, isAutoDispose: true,
); );
BookmarksProvider<T> call<T extends BookmarkItem>(String entryGuid) => BookmarksProvider<T> call<T extends BookmarkItem>(
BookmarksProvider<T>._(argument: entryGuid, from: this); String entryGuid, {
bool hideEmptyRoots = false,
}) => BookmarksProvider<T>._(
argument: (entryGuid, hideEmptyRoots: hideEmptyRoots),
from: this,
);
@override @override
String toString() => r'bookmarksProvider'; String toString() => r'bookmarksProvider';
/// {@macro riverpod.override_with} /// {@macro riverpod.override_with}
Override overrideWith( Override overrideWith(
AsyncValue<T?> Function<T extends BookmarkItem>(Ref ref, String args) AsyncValue<T?> Function<T extends BookmarkItem>(
Ref ref,
(String, {bool hideEmptyRoots}) args,
)
create, create,
) => $FamilyOverride( ) => $FamilyOverride(
from: this, from: this,
@@ -144,7 +156,7 @@ final class BookmarksFamily extends $Family {
final provider = pointer.origin as BookmarksProvider; final provider = pointer.origin as BookmarksProvider;
return provider._captureGenerics(<T extends BookmarkItem>() { return provider._captureGenerics(<T extends BookmarkItem>() {
provider as BookmarksProvider<T>; provider as BookmarksProvider<T>;
final argument = provider.argument as String; final argument = provider.argument as (String, {bool hideEmptyRoots});
return provider return provider
.$view(create: (ref) => create(ref, argument)) .$view(create: (ref) => create(ref, argument))
.$createElement(pointer); .$createElement(pointer);
@@ -160,7 +172,7 @@ final class SeamlessBookmarksProvider
extends $NotifierProvider<SeamlessBookmarks, AsyncValue<BookmarkItem?>> { extends $NotifierProvider<SeamlessBookmarks, AsyncValue<BookmarkItem?>> {
SeamlessBookmarksProvider._({ SeamlessBookmarksProvider._({
required SeamlessBookmarksFamily super.from, required SeamlessBookmarksFamily super.from,
required String super.argument, required (String, {bool hideEmptyRoots}) super.argument,
}) : super( }) : super(
retry: null, retry: null,
name: r'seamlessBookmarksProvider', name: r'seamlessBookmarksProvider',
@@ -176,7 +188,7 @@ final class SeamlessBookmarksProvider
String toString() { String toString() {
return r'seamlessBookmarksProvider' return r'seamlessBookmarksProvider'
'' ''
'($argument)'; '$argument';
} }
@$internal @$internal
@@ -202,7 +214,7 @@ final class SeamlessBookmarksProvider
} }
} }
String _$seamlessBookmarksHash() => r'ddbc51ba22141a2c81bd9cf25adc610e5cdd72d9'; String _$seamlessBookmarksHash() => r'240b213fa8fe595781ccc608c5d551be54c31992';
final class SeamlessBookmarksFamily extends $Family final class SeamlessBookmarksFamily extends $Family
with with
@@ -211,7 +223,7 @@ final class SeamlessBookmarksFamily extends $Family
AsyncValue<BookmarkItem?>, AsyncValue<BookmarkItem?>,
AsyncValue<BookmarkItem?>, AsyncValue<BookmarkItem?>,
AsyncValue<BookmarkItem?>, AsyncValue<BookmarkItem?>,
String (String, {bool hideEmptyRoots})
> { > {
SeamlessBookmarksFamily._() SeamlessBookmarksFamily._()
: super( : super(
@@ -222,8 +234,13 @@ final class SeamlessBookmarksFamily extends $Family
isAutoDispose: true, isAutoDispose: true,
); );
SeamlessBookmarksProvider call(String entryGuid) => SeamlessBookmarksProvider call(
SeamlessBookmarksProvider._(argument: entryGuid, from: this); String entryGuid, {
bool hideEmptyRoots = false,
}) => SeamlessBookmarksProvider._(
argument: (entryGuid, hideEmptyRoots: hideEmptyRoots),
from: this,
);
@override @override
String toString() => r'seamlessBookmarksProvider'; String toString() => r'seamlessBookmarksProvider';
@@ -231,10 +248,14 @@ final class SeamlessBookmarksFamily extends $Family
abstract class _$SeamlessBookmarks abstract class _$SeamlessBookmarks
extends $Notifier<AsyncValue<BookmarkItem?>> { extends $Notifier<AsyncValue<BookmarkItem?>> {
late final _$args = ref.$arg as String; late final _$args = ref.$arg as (String, {bool hideEmptyRoots});
String get entryGuid => _$args; String get entryGuid => _$args.$1;
bool get hideEmptyRoots => _$args.hideEmptyRoots;
AsyncValue<BookmarkItem?> build(String entryGuid); AsyncValue<BookmarkItem?> build(
String entryGuid, {
bool hideEmptyRoots = false,
});
@$mustCallSuper @$mustCallSuper
@override @override
void runBuild() { void runBuild() {
@@ -248,6 +269,9 @@ abstract class _$SeamlessBookmarks
Object?, Object?,
Object? Object?
>; >;
element.handleCreate(ref, () => build(_$args)); element.handleCreate(
ref,
() => build(_$args.$1, hideEmptyRoots: _$args.hideEmptyRoots),
);
} }
} }
@@ -48,15 +48,16 @@ class BookmarkListScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final treeKey = useMemoized(() => GlobalKey<TreeViewState>()); final treeKey = useMemoized(() => GlobalKey<TreeViewState>());
final bookmarkList = ref.watch(seamlessBookmarksProvider(entryGuid)); final hideEmptyRoots = useState(true);
final bookmarkList = ref.watch(seamlessBookmarksProvider(entryGuid, hideEmptyRoots: hideEmptyRoots.value));
final textFilterEnabled = useState(false); final textFilterEnabled = useState(false);
final textFilterController = useTextEditingController(); final textFilterController = useTextEditingController();
useOnListenableChange(textFilterController, () { useOnListenableChange(textFilterController, () {
if (ref.exists(seamlessBookmarksProvider(entryGuid))) { if (ref.exists(seamlessBookmarksProvider(entryGuid, hideEmptyRoots: hideEmptyRoots.value))) {
ref ref
.read(seamlessBookmarksProvider(entryGuid).notifier) .read(seamlessBookmarksProvider(entryGuid, hideEmptyRoots: hideEmptyRoots.value).notifier)
.search(textFilterController.text); .search(textFilterController.text);
} }
}); });
@@ -106,6 +107,18 @@ class BookmarkListScreen extends HookConsumerWidget {
}); });
}, },
), ),
if (entryGuid == BookmarkRoot.root.id)
MenuItemButton(
leadingIcon: Icon(hideEmptyRoots.value
? MdiIcons.eyeOff
: MdiIcons.eye),
child: Text(hideEmptyRoots.value
? 'Show Empty Root Folders'
: 'Hide Empty Root Folders'),
onPressed: () {
hideEmptyRoots.value = !hideEmptyRoots.value;
},
),
SubmenuButton( SubmenuButton(
leadingIcon: const Icon(MdiIcons.import), leadingIcon: const Icon(MdiIcons.import),
menuChildren: [ menuChildren: [