diff --git a/app/lib/core/routing/routes.browser.dart b/app/lib/core/routing/routes.browser.dart index d143f198..d5c7b0ac 100644 --- a/app/lib/core/routing/routes.browser.dart +++ b/app/lib/core/routing/routes.browser.dart @@ -79,6 +79,26 @@ part of 'routes.dart'; ), ], ), + TypedGoRoute( + name: 'BookmarkListRoute', + path: 'bookmarks/:entryGuid', + ), + TypedGoRoute( + name: 'BookmarkFolderAddRoute', + path: 'createFolder', + ), + TypedGoRoute( + name: 'BookmarkFolderEditRoute', + path: 'editFolder', + ), + TypedGoRoute( + name: 'BookmarkEntryAddRoute', + path: 'createEntry', + ), + TypedGoRoute( + name: 'BookmarkEntryEditRoute', + path: 'editEntry', + ), ], ) class BrowserRoute extends GoRouteData with $BrowserRoute { @@ -282,3 +302,71 @@ class EditProfileRoute extends GoRouteData with $EditProfileRoute { ); } } + +class BookmarkListRoute extends GoRouteData with $BookmarkListRoute { + final String entryGuid; + + const BookmarkListRoute({required this.entryGuid}); + + @override + Widget build(BuildContext context, GoRouterState state) { + return BookmarkListScreen(entryGuid: entryGuid); + } +} + +class BookmarkFolderAddRoute extends GoRouteData with $BookmarkFolderAddRoute { + final String? parentGuid; + + const BookmarkFolderAddRoute({required this.parentGuid}); + + @override + Widget build(BuildContext context, GoRouterState state) { + return BookmarkFolderEditScreen(parentGuid: parentGuid, folder: null); + } +} + +class BookmarkFolderEditRoute extends GoRouteData + with $BookmarkFolderEditRoute { + final String folder; + + const BookmarkFolderEditRoute({required this.folder}); + + @override + Widget build(BuildContext context, GoRouterState state) { + return BookmarkFolderEditScreen( + folder: BookmarkFolder.fromJson( + jsonDecode(folder) as Map, + ), + ); + } +} + +class BookmarkEntryAddRoute extends GoRouteData with $BookmarkEntryAddRoute { + final String bookmarkInfo; + + const BookmarkEntryAddRoute({required this.bookmarkInfo}); + + @override + Widget build(BuildContext context, GoRouterState state) { + return BookmarkEntryEditScreen( + initialInfo: BookmarkInfo.decode(jsonDecode(bookmarkInfo) as Object), + exisitingEntry: null, + ); + } +} + +class BookmarkEntryEditRoute extends GoRouteData with $BookmarkEntryEditRoute { + final String bookmarkEntry; + + const BookmarkEntryEditRoute({required this.bookmarkEntry}); + + @override + Widget build(BuildContext context, GoRouterState state) { + return BookmarkEntryEditScreen( + initialInfo: null, + exisitingEntry: BookmarkEntry.fromJson( + jsonDecode(bookmarkEntry) as Map, + ), + ); + } +} diff --git a/app/lib/core/routing/routes.dart b/app/lib/core/routing/routes.dart index 4f3c3f85..b8c3dea3 100644 --- a/app/lib/core/routing/routes.dart +++ b/app/lib/core/routing/routes.dart @@ -20,6 +20,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:go_router/go_router.dart'; import 'package:nullability/nullability.dart'; import 'package:weblibre/core/routing/widgets/dialog_page.dart'; @@ -32,6 +33,10 @@ import 'package:weblibre/features/bangs/presentation/screens/edit.dart'; import 'package:weblibre/features/bangs/presentation/screens/menu.dart'; import 'package:weblibre/features/bangs/presentation/screens/search.dart'; import 'package:weblibre/features/bangs/presentation/screens/user.dart'; +import 'package:weblibre/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart'; +import 'package:weblibre/features/geckoview/features/bookmarks/presentation/screens/bookmark_entry_edit.dart'; +import 'package:weblibre/features/geckoview/features/bookmarks/presentation/screens/bookmark_folder_edit.dart'; +import 'package:weblibre/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/open_shared_content.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/tab_tree.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/screens/browser.dart'; diff --git a/app/lib/core/routing/routes.g.dart b/app/lib/core/routing/routes.g.dart index 9e616a96..b824578e 100644 --- a/app/lib/core/routing/routes.g.dart +++ b/app/lib/core/routing/routes.g.dart @@ -464,6 +464,31 @@ RouteBase get $browserRoute => GoRouteData.$route( ), ], ), + GoRouteData.$route( + path: 'bookmarks/:entryGuid', + name: 'BookmarkListRoute', + factory: $BookmarkListRoute._fromState, + ), + GoRouteData.$route( + path: 'createFolder', + name: 'BookmarkFolderAddRoute', + factory: $BookmarkFolderAddRoute._fromState, + ), + GoRouteData.$route( + path: 'editFolder', + name: 'BookmarkFolderEditRoute', + factory: $BookmarkFolderEditRoute._fromState, + ), + GoRouteData.$route( + path: 'createEntry', + name: 'BookmarkEntryAddRoute', + factory: $BookmarkEntryAddRoute._fromState, + ), + GoRouteData.$route( + path: 'editEntry', + name: 'BookmarkEntryEditRoute', + factory: $BookmarkEntryEditRoute._fromState, + ), ], ); @@ -876,6 +901,143 @@ mixin $CreateProfileRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } +mixin $BookmarkListRoute on GoRouteData { + static BookmarkListRoute _fromState(GoRouterState state) => + BookmarkListRoute(entryGuid: state.pathParameters['entryGuid']!); + + BookmarkListRoute get _self => this as BookmarkListRoute; + + @override + String get location => GoRouteData.$location( + '/browser/bookmarks/${Uri.encodeComponent(_self.entryGuid)}', + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $BookmarkFolderAddRoute on GoRouteData { + static BookmarkFolderAddRoute _fromState(GoRouterState state) => + BookmarkFolderAddRoute( + parentGuid: state.uri.queryParameters['parent-guid'], + ); + + BookmarkFolderAddRoute get _self => this as BookmarkFolderAddRoute; + + @override + String get location => GoRouteData.$location( + '/browser/createFolder', + queryParams: { + if (_self.parentGuid != null) 'parent-guid': _self.parentGuid, + }, + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $BookmarkFolderEditRoute on GoRouteData { + static BookmarkFolderEditRoute _fromState(GoRouterState state) => + BookmarkFolderEditRoute(folder: state.uri.queryParameters['folder']!); + + BookmarkFolderEditRoute get _self => this as BookmarkFolderEditRoute; + + @override + String get location => GoRouteData.$location( + '/browser/editFolder', + queryParams: {'folder': _self.folder}, + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $BookmarkEntryAddRoute on GoRouteData { + static BookmarkEntryAddRoute _fromState(GoRouterState state) => + BookmarkEntryAddRoute( + bookmarkInfo: state.uri.queryParameters['bookmark-info']!, + ); + + BookmarkEntryAddRoute get _self => this as BookmarkEntryAddRoute; + + @override + String get location => GoRouteData.$location( + '/browser/createEntry', + queryParams: {'bookmark-info': _self.bookmarkInfo}, + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + +mixin $BookmarkEntryEditRoute on GoRouteData { + static BookmarkEntryEditRoute _fromState(GoRouterState state) => + BookmarkEntryEditRoute( + bookmarkEntry: state.uri.queryParameters['bookmark-entry']!, + ); + + BookmarkEntryEditRoute get _self => this as BookmarkEntryEditRoute; + + @override + String get location => GoRouteData.$location( + '/browser/editEntry', + queryParams: {'bookmark-entry': _self.bookmarkEntry}, + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + T? _$convertMapValue( String key, Map map, diff --git a/app/lib/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart b/app/lib/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart new file mode 100644 index 00000000..e8be67c2 --- /dev/null +++ b/app/lib/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart @@ -0,0 +1,145 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'bookmark_item.g.dart'; + +sealed class BookmarkItem { + abstract final String guid; + abstract final String? parentGuid; + abstract final String title; + abstract final int dateAdded; + abstract final int? position; + + const BookmarkItem(); + + static BookmarkItem? parseRecursive(BookmarkNode node) { + return switch (node.type) { + BookmarkNodeType.item => BookmarkEntry( + guid: node.guid, + parentGuid: node.parentGuid, + url: Uri.parse(node.url!), + title: node.title ?? node.url!, + previewImageUrl: Uri.parse(node.url!), + position: node.position, + dateAdded: node.dateAdded, + ), + BookmarkNodeType.folder => BookmarkFolder( + guid: node.guid, + parentGuid: node.parentGuid, + title: node.title ?? "Unnamed Folder", + position: node.position, + dateAdded: node.dateAdded, + children: node.children?.map(parseRecursive).nonNulls.toList(), + ), + BookmarkNodeType.separator => null, + }; + } + + factory BookmarkItem.fromJson(Map json) { + return (json.containsKey('url')) + ? BookmarkEntry.fromJson(json) + : BookmarkFolder.fromJson(json); + } + + Map toJson(); + + BookmarkItem clone(); +} + +@CopyWith() +@JsonSerializable() +class BookmarkEntry extends BookmarkItem with FastEquatable { + @override + final String guid; + @override + final String? parentGuid; + final Uri url; + @override + final String title; + final Uri previewImageUrl; + @override + final int? position; + @override + final int dateAdded; + + BookmarkEntry({ + required this.guid, + required this.parentGuid, + required this.url, + required this.title, + required this.previewImageUrl, + required this.position, + required this.dateAdded, + }); + + factory BookmarkEntry.fromJson(Map json) => + _$BookmarkEntryFromJson(json); + + @override + Map toJson() => _$BookmarkEntryToJson(this); + + @override + BookmarkItem clone() { + return copyWith(); + } + + @override + List get hashParameters => [ + guid, + parentGuid, + url, + title, + previewImageUrl, + position, + dateAdded, + ]; +} + +@CopyWith() +@JsonSerializable() +class BookmarkFolder extends BookmarkItem with FastEquatable { + @override + final String guid; + @override + final String? parentGuid; + @override + final String title; + @override + final int? position; + @override + final int dateAdded; + + final List? children; + + BookmarkFolder({ + required this.guid, + required this.parentGuid, + required String title, + required this.position, + required this.dateAdded, + required this.children, + }) : title = bookmarkRootDisplayNames[guid] ?? title; + + factory BookmarkFolder.fromJson(Map json) => + _$BookmarkFolderFromJson(json); + + @override + Map toJson() => _$BookmarkFolderToJson(this); + + @override + BookmarkItem clone() { + return copyWith(); + } + + @override + List get hashParameters => [ + guid, + parentGuid, + title, + position, + dateAdded, + children, + ]; +} diff --git a/app/lib/features/geckoview/features/bookmarks/domain/entities/bookmark_item.g.dart b/app/lib/features/geckoview/features/bookmarks/domain/entities/bookmark_item.g.dart new file mode 100644 index 00000000..9760c36b --- /dev/null +++ b/app/lib/features/geckoview/features/bookmarks/domain/entities/bookmark_item.g.dart @@ -0,0 +1,284 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'bookmark_item.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$BookmarkEntryCWProxy { + BookmarkEntry guid(String guid); + + BookmarkEntry parentGuid(String? parentGuid); + + BookmarkEntry url(Uri url); + + BookmarkEntry title(String title); + + BookmarkEntry previewImageUrl(Uri previewImageUrl); + + BookmarkEntry position(int? position); + + BookmarkEntry dateAdded(int dateAdded); + + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `BookmarkEntry(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// BookmarkEntry(...).copyWith(id: 12, name: "My name") + /// ``` + BookmarkEntry call({ + String guid, + String? parentGuid, + Uri url, + String title, + Uri previewImageUrl, + int? position, + int dateAdded, + }); +} + +/// Callable proxy for `copyWith` functionality. +/// Use as `instanceOfBookmarkEntry.copyWith(...)` or call `instanceOfBookmarkEntry.copyWith.fieldName(value)` for a single field. +class _$BookmarkEntryCWProxyImpl implements _$BookmarkEntryCWProxy { + const _$BookmarkEntryCWProxyImpl(this._value); + + final BookmarkEntry _value; + + @override + BookmarkEntry guid(String guid) => call(guid: guid); + + @override + BookmarkEntry parentGuid(String? parentGuid) => call(parentGuid: parentGuid); + + @override + BookmarkEntry url(Uri url) => call(url: url); + + @override + BookmarkEntry title(String title) => call(title: title); + + @override + BookmarkEntry previewImageUrl(Uri previewImageUrl) => + call(previewImageUrl: previewImageUrl); + + @override + BookmarkEntry position(int? position) => call(position: position); + + @override + BookmarkEntry dateAdded(int dateAdded) => call(dateAdded: dateAdded); + + @override + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `BookmarkEntry(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// BookmarkEntry(...).copyWith(id: 12, name: "My name") + /// ``` + BookmarkEntry call({ + Object? guid = const $CopyWithPlaceholder(), + Object? parentGuid = const $CopyWithPlaceholder(), + Object? url = const $CopyWithPlaceholder(), + Object? title = const $CopyWithPlaceholder(), + Object? previewImageUrl = const $CopyWithPlaceholder(), + Object? position = const $CopyWithPlaceholder(), + Object? dateAdded = const $CopyWithPlaceholder(), + }) { + return BookmarkEntry( + guid: guid == const $CopyWithPlaceholder() || guid == null + ? _value.guid + // ignore: cast_nullable_to_non_nullable + : guid as String, + parentGuid: parentGuid == const $CopyWithPlaceholder() + ? _value.parentGuid + // ignore: cast_nullable_to_non_nullable + : parentGuid as String?, + url: url == const $CopyWithPlaceholder() || url == null + ? _value.url + // ignore: cast_nullable_to_non_nullable + : url as Uri, + title: title == const $CopyWithPlaceholder() || title == null + ? _value.title + // ignore: cast_nullable_to_non_nullable + : title as String, + previewImageUrl: + previewImageUrl == const $CopyWithPlaceholder() || + previewImageUrl == null + ? _value.previewImageUrl + // ignore: cast_nullable_to_non_nullable + : previewImageUrl as Uri, + position: position == const $CopyWithPlaceholder() + ? _value.position + // ignore: cast_nullable_to_non_nullable + : position as int?, + dateAdded: dateAdded == const $CopyWithPlaceholder() || dateAdded == null + ? _value.dateAdded + // ignore: cast_nullable_to_non_nullable + : dateAdded as int, + ); + } +} + +extension $BookmarkEntryCopyWith on BookmarkEntry { + /// Returns a callable class used to build a new instance with modified fields. + /// Example: `instanceOfBookmarkEntry.copyWith(...)` or `instanceOfBookmarkEntry.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$BookmarkEntryCWProxy get copyWith => _$BookmarkEntryCWProxyImpl(this); +} + +abstract class _$BookmarkFolderCWProxy { + BookmarkFolder guid(String guid); + + BookmarkFolder parentGuid(String? parentGuid); + + BookmarkFolder title(String title); + + BookmarkFolder position(int? position); + + BookmarkFolder dateAdded(int dateAdded); + + BookmarkFolder children(List? children); + + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `BookmarkFolder(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// BookmarkFolder(...).copyWith(id: 12, name: "My name") + /// ``` + BookmarkFolder call({ + String guid, + String? parentGuid, + String title, + int? position, + int dateAdded, + List? children, + }); +} + +/// Callable proxy for `copyWith` functionality. +/// Use as `instanceOfBookmarkFolder.copyWith(...)` or call `instanceOfBookmarkFolder.copyWith.fieldName(value)` for a single field. +class _$BookmarkFolderCWProxyImpl implements _$BookmarkFolderCWProxy { + const _$BookmarkFolderCWProxyImpl(this._value); + + final BookmarkFolder _value; + + @override + BookmarkFolder guid(String guid) => call(guid: guid); + + @override + BookmarkFolder parentGuid(String? parentGuid) => call(parentGuid: parentGuid); + + @override + BookmarkFolder title(String title) => call(title: title); + + @override + BookmarkFolder position(int? position) => call(position: position); + + @override + BookmarkFolder dateAdded(int dateAdded) => call(dateAdded: dateAdded); + + @override + BookmarkFolder children(List? children) => + call(children: children); + + @override + /// Creates a new instance with the provided field values. + /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `BookmarkFolder(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// BookmarkFolder(...).copyWith(id: 12, name: "My name") + /// ``` + BookmarkFolder call({ + Object? guid = const $CopyWithPlaceholder(), + Object? parentGuid = const $CopyWithPlaceholder(), + Object? title = const $CopyWithPlaceholder(), + Object? position = const $CopyWithPlaceholder(), + Object? dateAdded = const $CopyWithPlaceholder(), + Object? children = const $CopyWithPlaceholder(), + }) { + return BookmarkFolder( + guid: guid == const $CopyWithPlaceholder() || guid == null + ? _value.guid + // ignore: cast_nullable_to_non_nullable + : guid as String, + parentGuid: parentGuid == const $CopyWithPlaceholder() + ? _value.parentGuid + // ignore: cast_nullable_to_non_nullable + : parentGuid as String?, + title: title == const $CopyWithPlaceholder() || title == null + ? _value.title + // ignore: cast_nullable_to_non_nullable + : title as String, + position: position == const $CopyWithPlaceholder() + ? _value.position + // ignore: cast_nullable_to_non_nullable + : position as int?, + dateAdded: dateAdded == const $CopyWithPlaceholder() || dateAdded == null + ? _value.dateAdded + // ignore: cast_nullable_to_non_nullable + : dateAdded as int, + children: children == const $CopyWithPlaceholder() + ? _value.children + // ignore: cast_nullable_to_non_nullable + : children as List?, + ); + } +} + +extension $BookmarkFolderCopyWith on BookmarkFolder { + /// Returns a callable class used to build a new instance with modified fields. + /// Example: `instanceOfBookmarkFolder.copyWith(...)` or `instanceOfBookmarkFolder.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$BookmarkFolderCWProxy get copyWith => _$BookmarkFolderCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +BookmarkEntry _$BookmarkEntryFromJson(Map json) => + BookmarkEntry( + guid: json['guid'] as String, + parentGuid: json['parentGuid'] as String?, + url: Uri.parse(json['url'] as String), + title: json['title'] as String, + previewImageUrl: Uri.parse(json['previewImageUrl'] as String), + position: (json['position'] as num?)?.toInt(), + dateAdded: (json['dateAdded'] as num).toInt(), + ); + +Map _$BookmarkEntryToJson(BookmarkEntry instance) => + { + 'guid': instance.guid, + 'parentGuid': instance.parentGuid, + 'url': instance.url.toString(), + 'title': instance.title, + 'previewImageUrl': instance.previewImageUrl.toString(), + 'position': instance.position, + 'dateAdded': instance.dateAdded, + }; + +BookmarkFolder _$BookmarkFolderFromJson(Map json) => + BookmarkFolder( + guid: json['guid'] as String, + parentGuid: json['parentGuid'] as String?, + title: json['title'] as String, + position: (json['position'] as num?)?.toInt(), + dateAdded: (json['dateAdded'] as num).toInt(), + children: (json['children'] as List?) + ?.map((e) => BookmarkItem.fromJson(e as Map)) + .toList(), + ); + +Map _$BookmarkFolderToJson(BookmarkFolder instance) => + { + 'guid': instance.guid, + 'parentGuid': instance.parentGuid, + 'title': instance.title, + 'position': instance.position, + 'dateAdded': instance.dateAdded, + 'children': instance.children?.map((e) => e.toJson()).toList(), + }; diff --git a/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart b/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart new file mode 100644 index 00000000..10a1bd79 --- /dev/null +++ b/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.dart @@ -0,0 +1,68 @@ +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/repositories/bookmarks.dart'; + +part 'bookmarks.g.dart'; + +T? _selectChildRecursive( + List children, + String guid, +) { + for (final child in children) { + if (child.guid == guid && child is T) { + return child; + } + + if (child case final BookmarkFolder folder) { + if (folder.children != null) { + final result = _selectChildRecursive(folder.children!, guid); + if (result != null) { + return result; + } + } + } + } + + return null; +} + +T _cloneAndFilterChildrenType(T node) { + if (node is BookmarkFolder) { + if (node.children != null) { + return node.copyWith.children( + node.children + ?.whereType() + .map((e) => _cloneAndFilterChildrenType(e)) + .toList(), + ) + as T; + } + } + + return node.clone() as T; +} + +@Riverpod() +AsyncValue bookmarks(Ref ref, String entryGuid) { + final bookmarksAsync = ref.watch(bookmarksRepositoryProvider); + + return bookmarksAsync.whenData((bookmarkNode) { + T? selectedNode; + + if (bookmarkNode != null && bookmarkNode is T) { + if (bookmarkNode.guid == entryGuid) { + selectedNode = bookmarkNode; + } else if (bookmarkNode case final BookmarkFolder folder) { + if (folder.children != null) { + selectedNode = _selectChildRecursive(folder.children!, entryGuid); + } + } + } + + if (selectedNode != null) { + return _cloneAndFilterChildrenType(selectedNode); + } + + return null; + }); +} 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 new file mode 100644 index 00000000..010cd3d2 --- /dev/null +++ b/app/lib/features/geckoview/features/bookmarks/domain/providers/bookmarks.g.dart @@ -0,0 +1,110 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'bookmarks.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(bookmarks) +const bookmarksProvider = BookmarksFamily._(); + +final class BookmarksProvider + extends $FunctionalProvider, AsyncValue, AsyncValue> + with $Provider> { + const BookmarksProvider._({ + required BookmarksFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'bookmarksProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$bookmarksHash(); + + @override + String toString() { + return r'bookmarksProvider' + '<${T}>' + '($argument)'; + } + + @$internal + @override + $ProviderElement> $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + AsyncValue create(Ref ref) { + final argument = this.argument as String; + return bookmarks(ref, argument); + } + + $R _captureGenerics<$R>($R Function() cb) { + return cb(); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(AsyncValue value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>(value), + ); + } + + @override + bool operator ==(Object other) { + return other is BookmarksProvider && + other.runtimeType == runtimeType && + other.argument == argument; + } + + @override + int get hashCode { + return Object.hash(runtimeType, argument); + } +} + +String _$bookmarksHash() => r'dfa19aea04f352b8a6cefe35a0b283c9fddb214f'; + +final class BookmarksFamily extends $Family { + const BookmarksFamily._() + : super( + retry: null, + name: r'bookmarksProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + BookmarksProvider call(String entryGuid) => + BookmarksProvider._(argument: entryGuid, from: this); + + @override + String toString() => r'bookmarksProvider'; + + /// {@macro riverpod.override_with} + Override overrideWith( + AsyncValue Function(Ref ref, String args) + create, + ) => $FamilyOverride( + from: this, + createElement: (pointer) { + final provider = pointer.origin as BookmarksProvider; + return provider._captureGenerics(() { + provider as BookmarksProvider; + final argument = provider.argument as String; + return provider + .$view(create: (ref) => create(ref, argument)) + .$createElement(pointer); + }); + }, + ); +} diff --git a/app/lib/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart b/app/lib/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart new file mode 100644 index 00000000..eb142973 --- /dev/null +++ b/app/lib/features/geckoview/features/bookmarks/domain/repositories/bookmarks.dart @@ -0,0 +1,60 @@ +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:nullability/nullability.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/features/geckoview/features/bookmarks/domain/entities/bookmark_item.dart'; + +part 'bookmarks.g.dart'; + +@Riverpod(keepAlive: true) +class BookmarksRepository extends _$BookmarksRepository { + final _service = GeckoBookmarksService(); + + Future addBookmark({ + required String parentGuid, + required Uri url, + required String title, + }) async { + await _service.addItem(parentGuid, url, title, null); + ref.invalidateSelf(); + } + + Future addFolder({ + required String parentGuid, + required String title, + }) async { + await _service.addFolder(parentGuid, title, null); + ref.invalidateSelf(); + } + + Future editBookmark({ + required String guid, + String? title, + Uri? url, + String? parentGuid, + }) async { + await _service.updateNode( + guid, + BookmarkInfo(title: title, url: url?.toString(), parentGuid: parentGuid), + ); + ref.invalidateSelf(); + } + + Future editFolder({required String guid, required String title}) async { + await _service.updateNode(guid, BookmarkInfo(title: title)); + ref.invalidateSelf(); + } + + Future delete(String guid) async { + await _service.deleteNode(guid); + ref.invalidateSelf(); + } + + @override + Future build() async { + final node = await _service.getTree( + BookmarkRoot.mobile.id, + recursive: true, + ); + return node.mapNotNull(BookmarkItem.parseRecursive); + } +} diff --git a/app/lib/features/geckoview/features/bookmarks/domain/repositories/bookmarks.g.dart b/app/lib/features/geckoview/features/bookmarks/domain/repositories/bookmarks.g.dart new file mode 100644 index 00000000..4975e95c --- /dev/null +++ b/app/lib/features/geckoview/features/bookmarks/domain/repositories/bookmarks.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'bookmarks.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(BookmarksRepository) +const bookmarksRepositoryProvider = BookmarksRepositoryProvider._(); + +final class BookmarksRepositoryProvider + extends $AsyncNotifierProvider { + const BookmarksRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'bookmarksRepositoryProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$bookmarksRepositoryHash(); + + @$internal + @override + BookmarksRepository create() => BookmarksRepository(); +} + +String _$bookmarksRepositoryHash() => + r'b8fa5b5699c053b91fcabd46dc97b7192e32d068'; + +abstract class _$BookmarksRepository extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref, BookmarkItem?>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, BookmarkItem?>, + AsyncValue, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_entry_edit.dart b/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_entry_edit.dart new file mode 100644 index 00000000..c2f31dcb --- /dev/null +++ b/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_entry_edit.dart @@ -0,0 +1,277 @@ +import 'package:animated_tree_view/animated_tree_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.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/repositories/bookmarks.dart'; +import 'package:weblibre/presentation/widgets/failure_widget.dart'; +import 'package:weblibre/utils/form_validators.dart'; +import 'package:weblibre/utils/uri_parser.dart' as uri_parser; + +class BookmarkEntryEditScreen extends HookConsumerWidget { + final BookmarkInfo? initialInfo; + final BookmarkEntry? exisitingEntry; + + const BookmarkEntryEditScreen({ + required this.exisitingEntry, + required this.initialInfo, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formKey = useMemoized(() => GlobalKey()); + + final folderList = ref.watch( + bookmarksProvider(BookmarkRoot.mobile.id), + ); + + final nameTextController = useTextEditingController( + text: initialInfo?.title ?? exisitingEntry?.title, + ); + final urlTextController = useTextEditingController( + text: initialInfo?.url ?? exisitingEntry?.url.toString(), + ); + + final parentGuid = useState( + initialInfo?.parentGuid ?? + exisitingEntry?.parentGuid ?? + BookmarkRoot.mobile.id, + ); + + return Scaffold( + appBar: AppBar( + title: (exisitingEntry != null) + ? const Text('Edit Bookmark') + : const Text('Create Bookmark'), + actions: [ + IconButton( + onPressed: () async { + if (formKey.currentState?.validate() ?? false) { + final newUrl = uri_parser.tryParseUrl( + urlTextController.text, + eagerParsing: true, + )!; + + if (exisitingEntry != null) { + await ref + .read(bookmarksRepositoryProvider.notifier) + .editBookmark( + guid: exisitingEntry!.guid, + title: + (nameTextController.text != exisitingEntry!.title) + ? nameTextController.text + : null, + parentGuid: + (parentGuid.value != exisitingEntry!.parentGuid) + ? parentGuid.value + : null, + url: (newUrl != exisitingEntry!.url) ? newUrl : null, + ); + + if (context.mounted) { + context.pop(); + } + } else { + await ref + .read(bookmarksRepositoryProvider.notifier) + .addBookmark( + parentGuid: parentGuid.value, + title: nameTextController.text, + url: newUrl, + ); + + if (context.mounted) { + context.pop(); + } + } + } + }, + icon: const Icon(Icons.check), + ), + ], + ), + body: SafeArea( + child: Form( + key: formKey, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: ListView( + children: [ + TextFormField( + controller: nameTextController, + decoration: const InputDecoration( + label: Text('Name'), + floatingLabelBehavior: FloatingLabelBehavior.always, + ), + minLines: 1, + maxLines: 3, + validator: validateRequired, + ), + const SizedBox(height: 8), + TextFormField( + controller: urlTextController, + keyboardType: TextInputType.url, + minLines: 1, + maxLines: 10, + decoration: const InputDecoration( + label: Text('URL'), + hintText: 'https://example.com/', + floatingLabelBehavior: FloatingLabelBehavior.always, + ), + validator: (value) { + return validateUrl( + value, + onlyHttpProtocol: true, + eagerParsing: true, + ); + }, + ), + const SizedBox(height: 16), + Text('Folder', style: Theme.of(context).textTheme.labelMedium), + folderList.when( + data: (list) { + TreeNode addChildren( + TreeNode? parent, + BookmarkFolder item, + ) { + final node = TreeNode( + key: item.guid, + data: item, + parent: parent, + ); + final targetNode = (parent?..add(node)) ?? node; + + if (item.children != null) { + for (final child in item.children!) { + addChildren(node, child as BookmarkFolder); + } + } + + return targetNode; + } + + final root = (list != null) + ? addChildren(null, list) + : TreeNode.root(); + + return TreeView.simple( + tree: root, + shrinkWrap: true, + onTreeReady: (controller) { + controller.expandAllChildren(root); + }, + expansionIndicatorBuilder: (context, tree) => + ChevronIndicator.upDown( + tree: tree, + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 12.0, + ), + ), + builder: (context, item) { + final isSelected = item.data?.guid == parentGuid.value; + + return Padding( + padding: const EdgeInsets.only(right: 16.0), + child: switch (item.data) { + final BookmarkFolder folder => ListTile( + selected: isSelected, + leading: (item.isExpanded) + ? const Icon(MdiIcons.folderOpen) + : const Icon(MdiIcons.folder), + trailing: isSelected + ? const Icon(Icons.check) + : null, + title: Text(folder.title), + onTap: () { + parentGuid.value = folder.guid; + }, + ), + null => const SizedBox.shrink(), + }, + ); + }, + ); + }, + error: (error, stackTrace) => Center( + child: FailureWidget( + title: 'Failed to load Bookmark Folders', + exception: error, + onRetry: () { + // ignore: unused_result + ref.refresh( + bookmarksProvider( + BookmarkRoot.mobile.id, + ), + ); + }, + ), + ), + loading: () => const SizedBox.shrink(), + ), + const SizedBox(height: 16), + if (exisitingEntry != null) + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + side: BorderSide( + color: Theme.of(context).colorScheme.error, + ), + foregroundColor: Theme.of(context).colorScheme.error, + iconColor: Theme.of(context).colorScheme.error, + ), + label: const Text('Delete'), + icon: const Icon(MdiIcons.bookmarkRemove), + onPressed: () async { + final result = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + icon: const Icon(Icons.warning), + title: const Text('Delete Bookmark'), + content: const Text( + 'Are you sure you want to delete this Bookmark?', + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context, false); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.pop(context, true); + }, + child: const Text('Delete'), + ), + ], + ); + }, + ); + + if (result == true) { + await ref + .read(bookmarksRepositoryProvider.notifier) + .delete(exisitingEntry!.guid); + + if (context.mounted) { + context.pop(); + } + } + }, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_folder_edit.dart b/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_folder_edit.dart new file mode 100644 index 00000000..7c2fb873 --- /dev/null +++ b/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_folder_edit.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.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/utils/form_validators.dart'; + +class BookmarkFolderEditScreen extends HookConsumerWidget { + final String? parentGuid; + final BookmarkFolder? folder; + + const BookmarkFolderEditScreen({required this.folder, this.parentGuid}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formKey = useMemoized(() => GlobalKey()); + final nameTextController = useTextEditingController(text: folder?.title); + + return Scaffold( + appBar: AppBar( + title: (folder != null) + ? const Text('Edit Folder') + : const Text('Create Folder'), + actions: [ + IconButton( + onPressed: () async { + if (formKey.currentState?.validate() ?? false) { + if (folder != null) { + await ref + .read(bookmarksRepositoryProvider.notifier) + .editFolder( + guid: folder!.guid, + title: nameTextController.text, + ); + + if (context.mounted) { + context.pop(); + } + } else { + await ref + .read(bookmarksRepositoryProvider.notifier) + .addFolder( + parentGuid: parentGuid ?? BookmarkRoot.mobile.id, + title: nameTextController.text, + ); + + if (context.mounted) { + context.pop(); + } + } + } + }, + icon: const Icon(Icons.check), + ), + ], + ), + body: Form( + key: formKey, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: ListView( + children: [ + TextFormField( + controller: nameTextController, + decoration: const InputDecoration( + label: Text('Name'), + floatingLabelBehavior: FloatingLabelBehavior.always, + ), + validator: validateRequired, + ), + const SizedBox(height: 16), + if (folder != null) + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + side: BorderSide( + color: Theme.of(context).colorScheme.error, + ), + foregroundColor: Theme.of(context).colorScheme.error, + iconColor: Theme.of(context).colorScheme.error, + ), + label: const Text('Delete'), + icon: const Icon(Icons.delete), + onPressed: () async { + final result = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + icon: const Icon(Icons.warning), + title: const Text('Delete Folder'), + content: const Text( + 'Are you sure you want to delete this Folder including all bookmarks?', + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context, false); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.pop(context, true); + }, + child: const Text('Delete'), + ), + ], + ); + }, + ); + + if (result == true) { + await ref + .read(bookmarksRepositoryProvider.notifier) + .delete(folder!.guid); + + if (context.mounted) { + context.pop(); + } + } + }, + ), + ), + ], + ), + ), + ), + ); + } +} 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 new file mode 100644 index 00000000..374957ef --- /dev/null +++ b/app/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart @@ -0,0 +1,198 @@ +import 'dart:convert'; + +import 'package:animated_tree_view/animated_tree_view.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:hooks_riverpod/hooks_riverpod.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/providers/bookmarks.dart'; +import 'package:weblibre/presentation/hooks/menu_controller.dart'; +import 'package:weblibre/presentation/widgets/failure_widget.dart'; +import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart'; +import 'package:weblibre/presentation/widgets/url_icon.dart'; + +class BookmarkListScreen extends HookConsumerWidget { + final String entryGuid; + + const BookmarkListScreen({super.key, required this.entryGuid}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final bookmarkList = ref.watch(bookmarksProvider(entryGuid)); + + return Scaffold( + appBar: AppBar(title: const Text('Bookmarks')), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(left: 12.0), + child: bookmarkList.when( + data: (list) { + TreeNode addChildren( + TreeNode? parent, + BookmarkItem item, + ) { + final node = TreeNode( + key: item.guid, + data: item, + parent: parent, + ); + final targetNode = (parent?..add(node)) ?? node; + + if (item is BookmarkFolder && item.children != null) { + for (final child in item.children!) { + addChildren(node, child); + } + } + + return targetNode; + } + + final root = (list != null) + ? addChildren(null, list) + : TreeNode.root(); + + return TreeView.simple( + tree: root, + onTreeReady: (controller) { + controller.expandNode(root); + }, + expansionIndicatorBuilder: (context, tree) => + ChevronIndicator.upDown( + tree: tree, + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 12.0, + ), + ), + builder: (context, item) { + return switch (item.data) { + final BookmarkEntry bookmark => ListTile( + contentPadding: EdgeInsets.zero, + leading: UrlIcon([bookmark.url], iconSize: 34.0), + trailing: IconButton( + icon: const Icon(Icons.edit), + onPressed: () async { + await BookmarkEntryEditRoute( + bookmarkEntry: jsonEncode(bookmark.toJson()), + ).push(context); + }, + ), + title: Text(bookmark.title), + subtitle: UriBreadcrumb(uri: bookmark.url), + onTap: () async { + final result = await OpenSharedContentRoute( + sharedUrl: bookmark.url.toString(), + ).push(context); + + if (result == true) { + if (context.mounted) { + const BrowserRoute().go(context); + } + } + }, + ), + final BookmarkFolder folder => Padding( + padding: const EdgeInsets.only(right: 42.0), + child: HookBuilder( + builder: (context) { + final controller = useMenuController(); + + return ListTile( + contentPadding: EdgeInsets.zero, + leading: (item.isExpanded) + ? const Icon(MdiIcons.folderOpen) + : const Icon(MdiIcons.folder), + title: Text(folder.title), + trailing: MenuAnchor( + controller: controller, + builder: (context, controller, child) => InkWell( + onTap: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: const Padding( + padding: EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 15.0, + ), + child: Icon(MdiIcons.dotsVertical), + ), + ), + menuChildren: [ + if (!bookmarkRootIds.contains(folder.guid)) + MenuItemButton( + leadingIcon: const Icon( + MdiIcons.folderEdit, + ), + child: const Text('Edit'), + onPressed: () async { + await BookmarkFolderEditRoute( + folder: jsonEncode(folder.toJson()), + ).push(context); + }, + ), + MenuItemButton( + leadingIcon: const Icon(MdiIcons.folderPlus), + child: const Text('Add Subfolder'), + onPressed: () async { + await BookmarkFolderAddRoute( + parentGuid: folder.guid, + ).push(context); + }, + ), + MenuItemButton( + leadingIcon: const Icon( + MdiIcons.bookmarkPlus, + ), + child: const Text('Add Bookmark'), + onPressed: () async { + await BookmarkEntryAddRoute( + bookmarkInfo: jsonEncode( + BookmarkInfo( + parentGuid: folder.guid, + ).encode(), + ), + ).push(context); + }, + ), + ], + ), + onTap: () async { + if (folder.guid != entryGuid) { + await BookmarkListRoute( + entryGuid: folder.guid, + ).push(context); + } + }, + ); + }, + ), + ), + null => const Center(child: Text('Empty')), + }; + }, + ); + }, + error: (error, stackTrace) => Center( + child: FailureWidget( + title: 'Failed to load Bookmarks', + exception: error, + onRetry: () { + // ignore: unused_result + ref.refresh(bookmarksProvider(entryGuid)); + }, + ), + ), + loading: () => const SizedBox.shrink(), + ), + ), + ), + ); + } +} diff --git a/app/lib/features/geckoview/features/browser/presentation/dialogs/open_shared_content.dart b/app/lib/features/geckoview/features/browser/presentation/dialogs/open_shared_content.dart index 8a30a1ac..a33fb166 100644 --- a/app/lib/features/geckoview/features/browser/presentation/dialogs/open_shared_content.dart +++ b/app/lib/features/geckoview/features/browser/presentation/dialogs/open_shared_content.dart @@ -53,7 +53,7 @@ class OpenSharedContent extends HookConsumerWidget { ); if (context.mounted) { - context.pop(); + context.pop(true); } } } diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart index 8818ed8b..8ff0b419 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart @@ -437,6 +437,15 @@ class BrowserBottomAppBar extends HookConsumerWidget { ), ), const Divider(), + MenuItemButton( + onPressed: () async { + await BookmarkListRoute( + entryGuid: BookmarkRoot.mobile.id, + ).push(context); + }, + leadingIcon: const Icon(MdiIcons.bookmarkMultiple), + child: const Text('Bookmarks'), + ), MenuItemButton( onPressed: () async { await const BangMenuRoute().push(context); diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_menu.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_menu.dart index 2baaad91..ed5717f6 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_menu.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_menu.dart @@ -17,12 +17,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'dart:convert'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:nullability/nullability.dart'; import 'package:share_plus/share_plus.dart'; @@ -164,6 +166,22 @@ class TabMenu extends HookConsumerWidget { await ui_helper.launchUrlFeedback(context, tabState.url); }, ), + MenuItemButton( + leadingIcon: const Icon(MdiIcons.bookmarkPlus), + child: const Text('Add Bookmark'), + onPressed: () async { + final tabState = ref.read(tabStateProvider(selectedTabId))!; + + await BookmarkEntryAddRoute( + bookmarkInfo: jsonEncode( + BookmarkInfo( + title: tabState.title, + url: tabState.url.toString(), + ).encode(), + ), + ).push(context); + }, + ), MenuItemButton( leadingIcon: const Icon(MdiIcons.tabPlus), child: const Text('Clone tab'), diff --git a/app/lib/features/user/domain/repositories/profile.dart b/app/lib/features/user/domain/repositories/profile.dart index 5cc371f6..4ca9bbb4 100644 --- a/app/lib/features/user/domain/repositories/profile.dart +++ b/app/lib/features/user/domain/repositories/profile.dart @@ -27,14 +27,14 @@ class ProfileRepository extends _$ProfileRepository { throw Exception('Could not create profile'); } - state = await AsyncValue.guard(_readProfiles); + ref.invalidateSelf(); return profile; } Future updateProfileMetadata(Profile profile) async { await filesystem.updateProfileMetadata(profile); - state = await AsyncValue.guard(_readProfiles); + ref.invalidateSelf(); } Future deleteProfile(String id) async { @@ -45,7 +45,7 @@ class ProfileRepository extends _$ProfileRepository { await filesystem.getProfileDir(uuid).delete(recursive: true); - state = await AsyncValue.guard(_readProfiles); + ref.invalidateSelf(); return true; } diff --git a/app/lib/features/user/domain/repositories/profile.g.dart b/app/lib/features/user/domain/repositories/profile.g.dart index 48224726..d0c803ce 100644 --- a/app/lib/features/user/domain/repositories/profile.g.dart +++ b/app/lib/features/user/domain/repositories/profile.g.dart @@ -33,7 +33,7 @@ final class ProfileRepositoryProvider ProfileRepository create() => ProfileRepository(); } -String _$profileRepositoryHash() => r'1357d42738d40e8e447ab8879292e81ad7b80b61'; +String _$profileRepositoryHash() => r'249240ce0e775d4fd9ee3558e6b05326d09d79de'; abstract class _$ProfileRepository extends $AsyncNotifier> { FutureOr> build(); diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 07f3f7a9..8e82edc0 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -8,6 +8,7 @@ environment: sdk: '>=3.8.0 <4.0.0' dependencies: + animated_tree_view: ^2.3.0 background_fetch: ^1.5.0 collection: ^1.19.1 copy_with_extension: ^10.0.1 diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBookmarksApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBookmarksApiImpl.kt new file mode 100644 index 00000000..5e1e3ea0 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBookmarksApiImpl.kt @@ -0,0 +1,211 @@ +package eu.weblibre.flutter_mozilla_components.api + +import eu.weblibre.flutter_mozilla_components.GlobalComponents +import eu.weblibre.flutter_mozilla_components.pigeons.BookmarkInfo +import eu.weblibre.flutter_mozilla_components.pigeons.BookmarkNode +import eu.weblibre.flutter_mozilla_components.pigeons.BookmarkNodeType +import eu.weblibre.flutter_mozilla_components.pigeons.GeckoBookmarksApi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class GeckoBookmarksApiImpl() : GeckoBookmarksApi { + companion object { + private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) + } + + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } + + fun mozilla.components.concept.storage.BookmarkNode.toPigeonBookmarkNode(): BookmarkNode { + return BookmarkNode( + type = when (this.type) { + mozilla.components.concept.storage.BookmarkNodeType.ITEM -> BookmarkNodeType.ITEM + mozilla.components.concept.storage.BookmarkNodeType.FOLDER -> BookmarkNodeType.FOLDER + mozilla.components.concept.storage.BookmarkNodeType.SEPARATOR -> BookmarkNodeType.SEPARATOR + }, + guid = this.guid, + parentGuid = this.parentGuid, + position = this.position?.toLong(), + title = this.title, + url = this.url, + dateAdded = this.dateAdded, + lastModified = this.lastModified, + children = this.children?.map { it.toPigeonBookmarkNode() } + ) + } + + fun BookmarkNode.toConceptStorageBookmarkNode(): mozilla.components.concept.storage.BookmarkNode { + return mozilla.components.concept.storage.BookmarkNode( + type = when (this.type) { + BookmarkNodeType.ITEM -> mozilla.components.concept.storage.BookmarkNodeType.ITEM + BookmarkNodeType.FOLDER -> mozilla.components.concept.storage.BookmarkNodeType.FOLDER + BookmarkNodeType.SEPARATOR -> mozilla.components.concept.storage.BookmarkNodeType.SEPARATOR + }, + guid = this.guid, + parentGuid = this.parentGuid, + position = this.position?.toUInt(), + title = this.title, + url = this.url, + dateAdded = this.dateAdded, + lastModified = this.lastModified, + children = this.children?.map { it.toConceptStorageBookmarkNode() } + ) + } + + + override fun getTree( + guid: String, + recursive: Boolean, + callback: (Result) -> Unit + ) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + val node = components.core.bookmarksStorage.getTree(guid, recursive) + node.fold( + { node -> + callback(Result.success(node?.toPigeonBookmarkNode())) + }, + { e -> callback(Result.failure(e)) }) + } + } + + } + + override fun getBookmark( + guid: String, + callback: (Result) -> Unit + ) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + components.core.bookmarksStorage.getBookmark(guid).fold( + { node -> callback(Result.success(node?.toPigeonBookmarkNode())) }, + { e -> callback(Result.failure(e)) } + ) + } + } + } + + override fun getBookmarksWithUrl( + url: String, + callback: (Result>) -> Unit + ) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + components.core.bookmarksStorage.getBookmarksWithUrl(url).fold( + { nodes -> callback(Result.success(nodes.map { it.toPigeonBookmarkNode() })) }, + { e -> callback(Result.failure(e)) } + ) + } + } + } + + override fun getRecentBookmarks( + limit: Long, + maxAge: Long?, + currentTime: Long, + callback: (Result>) -> Unit + ) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + components.core.bookmarksStorage.getRecentBookmarks( + limit = limit.toInt(), + maxAge = maxAge, + currentTime = currentTime + ).fold( + { nodes -> callback(Result.success(nodes.map { it.toPigeonBookmarkNode() })) }, + { e -> callback(Result.failure(e)) } + ) + } + } + } + + override fun searchBookmarks( + query: String, + limit: Long, + callback: (Result>) -> Unit + ) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + components.core.bookmarksStorage.searchBookmarks(query, limit.toInt()).fold( + { nodes -> callback(Result.success(nodes.map { it.toPigeonBookmarkNode() })) }, + { e -> callback(Result.failure(e)) } + ) + } + } + } + + override fun addItem( + parentGuid: String, + url: String, + title: String, + position: Long?, + callback: (Result) -> Unit + ) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + components.core.bookmarksStorage.addItem(parentGuid, url, title, position?.toUInt()) + .fold( + { guid -> callback(Result.success(guid)) }, + { e -> callback(Result.failure(e)) } + ) + } + } + } + + override fun addFolder( + parentGuid: String, + title: String, + position: Long?, + callback: (Result) -> Unit + ) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + components.core.bookmarksStorage.addFolder(parentGuid, title, position?.toUInt()) + .fold( + { guid -> callback(Result.success(guid)) }, + { e -> callback(Result.failure(e)) } + ) + } + } + } + + override fun updateNode( + guid: String, + info: BookmarkInfo, + callback: (Result) -> Unit + ) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + val conceptInfo = mozilla.components.concept.storage.BookmarkInfo( + parentGuid = info.parentGuid, + position = info.position?.toUInt(), + title = info.title, + url = info.url + ) + components.core.bookmarksStorage.updateNode(guid, conceptInfo).fold( + { callback(Result.success(Unit)) }, + { e -> callback(Result.failure(e)) } + ) + } + } + } + + override fun deleteNode( + guid: String, + callback: (Result) -> Unit + ) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + components.core.bookmarksStorage.deleteNode(guid).fold( + { deleted -> callback(Result.success(deleted)) }, + { e -> callback(Result.failure(e)) } + ) + } + } + } + +} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt index a3ef6259..bf5f4141 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt @@ -21,6 +21,7 @@ import eu.weblibre.flutter_mozilla_components.pigeons.BrowserExtensionEvents import eu.weblibre.flutter_mozilla_components.pigeons.ContentBlocking import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonsApi +import eu.weblibre.flutter_mozilla_components.pigeons.GeckoBookmarksApi import eu.weblibre.flutter_mozilla_components.pigeons.GeckoBrowserApi import eu.weblibre.flutter_mozilla_components.pigeons.GeckoBrowserExtensionApi import eu.weblibre.flutter_mozilla_components.pigeons.GeckoContainerProxyApi @@ -55,6 +56,7 @@ import mozilla.components.support.base.log.Log import mozilla.components.support.base.log.sink.LogSink import org.mozilla.gecko.util.ThreadUtils.runOnUiThread import org.mozilla.geckoview.BuildConfig as GeckoViewBuildConfig +import mozilla.appservices.places.BookmarkRoot class PriorityAwareLogSink( private val minLogPriority: Log.Priority, @@ -259,6 +261,7 @@ class GeckoBrowserApiImpl : GeckoBrowserApi { ) GeckoHistoryApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoHistoryApiImpl()) GeckoFetchApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFetchApiImpl()) + GeckoBookmarksApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBookmarksApiImpl()) ReaderViewEvents.setUp( _flutterPluginBinding.binaryMessenger, diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt index 446432cc..b5a18b1b 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt @@ -30,6 +30,7 @@ import mozilla.components.browser.session.storage.SessionStorage import mozilla.components.browser.state.engine.EngineMiddleware import mozilla.components.browser.state.engine.middleware.SessionPrioritizationMiddleware import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.storage.sync.PlacesBookmarksStorage import mozilla.components.browser.storage.sync.PlacesHistoryStorage import mozilla.components.browser.thumbnails.ThumbnailsMiddleware import mozilla.components.browser.thumbnails.storage.ThumbnailStorage @@ -239,11 +240,13 @@ class Core( * private sessions). */ val lazyHistoryStorage = lazy { PlacesHistoryStorage(context) } + val lazyBookmarksStorage = lazy { PlacesBookmarksStorage(context) } /** * A convenience accessor to the [PlacesHistoryStorage]. */ val historyStorage by lazy { lazyHistoryStorage.value } + val bookmarksStorage by lazy { lazyBookmarksStorage.value } val permissionStorage by lazy { PermissionStorage(geckoSitePermissionsStorage) } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt index aee906e0..75946072 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -424,6 +424,18 @@ enum class GeckoFetchCookiePolicy(val raw: Int) { } } +enum class BookmarkNodeType(val raw: Int) { + ITEM(0), + FOLDER(1), + SEPARATOR(2); + + companion object { + fun ofRaw(raw: Int): BookmarkNodeType? { + return values().firstOrNull { it.raw == raw } + } + } +} + /** * Translation options that map to the Gecko Translations Options. * @@ -2448,6 +2460,99 @@ data class GeckoFetchResponse ( override fun hashCode(): Int = toList().hashCode() } + +/** Generated class from Pigeon that represents data sent in messages. */ +data class BookmarkNode ( + val type: BookmarkNodeType, + val guid: String, + val parentGuid: String? = null, + val position: Long? = null, + val title: String? = null, + val url: String? = null, + val dateAdded: Long, + val lastModified: Long, + val children: List? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): BookmarkNode { + val type = pigeonVar_list[0] as BookmarkNodeType + val guid = pigeonVar_list[1] as String + val parentGuid = pigeonVar_list[2] as String? + val position = pigeonVar_list[3] as Long? + val title = pigeonVar_list[4] as String? + val url = pigeonVar_list[5] as String? + val dateAdded = pigeonVar_list[6] as Long + val lastModified = pigeonVar_list[7] as Long + val children = pigeonVar_list[8] as List? + return BookmarkNode(type, guid, parentGuid, position, title, url, dateAdded, lastModified, children) + } + } + fun toList(): List { + return listOf( + type, + guid, + parentGuid, + position, + title, + url, + dateAdded, + lastModified, + children, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is BookmarkNode) { + return false + } + if (this === other) { + return true + } + return GeckoPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Class for making alterations to any bookmark node + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class BookmarkInfo ( + val parentGuid: String? = null, + val position: Long? = null, + val title: String? = null, + val url: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): BookmarkInfo { + val parentGuid = pigeonVar_list[0] as String? + val position = pigeonVar_list[1] as Long? + val title = pigeonVar_list[2] as String? + val url = pigeonVar_list[3] as String? + return BookmarkInfo(parentGuid, position, title, url) + } + } + fun toList(): List { + return listOf( + parentGuid, + position, + title, + url, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is BookmarkInfo) { + return false + } + if (this === other) { + return true + } + return GeckoPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} private open class GeckoPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { @@ -2562,245 +2667,260 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { } } 151.toByte() -> { - return (readValue(buffer) as? List)?.let { - TranslationOptions.fromList(it) + return (readValue(buffer) as Long?)?.let { + BookmarkNodeType.ofRaw(it.toInt()) } } 152.toByte() -> { return (readValue(buffer) as? List)?.let { - ReaderState.fromList(it) + TranslationOptions.fromList(it) } } 153.toByte() -> { return (readValue(buffer) as? List)?.let { - LastMediaAccessState.fromList(it) + ReaderState.fromList(it) } } 154.toByte() -> { return (readValue(buffer) as? List)?.let { - HistoryMetadataKey.fromList(it) + LastMediaAccessState.fromList(it) } } 155.toByte() -> { return (readValue(buffer) as? List)?.let { - PackageCategoryValue.fromList(it) + HistoryMetadataKey.fromList(it) } } 156.toByte() -> { return (readValue(buffer) as? List)?.let { - ExternalPackage.fromList(it) + PackageCategoryValue.fromList(it) } } 157.toByte() -> { return (readValue(buffer) as? List)?.let { - LoadUrlFlagsValue.fromList(it) + ExternalPackage.fromList(it) } } 158.toByte() -> { return (readValue(buffer) as? List)?.let { - SourceValue.fromList(it) + LoadUrlFlagsValue.fromList(it) } } 159.toByte() -> { return (readValue(buffer) as? List)?.let { - TabState.fromList(it) + SourceValue.fromList(it) } } 160.toByte() -> { return (readValue(buffer) as? List)?.let { - RecoverableTab.fromList(it) + TabState.fromList(it) } } 161.toByte() -> { return (readValue(buffer) as? List)?.let { - RecoverableBrowserState.fromList(it) + RecoverableTab.fromList(it) } } 162.toByte() -> { return (readValue(buffer) as? List)?.let { - IconRequest.fromList(it) + RecoverableBrowserState.fromList(it) } } 163.toByte() -> { return (readValue(buffer) as? List)?.let { - ResourceSize.fromList(it) + IconRequest.fromList(it) } } 164.toByte() -> { return (readValue(buffer) as? List)?.let { - Resource.fromList(it) + ResourceSize.fromList(it) } } 165.toByte() -> { return (readValue(buffer) as? List)?.let { - IconResult.fromList(it) + Resource.fromList(it) } } 166.toByte() -> { return (readValue(buffer) as? List)?.let { - CookiePartitionKey.fromList(it) + IconResult.fromList(it) } } 167.toByte() -> { return (readValue(buffer) as? List)?.let { - Cookie.fromList(it) + CookiePartitionKey.fromList(it) } } 168.toByte() -> { return (readValue(buffer) as? List)?.let { - VisitInfo.fromList(it) + Cookie.fromList(it) } } 169.toByte() -> { return (readValue(buffer) as? List)?.let { - HistoryItem.fromList(it) + VisitInfo.fromList(it) } } 170.toByte() -> { return (readValue(buffer) as? List)?.let { - HistoryState.fromList(it) + HistoryItem.fromList(it) } } 171.toByte() -> { return (readValue(buffer) as? List)?.let { - ReaderableState.fromList(it) + HistoryState.fromList(it) } } 172.toByte() -> { return (readValue(buffer) as? List)?.let { - SecurityInfoState.fromList(it) + ReaderableState.fromList(it) } } 173.toByte() -> { return (readValue(buffer) as? List)?.let { - TabContentState.fromList(it) + SecurityInfoState.fromList(it) } } 174.toByte() -> { return (readValue(buffer) as? List)?.let { - FindResultState.fromList(it) + TabContentState.fromList(it) } } 175.toByte() -> { return (readValue(buffer) as? List)?.let { - CustomSelectionAction.fromList(it) + FindResultState.fromList(it) } } 176.toByte() -> { return (readValue(buffer) as? List)?.let { - WebExtensionData.fromList(it) + CustomSelectionAction.fromList(it) } } 177.toByte() -> { return (readValue(buffer) as? List)?.let { - GeckoSuggestion.fromList(it) + WebExtensionData.fromList(it) } } 178.toByte() -> { return (readValue(buffer) as? List)?.let { - TabContent.fromList(it) + GeckoSuggestion.fromList(it) } } 179.toByte() -> { return (readValue(buffer) as? List)?.let { - ContentBlocking.fromList(it) + TabContent.fromList(it) } } 180.toByte() -> { return (readValue(buffer) as? List)?.let { - DohSettings.fromList(it) + ContentBlocking.fromList(it) } } 181.toByte() -> { return (readValue(buffer) as? List)?.let { - GeckoEngineSettings.fromList(it) + DohSettings.fromList(it) } } 182.toByte() -> { return (readValue(buffer) as? List)?.let { - AutocompleteResult.fromList(it) + GeckoEngineSettings.fromList(it) } } 183.toByte() -> { return (readValue(buffer) as? List)?.let { - UnknownHitResult.fromList(it) + AutocompleteResult.fromList(it) } } 184.toByte() -> { return (readValue(buffer) as? List)?.let { - ImageHitResult.fromList(it) + UnknownHitResult.fromList(it) } } 185.toByte() -> { return (readValue(buffer) as? List)?.let { - VideoHitResult.fromList(it) + ImageHitResult.fromList(it) } } 186.toByte() -> { return (readValue(buffer) as? List)?.let { - AudioHitResult.fromList(it) + VideoHitResult.fromList(it) } } 187.toByte() -> { return (readValue(buffer) as? List)?.let { - ImageSrcHitResult.fromList(it) + AudioHitResult.fromList(it) } } 188.toByte() -> { return (readValue(buffer) as? List)?.let { - PhoneHitResult.fromList(it) + ImageSrcHitResult.fromList(it) } } 189.toByte() -> { return (readValue(buffer) as? List)?.let { - EmailHitResult.fromList(it) + PhoneHitResult.fromList(it) } } 190.toByte() -> { return (readValue(buffer) as? List)?.let { - GeoHitResult.fromList(it) + EmailHitResult.fromList(it) } } 191.toByte() -> { return (readValue(buffer) as? List)?.let { - DownloadState.fromList(it) + GeoHitResult.fromList(it) } } 192.toByte() -> { return (readValue(buffer) as? List)?.let { - ShareInternetResourceState.fromList(it) + DownloadState.fromList(it) } } 193.toByte() -> { return (readValue(buffer) as? List)?.let { - AddonCollection.fromList(it) + ShareInternetResourceState.fromList(it) } } 194.toByte() -> { return (readValue(buffer) as? List)?.let { - GeckoPref.fromList(it) + AddonCollection.fromList(it) } } 195.toByte() -> { return (readValue(buffer) as? List)?.let { - ContainerSiteAssignment.fromList(it) + GeckoPref.fromList(it) } } 196.toByte() -> { return (readValue(buffer) as? List)?.let { - GeckoHeader.fromList(it) + ContainerSiteAssignment.fromList(it) } } 197.toByte() -> { return (readValue(buffer) as? List)?.let { - GeckoFetchRequest.fromList(it) + GeckoHeader.fromList(it) } } 198.toByte() -> { + return (readValue(buffer) as? List)?.let { + GeckoFetchRequest.fromList(it) + } + } + 199.toByte() -> { return (readValue(buffer) as? List)?.let { GeckoFetchResponse.fromList(it) } } + 200.toByte() -> { + return (readValue(buffer) as? List)?.let { + BookmarkNode.fromList(it) + } + } + 201.toByte() -> { + return (readValue(buffer) as? List)?.let { + BookmarkInfo.fromList(it) + } + } else -> super.readValueOfType(type, buffer) } } @@ -2894,198 +3014,210 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { stream.write(150) writeValue(stream, value.raw.toLong()) } - is TranslationOptions -> { + is BookmarkNodeType -> { stream.write(151) - writeValue(stream, value.toList()) + writeValue(stream, value.raw.toLong()) } - is ReaderState -> { + is TranslationOptions -> { stream.write(152) writeValue(stream, value.toList()) } - is LastMediaAccessState -> { + is ReaderState -> { stream.write(153) writeValue(stream, value.toList()) } - is HistoryMetadataKey -> { + is LastMediaAccessState -> { stream.write(154) writeValue(stream, value.toList()) } - is PackageCategoryValue -> { + is HistoryMetadataKey -> { stream.write(155) writeValue(stream, value.toList()) } - is ExternalPackage -> { + is PackageCategoryValue -> { stream.write(156) writeValue(stream, value.toList()) } - is LoadUrlFlagsValue -> { + is ExternalPackage -> { stream.write(157) writeValue(stream, value.toList()) } - is SourceValue -> { + is LoadUrlFlagsValue -> { stream.write(158) writeValue(stream, value.toList()) } - is TabState -> { + is SourceValue -> { stream.write(159) writeValue(stream, value.toList()) } - is RecoverableTab -> { + is TabState -> { stream.write(160) writeValue(stream, value.toList()) } - is RecoverableBrowserState -> { + is RecoverableTab -> { stream.write(161) writeValue(stream, value.toList()) } - is IconRequest -> { + is RecoverableBrowserState -> { stream.write(162) writeValue(stream, value.toList()) } - is ResourceSize -> { + is IconRequest -> { stream.write(163) writeValue(stream, value.toList()) } - is Resource -> { + is ResourceSize -> { stream.write(164) writeValue(stream, value.toList()) } - is IconResult -> { + is Resource -> { stream.write(165) writeValue(stream, value.toList()) } - is CookiePartitionKey -> { + is IconResult -> { stream.write(166) writeValue(stream, value.toList()) } - is Cookie -> { + is CookiePartitionKey -> { stream.write(167) writeValue(stream, value.toList()) } - is VisitInfo -> { + is Cookie -> { stream.write(168) writeValue(stream, value.toList()) } - is HistoryItem -> { + is VisitInfo -> { stream.write(169) writeValue(stream, value.toList()) } - is HistoryState -> { + is HistoryItem -> { stream.write(170) writeValue(stream, value.toList()) } - is ReaderableState -> { + is HistoryState -> { stream.write(171) writeValue(stream, value.toList()) } - is SecurityInfoState -> { + is ReaderableState -> { stream.write(172) writeValue(stream, value.toList()) } - is TabContentState -> { + is SecurityInfoState -> { stream.write(173) writeValue(stream, value.toList()) } - is FindResultState -> { + is TabContentState -> { stream.write(174) writeValue(stream, value.toList()) } - is CustomSelectionAction -> { + is FindResultState -> { stream.write(175) writeValue(stream, value.toList()) } - is WebExtensionData -> { + is CustomSelectionAction -> { stream.write(176) writeValue(stream, value.toList()) } - is GeckoSuggestion -> { + is WebExtensionData -> { stream.write(177) writeValue(stream, value.toList()) } - is TabContent -> { + is GeckoSuggestion -> { stream.write(178) writeValue(stream, value.toList()) } - is ContentBlocking -> { + is TabContent -> { stream.write(179) writeValue(stream, value.toList()) } - is DohSettings -> { + is ContentBlocking -> { stream.write(180) writeValue(stream, value.toList()) } - is GeckoEngineSettings -> { + is DohSettings -> { stream.write(181) writeValue(stream, value.toList()) } - is AutocompleteResult -> { + is GeckoEngineSettings -> { stream.write(182) writeValue(stream, value.toList()) } - is UnknownHitResult -> { + is AutocompleteResult -> { stream.write(183) writeValue(stream, value.toList()) } - is ImageHitResult -> { + is UnknownHitResult -> { stream.write(184) writeValue(stream, value.toList()) } - is VideoHitResult -> { + is ImageHitResult -> { stream.write(185) writeValue(stream, value.toList()) } - is AudioHitResult -> { + is VideoHitResult -> { stream.write(186) writeValue(stream, value.toList()) } - is ImageSrcHitResult -> { + is AudioHitResult -> { stream.write(187) writeValue(stream, value.toList()) } - is PhoneHitResult -> { + is ImageSrcHitResult -> { stream.write(188) writeValue(stream, value.toList()) } - is EmailHitResult -> { + is PhoneHitResult -> { stream.write(189) writeValue(stream, value.toList()) } - is GeoHitResult -> { + is EmailHitResult -> { stream.write(190) writeValue(stream, value.toList()) } - is DownloadState -> { + is GeoHitResult -> { stream.write(191) writeValue(stream, value.toList()) } - is ShareInternetResourceState -> { + is DownloadState -> { stream.write(192) writeValue(stream, value.toList()) } - is AddonCollection -> { + is ShareInternetResourceState -> { stream.write(193) writeValue(stream, value.toList()) } - is GeckoPref -> { + is AddonCollection -> { stream.write(194) writeValue(stream, value.toList()) } - is ContainerSiteAssignment -> { + is GeckoPref -> { stream.write(195) writeValue(stream, value.toList()) } - is GeckoHeader -> { + is ContainerSiteAssignment -> { stream.write(196) writeValue(stream, value.toList()) } - is GeckoFetchRequest -> { + is GeckoHeader -> { stream.write(197) writeValue(stream, value.toList()) } - is GeckoFetchResponse -> { + is GeckoFetchRequest -> { stream.write(198) writeValue(stream, value.toList()) } + is GeckoFetchResponse -> { + stream.write(199) + writeValue(stream, value.toList()) + } + is BookmarkNode -> { + stream.write(200) + writeValue(stream, value.toList()) + } + is BookmarkInfo -> { + stream.write(201) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -5594,3 +5726,286 @@ interface GeckoFetchApi { } } } +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface GeckoBookmarksApi { + /** + * Produces a bookmarks tree for the given guid string. + * + * @param guid The bookmark guid to obtain. + * @param recursive Whether to recurse and obtain all levels of children. + * @return The populated root starting from the guid. + */ + fun getTree(guid: String, recursive: Boolean, callback: (Result) -> Unit) + /** + * Obtains the details of a bookmark without children, if one exists with that guid. Otherwise, null. + * + * @param guid The bookmark guid to obtain. + * @return The bookmark node or null if it does not exist. + */ + fun getBookmark(guid: String, callback: (Result) -> Unit) + /** + * Produces a list of all bookmarks with the given URL. + * + * @param url The URL string. + * @return The list of bookmarks that match the URL + */ + fun getBookmarksWithUrl(url: String, callback: (Result>) -> Unit) + /** + * Produces a list of the most recently added bookmarks. + * + * @param limit The maximum number of entries to return. + * @param maxAge Optional parameter used to filter out entries older than this number of milliseconds. + * @param currentTime Optional parameter for current time. Defaults toSystem.currentTimeMillis() + * @return The list of bookmarks that have been recently added up to the limit number of items. + */ + fun getRecentBookmarks(limit: Long, maxAge: Long?, currentTime: Long, callback: (Result>) -> Unit) + /** + * Searches bookmarks with a query string. + * + * @param query The query string to search. + * @param limit The maximum number of entries to return. + * @return The list of matching bookmark nodes up to the limit number of items. + */ + fun searchBookmarks(query: String, limit: Long, callback: (Result>) -> Unit) + /** + * Adds a new bookmark item to a given node. + * + * Sync behavior: will add new bookmark item to remote devices. + * + * @param parentGuid The parent guid of the new node. + * @param url The URL of the bookmark item to add. + * @param title The title of the bookmark item to add. + * @param position The optional position to add the new node or null to append. + * @return The guid of the newly inserted bookmark item. + */ + fun addItem(parentGuid: String, url: String, title: String, position: Long?, callback: (Result) -> Unit) + /** + * Adds a new bookmark folder to a given node. + * + * Sync behavior: will add new separator to remote devices. + * + * @param parentGuid The parent guid of the new node. + * @param title The title of the bookmark folder to add. + * @param position The optional position to add the new node or null to append. + * @return The guid of the newly inserted bookmark item. + */ + fun addFolder(parentGuid: String, title: String, position: Long?, callback: (Result) -> Unit) + /** + * Edits the properties of an existing bookmark item and/or moves an existing one underneath a new parent guid. + * + * Sync behavior: will alter bookmark item on remote devices. + * + * @param guid The guid of the item to update. + * @param info The info to change in the bookmark. + */ + fun updateNode(guid: String, info: BookmarkInfo, callback: (Result) -> Unit) + /** + * Deletes a bookmark node and all of its children, if any. + * + * Sync behavior: will remove bookmark from remote devices. + * + * @return Whether the bookmark existed or not. + */ + fun deleteNode(guid: String, callback: (Result) -> Unit) + + companion object { + /** The codec used by GeckoBookmarksApi. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + /** Sets up an instance of `GeckoBookmarksApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: GeckoBookmarksApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getTree$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val guidArg = args[0] as String + val recursiveArg = args[1] as Boolean + api.getTree(guidArg, recursiveArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeckoPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(GeckoPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getBookmark$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val guidArg = args[0] as String + api.getBookmark(guidArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeckoPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(GeckoPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getBookmarksWithUrl$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val urlArg = args[0] as String + api.getBookmarksWithUrl(urlArg) { result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeckoPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(GeckoPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getRecentBookmarks$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val limitArg = args[0] as Long + val maxAgeArg = args[1] as Long? + val currentTimeArg = args[2] as Long + api.getRecentBookmarks(limitArg, maxAgeArg, currentTimeArg) { result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeckoPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(GeckoPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.searchBookmarks$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val queryArg = args[0] as String + val limitArg = args[1] as Long + api.searchBookmarks(queryArg, limitArg) { result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeckoPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(GeckoPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.addItem$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val parentGuidArg = args[0] as String + val urlArg = args[1] as String + val titleArg = args[2] as String + val positionArg = args[3] as Long? + api.addItem(parentGuidArg, urlArg, titleArg, positionArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeckoPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(GeckoPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.addFolder$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val parentGuidArg = args[0] as String + val titleArg = args[1] as String + val positionArg = args[2] as Long? + api.addFolder(parentGuidArg, titleArg, positionArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeckoPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(GeckoPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.updateNode$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val guidArg = args[0] as String + val infoArg = args[1] as BookmarkInfo + api.updateNode(guidArg, infoArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeckoPigeonUtils.wrapError(error)) + } else { + reply.reply(GeckoPigeonUtils.wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.deleteNode$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val guidArg = args[0] as String + api.deleteNode(guidArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeckoPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(GeckoPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart index 3e357761..a75207b1 100644 --- a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart +++ b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart @@ -8,6 +8,7 @@ export 'src/data/models/load_url_flags.dart'; export 'src/data/models/source.dart'; export 'src/domain/entities/default_selection_actions.dart'; export 'src/domain/services/gecko_addon.dart'; +export 'src/domain/services/gecko_bookmarks.dart'; export 'src/domain/services/gecko_browser.dart'; export 'src/domain/services/gecko_browser_extension.dart'; export 'src/domain/services/gecko_container_proxy.dart'; @@ -34,6 +35,9 @@ export 'src/pigeons/gecko.g.dart' show AddonCollection, AudioHitResult, + BookmarkInfo, + BookmarkNode, + BookmarkNodeType, BounceTrackingProtectionMode, ColorScheme, ContentBlocking, diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_bookmarks.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_bookmarks.dart new file mode 100644 index 00000000..35466646 --- /dev/null +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_bookmarks.dart @@ -0,0 +1,131 @@ +import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; + +/// Enumeration of the ids of the roots of the bookmarks tree. +/// +/// There are 5 "roots" in the bookmark tree. The actual root +/// (which has no parent), and it's 4 children (which have the +/// actual root as their parent). +/// +/// You cannot delete or move any of these items. +enum BookmarkRoot { + root("root________", "Default"), + menu("menu________", "Menu"), + toolbar("toolbar_____", "Toolbar"), + unfiled("unfiled_____", "Unified"), + mobile("mobile______", "WebLibre"); + + final String id; + final String displayName; + + const BookmarkRoot(this.id, this.displayName); +} + +final bookmarkRootIds = BookmarkRoot.values.map((e) => e.id).toSet(); +final bookmarkRootDisplayNames = Map.fromEntries( + BookmarkRoot.values.map((e) => MapEntry(e.id, e.displayName)), +); + +final _api = GeckoBookmarksApi(); + +class GeckoBookmarksService { + /// Produces a bookmarks tree for the given guid string. + /// + /// @param guid The bookmark guid to obtain. + /// @param recursive Whether to recurse and obtain all levels of children. + /// @return The populated root starting from the guid. + Future getTree(String guid, {bool recursive = false}) { + return _api.getTree(guid, recursive); + } + + /// Obtains the details of a bookmark without children, if one exists with that guid. Otherwise, null. + /// + /// @param guid The bookmark guid to obtain. + /// @return The bookmark node or null if it does not exist. + Future getBookmark(String guid) { + return _api.getBookmark(guid); + } + + /// Produces a list of all bookmarks with the given URL. + /// + /// @param url The URL string. + /// @return The list of bookmarks that match the URL + Future> getBookmarksWithUrl(Uri url) { + return _api.getBookmarksWithUrl(url.toString()); + } + + /// Produces a list of the most recently added bookmarks. + /// + /// @param limit The maximum number of entries to return. + /// @param maxAge Optional parameter used to filter out entries older than this number of milliseconds. + /// @param currentTime Optional parameter for current time. Defaults toSystem.currentTimeMillis() + /// @return The list of bookmarks that have been recently added up to the limit number of items. + Future> getRecentBookmarks( + int limit, { + Duration maxAge = Duration.zero, + DateTime? currentTime, + }) { + return _api.getRecentBookmarks( + limit, + maxAge.inMilliseconds, + (currentTime ?? DateTime.now()).millisecondsSinceEpoch, + ); + } + + /// Searches bookmarks with a query string. + /// + /// @param query The query string to search. + /// @param limit The maximum number of entries to return. + /// @return The list of matching bookmark nodes up to the limit number of items. + Future> searchBookmarks(String query, {int limit = 10}) { + return _api.searchBookmarks(query, limit); + } + + /// Adds a new bookmark item to a given node. + /// + /// Sync behavior: will add new bookmark item to remote devices. + /// + /// @param parentGuid The parent guid of the new node. + /// @param url The URL of the bookmark item to add. + /// @param title The title of the bookmark item to add. + /// @param position The optional position to add the new node or null to append. + /// @return The guid of the newly inserted bookmark item. + Future addItem( + String parentGuid, + Uri url, + String title, + int? position, + ) { + return _api.addItem(parentGuid, url.toString(), title, position); + } + + /// Adds a new bookmark folder to a given node. + /// + /// Sync behavior: will add new separator to remote devices. + /// + /// @param parentGuid The parent guid of the new node. + /// @param title The title of the bookmark folder to add. + /// @param position The optional position to add the new node or null to append. + /// @return The guid of the newly inserted bookmark item. + Future addFolder(String parentGuid, String title, int? position) { + return _api.addFolder(parentGuid, title, position); + } + + /// Edits the properties of an existing bookmark item and/or moves an existing one underneath a new parent guid. + /// + /// Sync behavior: will alter bookmark item on remote devices. + /// + /// @param guid The guid of the item to update. + /// @param info The info to change in the bookmark. + Future updateNode(String guid, BookmarkInfo info) { + return _api.updateNode(guid, info); + } + + /// Deletes a bookmark node and all of its children, if any. + /// + /// Sync behavior: will remove bookmark from remote devices. + /// + /// @return Whether the bookmark existed or not. + Future deleteNode(String guid) { + return _api.deleteNode(guid); + } +} diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index 18342a01..6e04fd16 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -238,6 +238,12 @@ enum GeckoFetchCookiePolicy { omit, } +enum BookmarkNodeType { + item, + folder, + separator, +} + /// Translation options that map to the Gecko Translations Options. /// /// @property downloadModel If the necessary models should be downloaded on request. If false, then @@ -3139,6 +3145,144 @@ class GeckoFetchResponse { ; } +class BookmarkNode { + BookmarkNode({ + required this.type, + required this.guid, + this.parentGuid, + this.position, + this.title, + this.url, + required this.dateAdded, + required this.lastModified, + this.children, + }); + + BookmarkNodeType type; + + String guid; + + String? parentGuid; + + int? position; + + String? title; + + String? url; + + int dateAdded; + + int lastModified; + + List? children; + + List _toList() { + return [ + type, + guid, + parentGuid, + position, + title, + url, + dateAdded, + lastModified, + children, + ]; + } + + Object encode() { + return _toList(); } + + static BookmarkNode decode(Object result) { + result as List; + return BookmarkNode( + type: result[0]! as BookmarkNodeType, + guid: result[1]! as String, + parentGuid: result[2] as String?, + position: result[3] as int?, + title: result[4] as String?, + url: result[5] as String?, + dateAdded: result[6]! as int, + lastModified: result[7]! as int, + children: (result[8] as List?)?.cast(), + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! BookmarkNode || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + +/// Class for making alterations to any bookmark node +class BookmarkInfo { + BookmarkInfo({ + this.parentGuid, + this.position, + this.title, + this.url, + }); + + String? parentGuid; + + int? position; + + String? title; + + String? url; + + List _toList() { + return [ + parentGuid, + position, + title, + url, + ]; + } + + Object encode() { + return _toList(); } + + static BookmarkInfo decode(Object result) { + result as List; + return BookmarkInfo( + parentGuid: result[0] as String?, + position: result[1] as int?, + title: result[2] as String?, + url: result[3] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! BookmarkInfo || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @@ -3213,150 +3357,159 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is GeckoFetchCookiePolicy) { buffer.putUint8(150); writeValue(buffer, value.index); - } else if (value is TranslationOptions) { + } else if (value is BookmarkNodeType) { buffer.putUint8(151); - writeValue(buffer, value.encode()); - } else if (value is ReaderState) { + writeValue(buffer, value.index); + } else if (value is TranslationOptions) { buffer.putUint8(152); writeValue(buffer, value.encode()); - } else if (value is LastMediaAccessState) { + } else if (value is ReaderState) { buffer.putUint8(153); writeValue(buffer, value.encode()); - } else if (value is HistoryMetadataKey) { + } else if (value is LastMediaAccessState) { buffer.putUint8(154); writeValue(buffer, value.encode()); - } else if (value is PackageCategoryValue) { + } else if (value is HistoryMetadataKey) { buffer.putUint8(155); writeValue(buffer, value.encode()); - } else if (value is ExternalPackage) { + } else if (value is PackageCategoryValue) { buffer.putUint8(156); writeValue(buffer, value.encode()); - } else if (value is LoadUrlFlagsValue) { + } else if (value is ExternalPackage) { buffer.putUint8(157); writeValue(buffer, value.encode()); - } else if (value is SourceValue) { + } else if (value is LoadUrlFlagsValue) { buffer.putUint8(158); writeValue(buffer, value.encode()); - } else if (value is TabState) { + } else if (value is SourceValue) { buffer.putUint8(159); writeValue(buffer, value.encode()); - } else if (value is RecoverableTab) { + } else if (value is TabState) { buffer.putUint8(160); writeValue(buffer, value.encode()); - } else if (value is RecoverableBrowserState) { + } else if (value is RecoverableTab) { buffer.putUint8(161); writeValue(buffer, value.encode()); - } else if (value is IconRequest) { + } else if (value is RecoverableBrowserState) { buffer.putUint8(162); writeValue(buffer, value.encode()); - } else if (value is ResourceSize) { + } else if (value is IconRequest) { buffer.putUint8(163); writeValue(buffer, value.encode()); - } else if (value is Resource) { + } else if (value is ResourceSize) { buffer.putUint8(164); writeValue(buffer, value.encode()); - } else if (value is IconResult) { + } else if (value is Resource) { buffer.putUint8(165); writeValue(buffer, value.encode()); - } else if (value is CookiePartitionKey) { + } else if (value is IconResult) { buffer.putUint8(166); writeValue(buffer, value.encode()); - } else if (value is Cookie) { + } else if (value is CookiePartitionKey) { buffer.putUint8(167); writeValue(buffer, value.encode()); - } else if (value is VisitInfo) { + } else if (value is Cookie) { buffer.putUint8(168); writeValue(buffer, value.encode()); - } else if (value is HistoryItem) { + } else if (value is VisitInfo) { buffer.putUint8(169); writeValue(buffer, value.encode()); - } else if (value is HistoryState) { + } else if (value is HistoryItem) { buffer.putUint8(170); writeValue(buffer, value.encode()); - } else if (value is ReaderableState) { + } else if (value is HistoryState) { buffer.putUint8(171); writeValue(buffer, value.encode()); - } else if (value is SecurityInfoState) { + } else if (value is ReaderableState) { buffer.putUint8(172); writeValue(buffer, value.encode()); - } else if (value is TabContentState) { + } else if (value is SecurityInfoState) { buffer.putUint8(173); writeValue(buffer, value.encode()); - } else if (value is FindResultState) { + } else if (value is TabContentState) { buffer.putUint8(174); writeValue(buffer, value.encode()); - } else if (value is CustomSelectionAction) { + } else if (value is FindResultState) { buffer.putUint8(175); writeValue(buffer, value.encode()); - } else if (value is WebExtensionData) { + } else if (value is CustomSelectionAction) { buffer.putUint8(176); writeValue(buffer, value.encode()); - } else if (value is GeckoSuggestion) { + } else if (value is WebExtensionData) { buffer.putUint8(177); writeValue(buffer, value.encode()); - } else if (value is TabContent) { + } else if (value is GeckoSuggestion) { buffer.putUint8(178); writeValue(buffer, value.encode()); - } else if (value is ContentBlocking) { + } else if (value is TabContent) { buffer.putUint8(179); writeValue(buffer, value.encode()); - } else if (value is DohSettings) { + } else if (value is ContentBlocking) { buffer.putUint8(180); writeValue(buffer, value.encode()); - } else if (value is GeckoEngineSettings) { + } else if (value is DohSettings) { buffer.putUint8(181); writeValue(buffer, value.encode()); - } else if (value is AutocompleteResult) { + } else if (value is GeckoEngineSettings) { buffer.putUint8(182); writeValue(buffer, value.encode()); - } else if (value is UnknownHitResult) { + } else if (value is AutocompleteResult) { buffer.putUint8(183); writeValue(buffer, value.encode()); - } else if (value is ImageHitResult) { + } else if (value is UnknownHitResult) { buffer.putUint8(184); writeValue(buffer, value.encode()); - } else if (value is VideoHitResult) { + } else if (value is ImageHitResult) { buffer.putUint8(185); writeValue(buffer, value.encode()); - } else if (value is AudioHitResult) { + } else if (value is VideoHitResult) { buffer.putUint8(186); writeValue(buffer, value.encode()); - } else if (value is ImageSrcHitResult) { + } else if (value is AudioHitResult) { buffer.putUint8(187); writeValue(buffer, value.encode()); - } else if (value is PhoneHitResult) { + } else if (value is ImageSrcHitResult) { buffer.putUint8(188); writeValue(buffer, value.encode()); - } else if (value is EmailHitResult) { + } else if (value is PhoneHitResult) { buffer.putUint8(189); writeValue(buffer, value.encode()); - } else if (value is GeoHitResult) { + } else if (value is EmailHitResult) { buffer.putUint8(190); writeValue(buffer, value.encode()); - } else if (value is DownloadState) { + } else if (value is GeoHitResult) { buffer.putUint8(191); writeValue(buffer, value.encode()); - } else if (value is ShareInternetResourceState) { + } else if (value is DownloadState) { buffer.putUint8(192); writeValue(buffer, value.encode()); - } else if (value is AddonCollection) { + } else if (value is ShareInternetResourceState) { buffer.putUint8(193); writeValue(buffer, value.encode()); - } else if (value is GeckoPref) { + } else if (value is AddonCollection) { buffer.putUint8(194); writeValue(buffer, value.encode()); - } else if (value is ContainerSiteAssignment) { + } else if (value is GeckoPref) { buffer.putUint8(195); writeValue(buffer, value.encode()); - } else if (value is GeckoHeader) { + } else if (value is ContainerSiteAssignment) { buffer.putUint8(196); writeValue(buffer, value.encode()); - } else if (value is GeckoFetchRequest) { + } else if (value is GeckoHeader) { buffer.putUint8(197); writeValue(buffer, value.encode()); - } else if (value is GeckoFetchResponse) { + } else if (value is GeckoFetchRequest) { buffer.putUint8(198); writeValue(buffer, value.encode()); + } else if (value is GeckoFetchResponse) { + buffer.putUint8(199); + writeValue(buffer, value.encode()); + } else if (value is BookmarkNode) { + buffer.putUint8(200); + writeValue(buffer, value.encode()); + } else if (value is BookmarkInfo) { + buffer.putUint8(201); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -3432,101 +3585,108 @@ class _PigeonCodec extends StandardMessageCodec { final int? value = readValue(buffer) as int?; return value == null ? null : GeckoFetchCookiePolicy.values[value]; case 151: - return TranslationOptions.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : BookmarkNodeType.values[value]; case 152: - return ReaderState.decode(readValue(buffer)!); + return TranslationOptions.decode(readValue(buffer)!); case 153: - return LastMediaAccessState.decode(readValue(buffer)!); + return ReaderState.decode(readValue(buffer)!); case 154: - return HistoryMetadataKey.decode(readValue(buffer)!); + return LastMediaAccessState.decode(readValue(buffer)!); case 155: - return PackageCategoryValue.decode(readValue(buffer)!); + return HistoryMetadataKey.decode(readValue(buffer)!); case 156: - return ExternalPackage.decode(readValue(buffer)!); + return PackageCategoryValue.decode(readValue(buffer)!); case 157: - return LoadUrlFlagsValue.decode(readValue(buffer)!); + return ExternalPackage.decode(readValue(buffer)!); case 158: - return SourceValue.decode(readValue(buffer)!); + return LoadUrlFlagsValue.decode(readValue(buffer)!); case 159: - return TabState.decode(readValue(buffer)!); + return SourceValue.decode(readValue(buffer)!); case 160: - return RecoverableTab.decode(readValue(buffer)!); + return TabState.decode(readValue(buffer)!); case 161: - return RecoverableBrowserState.decode(readValue(buffer)!); + return RecoverableTab.decode(readValue(buffer)!); case 162: - return IconRequest.decode(readValue(buffer)!); + return RecoverableBrowserState.decode(readValue(buffer)!); case 163: - return ResourceSize.decode(readValue(buffer)!); + return IconRequest.decode(readValue(buffer)!); case 164: - return Resource.decode(readValue(buffer)!); + return ResourceSize.decode(readValue(buffer)!); case 165: - return IconResult.decode(readValue(buffer)!); + return Resource.decode(readValue(buffer)!); case 166: - return CookiePartitionKey.decode(readValue(buffer)!); + return IconResult.decode(readValue(buffer)!); case 167: - return Cookie.decode(readValue(buffer)!); + return CookiePartitionKey.decode(readValue(buffer)!); case 168: - return VisitInfo.decode(readValue(buffer)!); + return Cookie.decode(readValue(buffer)!); case 169: - return HistoryItem.decode(readValue(buffer)!); + return VisitInfo.decode(readValue(buffer)!); case 170: - return HistoryState.decode(readValue(buffer)!); + return HistoryItem.decode(readValue(buffer)!); case 171: - return ReaderableState.decode(readValue(buffer)!); + return HistoryState.decode(readValue(buffer)!); case 172: - return SecurityInfoState.decode(readValue(buffer)!); + return ReaderableState.decode(readValue(buffer)!); case 173: - return TabContentState.decode(readValue(buffer)!); + return SecurityInfoState.decode(readValue(buffer)!); case 174: - return FindResultState.decode(readValue(buffer)!); + return TabContentState.decode(readValue(buffer)!); case 175: - return CustomSelectionAction.decode(readValue(buffer)!); + return FindResultState.decode(readValue(buffer)!); case 176: - return WebExtensionData.decode(readValue(buffer)!); + return CustomSelectionAction.decode(readValue(buffer)!); case 177: - return GeckoSuggestion.decode(readValue(buffer)!); + return WebExtensionData.decode(readValue(buffer)!); case 178: - return TabContent.decode(readValue(buffer)!); + return GeckoSuggestion.decode(readValue(buffer)!); case 179: - return ContentBlocking.decode(readValue(buffer)!); + return TabContent.decode(readValue(buffer)!); case 180: - return DohSettings.decode(readValue(buffer)!); + return ContentBlocking.decode(readValue(buffer)!); case 181: - return GeckoEngineSettings.decode(readValue(buffer)!); + return DohSettings.decode(readValue(buffer)!); case 182: - return AutocompleteResult.decode(readValue(buffer)!); + return GeckoEngineSettings.decode(readValue(buffer)!); case 183: - return UnknownHitResult.decode(readValue(buffer)!); + return AutocompleteResult.decode(readValue(buffer)!); case 184: - return ImageHitResult.decode(readValue(buffer)!); + return UnknownHitResult.decode(readValue(buffer)!); case 185: - return VideoHitResult.decode(readValue(buffer)!); + return ImageHitResult.decode(readValue(buffer)!); case 186: - return AudioHitResult.decode(readValue(buffer)!); + return VideoHitResult.decode(readValue(buffer)!); case 187: - return ImageSrcHitResult.decode(readValue(buffer)!); + return AudioHitResult.decode(readValue(buffer)!); case 188: - return PhoneHitResult.decode(readValue(buffer)!); + return ImageSrcHitResult.decode(readValue(buffer)!); case 189: - return EmailHitResult.decode(readValue(buffer)!); + return PhoneHitResult.decode(readValue(buffer)!); case 190: - return GeoHitResult.decode(readValue(buffer)!); + return EmailHitResult.decode(readValue(buffer)!); case 191: - return DownloadState.decode(readValue(buffer)!); + return GeoHitResult.decode(readValue(buffer)!); case 192: - return ShareInternetResourceState.decode(readValue(buffer)!); + return DownloadState.decode(readValue(buffer)!); case 193: - return AddonCollection.decode(readValue(buffer)!); + return ShareInternetResourceState.decode(readValue(buffer)!); case 194: - return GeckoPref.decode(readValue(buffer)!); + return AddonCollection.decode(readValue(buffer)!); case 195: - return ContainerSiteAssignment.decode(readValue(buffer)!); + return GeckoPref.decode(readValue(buffer)!); case 196: - return GeckoHeader.decode(readValue(buffer)!); + return ContainerSiteAssignment.decode(readValue(buffer)!); case 197: - return GeckoFetchRequest.decode(readValue(buffer)!); + return GeckoHeader.decode(readValue(buffer)!); case 198: + return GeckoFetchRequest.decode(readValue(buffer)!); + case 199: return GeckoFetchResponse.decode(readValue(buffer)!); + case 200: + return BookmarkNode.decode(readValue(buffer)!); + case 201: + return BookmarkInfo.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -6734,3 +6894,306 @@ class GeckoFetchApi { } } } + +class GeckoBookmarksApi { + /// Constructor for [GeckoBookmarksApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + GeckoBookmarksApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// Produces a bookmarks tree for the given guid string. + /// + /// @param guid The bookmark guid to obtain. + /// @param recursive Whether to recurse and obtain all levels of children. + /// @return The populated root starting from the guid. + Future getTree(String guid, bool recursive) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getTree$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([guid, recursive]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return (pigeonVar_replyList[0] as BookmarkNode?); + } + } + + /// Obtains the details of a bookmark without children, if one exists with that guid. Otherwise, null. + /// + /// @param guid The bookmark guid to obtain. + /// @return The bookmark node or null if it does not exist. + Future getBookmark(String guid) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getBookmark$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([guid]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return (pigeonVar_replyList[0] as BookmarkNode?); + } + } + + /// Produces a list of all bookmarks with the given URL. + /// + /// @param url The URL string. + /// @return The list of bookmarks that match the URL + Future> getBookmarksWithUrl(String url) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getBookmarksWithUrl$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([url]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + + /// Produces a list of the most recently added bookmarks. + /// + /// @param limit The maximum number of entries to return. + /// @param maxAge Optional parameter used to filter out entries older than this number of milliseconds. + /// @param currentTime Optional parameter for current time. Defaults toSystem.currentTimeMillis() + /// @return The list of bookmarks that have been recently added up to the limit number of items. + Future> getRecentBookmarks(int limit, int? maxAge, int currentTime) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.getRecentBookmarks$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([limit, maxAge, currentTime]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + + /// Searches bookmarks with a query string. + /// + /// @param query The query string to search. + /// @param limit The maximum number of entries to return. + /// @return The list of matching bookmark nodes up to the limit number of items. + Future> searchBookmarks(String query, int limit) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.searchBookmarks$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([query, limit]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + + /// Adds a new bookmark item to a given node. + /// + /// Sync behavior: will add new bookmark item to remote devices. + /// + /// @param parentGuid The parent guid of the new node. + /// @param url The URL of the bookmark item to add. + /// @param title The title of the bookmark item to add. + /// @param position The optional position to add the new node or null to append. + /// @return The guid of the newly inserted bookmark item. + Future addItem(String parentGuid, String url, String title, int? position) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.addItem$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([parentGuid, url, title, position]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as String?)!; + } + } + + /// Adds a new bookmark folder to a given node. + /// + /// Sync behavior: will add new separator to remote devices. + /// + /// @param parentGuid The parent guid of the new node. + /// @param title The title of the bookmark folder to add. + /// @param position The optional position to add the new node or null to append. + /// @return The guid of the newly inserted bookmark item. + Future addFolder(String parentGuid, String title, int? position) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.addFolder$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([parentGuid, title, position]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as String?)!; + } + } + + /// Edits the properties of an existing bookmark item and/or moves an existing one underneath a new parent guid. + /// + /// Sync behavior: will alter bookmark item on remote devices. + /// + /// @param guid The guid of the item to update. + /// @param info The info to change in the bookmark. + Future updateNode(String guid, BookmarkInfo info) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.updateNode$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([guid, info]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + /// Deletes a bookmark node and all of its children, if any. + /// + /// Sync behavior: will remove bookmark from remote devices. + /// + /// @return Whether the bookmark existed or not. + Future deleteNode(String guid) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.deleteNode$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([guid]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } +} diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index be3e1e52..0b927dd7 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -1426,3 +1426,130 @@ abstract class GeckoFetchApi { @async GeckoFetchResponse fetch(GeckoFetchRequest request); } + +enum BookmarkNodeType { item, folder, separator } + +class BookmarkNode { + final BookmarkNodeType type; + final String guid; + final String? parentGuid; + final int? position; + final String? title; + final String? url; + final int dateAdded; + final int lastModified; + final List? children; + + BookmarkNode({ + required this.type, + required this.guid, + required this.parentGuid, + required this.position, + required this.title, + required this.url, + required this.dateAdded, + required this.lastModified, + required this.children, + }); +} + +/// Class for making alterations to any bookmark node +class BookmarkInfo { + final String? parentGuid; + final int? position; + final String? title; + final String? url; + + BookmarkInfo({ + required this.parentGuid, + required this.position, + required this.title, + required this.url, + }); +} + +@HostApi() +abstract class GeckoBookmarksApi { + /// Produces a bookmarks tree for the given guid string. + /// + /// @param guid The bookmark guid to obtain. + /// @param recursive Whether to recurse and obtain all levels of children. + /// @return The populated root starting from the guid. + @async + BookmarkNode? getTree(String guid, bool recursive); + + /// Obtains the details of a bookmark without children, if one exists with that guid. Otherwise, null. + /// + /// @param guid The bookmark guid to obtain. + /// @return The bookmark node or null if it does not exist. + @async + BookmarkNode? getBookmark(String guid); + + /// Produces a list of all bookmarks with the given URL. + /// + /// @param url The URL string. + /// @return The list of bookmarks that match the URL + @async + List getBookmarksWithUrl(String url); + + /// Produces a list of the most recently added bookmarks. + /// + /// @param limit The maximum number of entries to return. + /// @param maxAge Optional parameter used to filter out entries older than this number of milliseconds. + /// @param currentTime Optional parameter for current time. Defaults toSystem.currentTimeMillis() + /// @return The list of bookmarks that have been recently added up to the limit number of items. + @async + List getRecentBookmarks( + int limit, + int? maxAge, + int currentTime, + ); + + /// Searches bookmarks with a query string. + /// + /// @param query The query string to search. + /// @param limit The maximum number of entries to return. + /// @return The list of matching bookmark nodes up to the limit number of items. + @async + List searchBookmarks(String query, int limit); + + /// Adds a new bookmark item to a given node. + /// + /// Sync behavior: will add new bookmark item to remote devices. + /// + /// @param parentGuid The parent guid of the new node. + /// @param url The URL of the bookmark item to add. + /// @param title The title of the bookmark item to add. + /// @param position The optional position to add the new node or null to append. + /// @return The guid of the newly inserted bookmark item. + @async + String addItem(String parentGuid, String url, String title, int? position); + + /// Adds a new bookmark folder to a given node. + /// + /// Sync behavior: will add new separator to remote devices. + /// + /// @param parentGuid The parent guid of the new node. + /// @param title The title of the bookmark folder to add. + /// @param position The optional position to add the new node or null to append. + /// @return The guid of the newly inserted bookmark item. + @async + String addFolder(String parentGuid, String title, int? position); + + /// Edits the properties of an existing bookmark item and/or moves an existing one underneath a new parent guid. + /// + /// Sync behavior: will alter bookmark item on remote devices. + /// + /// @param guid The guid of the item to update. + /// @param info The info to change in the bookmark. + @async + void updateNode(String guid, BookmarkInfo info); + + /// Deletes a bookmark node and all of its children, if any. + /// + /// Sync behavior: will remove bookmark from remote devices. + /// + /// @return Whether the bookmark existed or not. + @async + bool deleteNode(String guid); +}