implement text filter; expand all nodes action;
This commit is contained in:
@@ -1,3 +1,7 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
|
import 'package:nullability/nullability.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart';
|
import 'package:weblibre/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart';
|
import 'package:weblibre/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart';
|
||||||
@@ -42,6 +46,57 @@ T _cloneAndFilterChildrenType<T extends BookmarkItem>(T node) {
|
|||||||
return node.clone() as T;
|
return node.clone() as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BookmarkItem? _cloneAndFilterOnGuids(BookmarkItem node, Set<String> guids) {
|
||||||
|
if (node is BookmarkFolder) {
|
||||||
|
if (node.children != null) {
|
||||||
|
final filtered = node.children
|
||||||
|
?.where((e) => e is BookmarkFolder || guids.contains(e.guid))
|
||||||
|
.map((e) => _cloneAndFilterOnGuids(e, guids))
|
||||||
|
.nonNulls
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (filtered.isNotEmpty) {
|
||||||
|
return node.copyWith.children(filtered);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guids.contains(node.guid)) {
|
||||||
|
return node.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
class BookmarksSearch extends _$BookmarksSearch {
|
||||||
|
final _service = GeckoBookmarksService();
|
||||||
|
late StreamController<Set<String>> _streamController;
|
||||||
|
|
||||||
|
Future<void> search(String query, {int limit = 10}) async {
|
||||||
|
if (query.isNotEmpty) {
|
||||||
|
await _service.searchBookmarks(query, limit: limit).then((value) {
|
||||||
|
if (!_streamController.isClosed) {
|
||||||
|
_streamController.add(value.map((e) => e.guid).toSet());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Set<String>> build() {
|
||||||
|
_streamController = StreamController();
|
||||||
|
|
||||||
|
ref.onDispose(() async {
|
||||||
|
await _streamController.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return _streamController.stream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
AsyncValue<T?> bookmarks<T extends BookmarkItem>(Ref ref, String entryGuid) {
|
AsyncValue<T?> bookmarks<T extends BookmarkItem>(Ref ref, String entryGuid) {
|
||||||
final bookmarksAsync = ref.watch(bookmarksRepositoryProvider);
|
final bookmarksAsync = ref.watch(bookmarksRepositoryProvider);
|
||||||
@@ -66,3 +121,45 @@ AsyncValue<T?> bookmarks<T extends BookmarkItem>(Ref ref, String entryGuid) {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
class SeamlessBookmarks extends _$SeamlessBookmarks {
|
||||||
|
bool _hasSearch = false;
|
||||||
|
|
||||||
|
void search(String input) {
|
||||||
|
if (input.isNotEmpty) {
|
||||||
|
if (!_hasSearch) {
|
||||||
|
_hasSearch = true;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Don't block
|
||||||
|
unawaited(ref.read(bookmarksSearchProvider.notifier).search(input));
|
||||||
|
} else if (_hasSearch) {
|
||||||
|
_hasSearch = false;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AsyncValue<BookmarkItem?> build(String entryGuid) {
|
||||||
|
final bookmarks = ref.watch(bookmarksProvider<BookmarkItem>(entryGuid));
|
||||||
|
|
||||||
|
if (_hasSearch) {
|
||||||
|
final filterGuids = ref.watch(bookmarksSearchProvider);
|
||||||
|
return bookmarks.map(
|
||||||
|
data: (node) =>
|
||||||
|
node.value.mapNotNull(
|
||||||
|
(node) => filterGuids.whenData(
|
||||||
|
(results) => _cloneAndFilterOnGuids(node, results),
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
const AsyncValue.data(null),
|
||||||
|
error: (e) => e,
|
||||||
|
loading: (s) => s,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return bookmarks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,51 @@ part of 'bookmarks.dart';
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
// ignore_for_file: type=lint, type=warning
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(BookmarksSearch)
|
||||||
|
const bookmarksSearchProvider = BookmarksSearchProvider._();
|
||||||
|
|
||||||
|
final class BookmarksSearchProvider
|
||||||
|
extends $StreamNotifierProvider<BookmarksSearch, Set<String>> {
|
||||||
|
const BookmarksSearchProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'bookmarksSearchProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$bookmarksSearchHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
BookmarksSearch create() => BookmarksSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$bookmarksSearchHash() => r'ac2ce51c4fd66c2c0e25bdf2475cf2fba57c9e0c';
|
||||||
|
|
||||||
|
abstract class _$BookmarksSearch extends $StreamNotifier<Set<String>> {
|
||||||
|
Stream<Set<String>> build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final created = build();
|
||||||
|
final ref = this.ref as $Ref<AsyncValue<Set<String>>, Set<String>>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<AsyncValue<Set<String>>, Set<String>>,
|
||||||
|
AsyncValue<Set<String>>,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleValue(ref, created);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ProviderFor(bookmarks)
|
@ProviderFor(bookmarks)
|
||||||
const bookmarksProvider = BookmarksFamily._();
|
const bookmarksProvider = BookmarksFamily._();
|
||||||
|
|
||||||
@@ -108,3 +153,103 @@ final class BookmarksFamily extends $Family {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ProviderFor(SeamlessBookmarks)
|
||||||
|
const seamlessBookmarksProvider = SeamlessBookmarksFamily._();
|
||||||
|
|
||||||
|
final class SeamlessBookmarksProvider
|
||||||
|
extends $NotifierProvider<SeamlessBookmarks, AsyncValue<BookmarkItem?>> {
|
||||||
|
const SeamlessBookmarksProvider._({
|
||||||
|
required SeamlessBookmarksFamily super.from,
|
||||||
|
required String super.argument,
|
||||||
|
}) : super(
|
||||||
|
retry: null,
|
||||||
|
name: r'seamlessBookmarksProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$seamlessBookmarksHash();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return r'seamlessBookmarksProvider'
|
||||||
|
''
|
||||||
|
'($argument)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
SeamlessBookmarks create() => SeamlessBookmarks();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(AsyncValue<BookmarkItem?> value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<AsyncValue<BookmarkItem?>>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is SeamlessBookmarksProvider && other.argument == argument;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return argument.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$seamlessBookmarksHash() => r'ddbc51ba22141a2c81bd9cf25adc610e5cdd72d9';
|
||||||
|
|
||||||
|
final class SeamlessBookmarksFamily extends $Family
|
||||||
|
with
|
||||||
|
$ClassFamilyOverride<
|
||||||
|
SeamlessBookmarks,
|
||||||
|
AsyncValue<BookmarkItem?>,
|
||||||
|
AsyncValue<BookmarkItem?>,
|
||||||
|
AsyncValue<BookmarkItem?>,
|
||||||
|
String
|
||||||
|
> {
|
||||||
|
const SeamlessBookmarksFamily._()
|
||||||
|
: super(
|
||||||
|
retry: null,
|
||||||
|
name: r'seamlessBookmarksProvider',
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
isAutoDispose: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
SeamlessBookmarksProvider call(String entryGuid) =>
|
||||||
|
SeamlessBookmarksProvider._(argument: entryGuid, from: this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => r'seamlessBookmarksProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$SeamlessBookmarks
|
||||||
|
extends $Notifier<AsyncValue<BookmarkItem?>> {
|
||||||
|
late final _$args = ref.$arg as String;
|
||||||
|
String get entryGuid => _$args;
|
||||||
|
|
||||||
|
AsyncValue<BookmarkItem?> build(String entryGuid);
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final created = build(_$args);
|
||||||
|
final ref =
|
||||||
|
this.ref as $Ref<AsyncValue<BookmarkItem?>, AsyncValue<BookmarkItem?>>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<AsyncValue<BookmarkItem?>, AsyncValue<BookmarkItem?>>,
|
||||||
|
AsyncValue<BookmarkItem?>,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleValue(ref, created);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+84
-4
@@ -9,6 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart';
|
import 'package:weblibre/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart';
|
import 'package:weblibre/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart';
|
||||||
|
import 'package:weblibre/presentation/hooks/listenable_callback.dart';
|
||||||
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||||
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
||||||
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||||
@@ -21,14 +22,82 @@ class BookmarkListScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final bookmarkList = ref.watch(bookmarksProvider<BookmarkItem>(entryGuid));
|
final treeKey = useMemoized(() => GlobalKey<TreeViewState>());
|
||||||
|
final bookmarkList = ref.watch(seamlessBookmarksProvider(entryGuid));
|
||||||
|
|
||||||
|
final textFilterEnabled = useState(false);
|
||||||
|
final textFilterController = useTextEditingController();
|
||||||
|
|
||||||
|
useListenableCallback(textFilterController, () {
|
||||||
|
if (ref.exists(seamlessBookmarksProvider(entryGuid))) {
|
||||||
|
ref
|
||||||
|
.read(seamlessBookmarksProvider(entryGuid).notifier)
|
||||||
|
.search(textFilterController.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Bookmarks')),
|
appBar: AppBar(
|
||||||
|
title: textFilterEnabled.value
|
||||||
|
? TextField(
|
||||||
|
controller: textFilterController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.only(top: 12),
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: 'Filter bookmarks...',
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (textFilterController.text.isNotEmpty) {
|
||||||
|
textFilterController.clear();
|
||||||
|
} else {
|
||||||
|
textFilterEnabled.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text('Bookmarks'),
|
||||||
|
actions: [
|
||||||
|
if (!textFilterEnabled.value)
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
textFilterEnabled.value = !textFilterEnabled.value;
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
),
|
||||||
|
MenuAnchor(
|
||||||
|
menuChildren: [
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: const Icon(MdiIcons.expandAll),
|
||||||
|
child: const Text('Expand All Folders'),
|
||||||
|
onPressed: () {
|
||||||
|
treeKey.currentState?.controller.expandAllChildren(
|
||||||
|
treeKey.currentState!.controller.tree,
|
||||||
|
recursive: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
builder: (context, controller, child) => IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (controller.isOpen) {
|
||||||
|
controller.close();
|
||||||
|
} else {
|
||||||
|
controller.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(MdiIcons.dotsVertical),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 12.0),
|
padding: const EdgeInsets.only(left: 12.0),
|
||||||
child: bookmarkList.when(
|
child: bookmarkList.when(
|
||||||
|
skipLoadingOnReload: true,
|
||||||
data: (list) {
|
data: (list) {
|
||||||
TreeNode<BookmarkItem> addChildren(
|
TreeNode<BookmarkItem> addChildren(
|
||||||
TreeNode<BookmarkItem>? parent,
|
TreeNode<BookmarkItem>? parent,
|
||||||
@@ -55,9 +124,14 @@ class BookmarkListScreen extends HookConsumerWidget {
|
|||||||
: TreeNode<BookmarkItem>.root();
|
: TreeNode<BookmarkItem>.root();
|
||||||
|
|
||||||
return TreeView.simple(
|
return TreeView.simple(
|
||||||
|
key: treeKey,
|
||||||
tree: root,
|
tree: root,
|
||||||
onTreeReady: (controller) {
|
onTreeReady: (controller) {
|
||||||
controller.expandNode(root);
|
if (textFilterEnabled.value) {
|
||||||
|
controller.expandAllChildren(root, recursive: true);
|
||||||
|
} else {
|
||||||
|
controller.expandNode(root);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
expansionIndicatorBuilder: (context, tree) =>
|
expansionIndicatorBuilder: (context, tree) =>
|
||||||
ChevronIndicator.upDown(
|
ChevronIndicator.upDown(
|
||||||
@@ -70,6 +144,7 @@ class BookmarkListScreen extends HookConsumerWidget {
|
|||||||
builder: (context, item) {
|
builder: (context, item) {
|
||||||
return switch (item.data) {
|
return switch (item.data) {
|
||||||
final BookmarkEntry bookmark => ListTile(
|
final BookmarkEntry bookmark => ListTile(
|
||||||
|
key: ValueKey(bookmark.guid),
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
leading: UrlIcon([bookmark.url], iconSize: 34.0),
|
leading: UrlIcon([bookmark.url], iconSize: 34.0),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
@@ -80,7 +155,11 @@ class BookmarkListScreen extends HookConsumerWidget {
|
|||||||
).push(context);
|
).push(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
title: Text(bookmark.title),
|
title: Text(
|
||||||
|
bookmark.title,
|
||||||
|
maxLines: 3,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
subtitle: UriBreadcrumb(uri: bookmark.url),
|
subtitle: UriBreadcrumb(uri: bookmark.url),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final result = await OpenSharedContentRoute(
|
final result = await OpenSharedContentRoute(
|
||||||
@@ -95,6 +174,7 @@ class BookmarkListScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
final BookmarkFolder folder => Padding(
|
final BookmarkFolder folder => Padding(
|
||||||
|
key: ValueKey(folder.guid),
|
||||||
padding: const EdgeInsets.only(right: 42.0),
|
padding: const EdgeInsets.only(right: 42.0),
|
||||||
child: HookBuilder(
|
child: HookBuilder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
|
|||||||
Reference in New Issue
Block a user