diff --git a/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart b/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart index 9bf61deb..5473fda8 100644 --- a/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart +++ b/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart @@ -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( List children, String guid, @@ -117,7 +124,11 @@ class BookmarksSearch extends _$BookmarksSearch { } @Riverpod() -AsyncValue bookmarks(Ref ref, String entryGuid) { +AsyncValue bookmarks( + Ref ref, + String entryGuid, { + bool hideEmptyRoots = false, +}) { final bookmarksAsync = ref.watch(bookmarksRepositoryProvider); return bookmarksAsync.whenData((bookmarkNode) { @@ -134,7 +145,22 @@ AsyncValue bookmarks(Ref ref, String entryGuid) { } if (selectedNode != null) { - return _cloneAndFilterChildrenType(selectedNode); + var result = _cloneAndFilterChildrenType(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 build(String entryGuid) { - final bookmarks = ref.watch(bookmarksProvider(entryGuid)); + AsyncValue build(String entryGuid, {bool hideEmptyRoots = false}) { + final bookmarks = ref.watch(bookmarksProvider(entryGuid, hideEmptyRoots: hideEmptyRoots)); if (_hasSearch) { final filterGuids = ref.watch(bookmarksSearchProvider); diff --git a/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.g.dart b/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.g.dart index 150febbd..e1a2f409 100644 --- a/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.g.dart +++ b/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.g.dart @@ -61,7 +61,7 @@ final class BookmarksProvider with $Provider> { 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 String toString() { return r'bookmarksProvider' '<${T}>' - '($argument)'; + '$argument'; } @$internal @@ -87,8 +87,12 @@ final class BookmarksProvider @override AsyncValue create(Ref ref) { - final argument = this.argument as String; - return bookmarks(ref, argument); + final argument = this.argument as (String, {bool hideEmptyRoots}); + return bookmarks( + ref, + argument.$1, + hideEmptyRoots: argument.hideEmptyRoots, + ); } $R _captureGenerics<$R>($R Function() cb) { @@ -116,7 +120,7 @@ final class BookmarksProvider } } -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 call(String entryGuid) => - BookmarksProvider._(argument: entryGuid, from: this); + BookmarksProvider call( + String entryGuid, { + bool hideEmptyRoots = false, + }) => BookmarksProvider._( + argument: (entryGuid, hideEmptyRoots: hideEmptyRoots), + from: this, + ); @override String toString() => r'bookmarksProvider'; /// {@macro riverpod.override_with} Override overrideWith( - AsyncValue Function(Ref ref, String args) + AsyncValue Function( + 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(() { provider as BookmarksProvider; - 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> { 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, AsyncValue, AsyncValue, - 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> { - 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 build(String entryGuid); + AsyncValue 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), + ); } } diff --git a/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart b/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart index 5829be97..71f9600c 100644 --- a/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart +++ b/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart @@ -48,15 +48,16 @@ class BookmarkListScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final treeKey = useMemoized(() => GlobalKey()); - 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: [