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';
/// 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>(
List<BookmarkItem> children,
String guid,
@@ -117,7 +124,11 @@ class BookmarksSearch extends _$BookmarksSearch {
}
@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);
return bookmarksAsync.whenData((bookmarkNode) {
@@ -134,7 +145,22 @@ AsyncValue<T?> bookmarks<T extends BookmarkItem>(Ref ref, String entryGuid) {
}
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;
@@ -161,8 +187,8 @@ class SeamlessBookmarks extends _$SeamlessBookmarks {
}
@override
AsyncValue<BookmarkItem?> build(String entryGuid) {
final bookmarks = ref.watch(bookmarksProvider<BookmarkItem>(entryGuid));
AsyncValue<BookmarkItem?> build(String entryGuid, {bool hideEmptyRoots = false}) {
final bookmarks = ref.watch(bookmarksProvider<BookmarkItem>(entryGuid, hideEmptyRoots: hideEmptyRoots));
if (_hasSearch) {
final filterGuids = ref.watch(bookmarksSearchProvider);
@@ -61,7 +61,7 @@ final class BookmarksProvider<T extends BookmarkItem>
with $Provider<AsyncValue<T?>> {
BookmarksProvider._({
required BookmarksFamily super.from,
required String super.argument,
required (String, {bool hideEmptyRoots}) super.argument,
}) : super(
retry: null,
name: r'bookmarksProvider',
@@ -77,7 +77,7 @@ final class BookmarksProvider<T extends BookmarkItem>
String toString() {
return r'bookmarksProvider'
'<${T}>'
'($argument)';
'$argument';
}
@$internal
@@ -87,8 +87,12 @@ final class BookmarksProvider<T extends BookmarkItem>
@override
AsyncValue<T?> create(Ref ref) {
final argument = this.argument as String;
return bookmarks<T>(ref, argument);
final argument = this.argument as (String, {bool hideEmptyRoots});
return bookmarks<T>(
ref,
argument.$1,
hideEmptyRoots: argument.hideEmptyRoots,
);
}
$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 {
BookmarksFamily._()
@@ -128,15 +132,23 @@ final class BookmarksFamily extends $Family {
isAutoDispose: true,
);
BookmarksProvider<T> call<T extends BookmarkItem>(String entryGuid) =>
BookmarksProvider<T>._(argument: entryGuid, from: this);
BookmarksProvider<T> call<T extends BookmarkItem>(
String entryGuid, {
bool hideEmptyRoots = false,
}) => BookmarksProvider<T>._(
argument: (entryGuid, hideEmptyRoots: hideEmptyRoots),
from: this,
);
@override
String toString() => r'bookmarksProvider';
/// {@macro riverpod.override_with}
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,
) => $FamilyOverride(
from: this,
@@ -144,7 +156,7 @@ final class BookmarksFamily extends $Family {
final provider = pointer.origin as BookmarksProvider;
return provider._captureGenerics(<T extends BookmarkItem>() {
provider as BookmarksProvider<T>;
final argument = provider.argument as String;
final argument = provider.argument as (String, {bool hideEmptyRoots});
return provider
.$view(create: (ref) => create(ref, argument))
.$createElement(pointer);
@@ -160,7 +172,7 @@ final class SeamlessBookmarksProvider
extends $NotifierProvider<SeamlessBookmarks, AsyncValue<BookmarkItem?>> {
SeamlessBookmarksProvider._({
required SeamlessBookmarksFamily super.from,
required String super.argument,
required (String, {bool hideEmptyRoots}) super.argument,
}) : super(
retry: null,
name: r'seamlessBookmarksProvider',
@@ -176,7 +188,7 @@ final class SeamlessBookmarksProvider
String toString() {
return r'seamlessBookmarksProvider'
''
'($argument)';
'$argument';
}
@$internal
@@ -202,7 +214,7 @@ final class SeamlessBookmarksProvider
}
}
String _$seamlessBookmarksHash() => r'ddbc51ba22141a2c81bd9cf25adc610e5cdd72d9';
String _$seamlessBookmarksHash() => r'240b213fa8fe595781ccc608c5d551be54c31992';
final class SeamlessBookmarksFamily extends $Family
with
@@ -211,7 +223,7 @@ final class SeamlessBookmarksFamily extends $Family
AsyncValue<BookmarkItem?>,
AsyncValue<BookmarkItem?>,
AsyncValue<BookmarkItem?>,
String
(String, {bool hideEmptyRoots})
> {
SeamlessBookmarksFamily._()
: super(
@@ -222,8 +234,13 @@ final class SeamlessBookmarksFamily extends $Family
isAutoDispose: true,
);
SeamlessBookmarksProvider call(String entryGuid) =>
SeamlessBookmarksProvider._(argument: entryGuid, from: this);
SeamlessBookmarksProvider call(
String entryGuid, {
bool hideEmptyRoots = false,
}) => SeamlessBookmarksProvider._(
argument: (entryGuid, hideEmptyRoots: hideEmptyRoots),
from: this,
);
@override
String toString() => r'seamlessBookmarksProvider';
@@ -231,10 +248,14 @@ final class SeamlessBookmarksFamily extends $Family
abstract class _$SeamlessBookmarks
extends $Notifier<AsyncValue<BookmarkItem?>> {
late final _$args = ref.$arg as String;
String get entryGuid => _$args;
late final _$args = ref.$arg as (String, {bool hideEmptyRoots});
String get entryGuid => _$args.$1;
bool get hideEmptyRoots => _$args.hideEmptyRoots;
AsyncValue<BookmarkItem?> build(String entryGuid);
AsyncValue<BookmarkItem?> build(
String entryGuid, {
bool hideEmptyRoots = false,
});
@$mustCallSuper
@override
void runBuild() {
@@ -248,6 +269,9 @@ abstract class _$SeamlessBookmarks
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
Widget build(BuildContext context, WidgetRef ref) {
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 textFilterController = useTextEditingController();
useOnListenableChange(textFilterController, () {
if (ref.exists(seamlessBookmarksProvider(entryGuid))) {
if (ref.exists(seamlessBookmarksProvider(entryGuid, hideEmptyRoots: hideEmptyRoots.value))) {
ref
.read(seamlessBookmarksProvider(entryGuid).notifier)
.read(seamlessBookmarksProvider(entryGuid, hideEmptyRoots: hideEmptyRoots.value).notifier)
.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(
leadingIcon: const Icon(MdiIcons.import),
menuChildren: [