bookmark feature rewrite
This commit is contained in:
+266
@@ -2,7 +2,9 @@ package eu.weblibre.flutter_mozilla_components.api
|
||||
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.feature.GeckoBookmarksExtensionBridge
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.BookmarkImportNode
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.BookmarkInfo
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.BookmarkInsertTreeResult
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.BookmarkNode
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.BookmarkNodeType
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoBookmarksApi
|
||||
@@ -11,10 +13,19 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.concept.storage.BookmarkInfo as MozillaBookmarkInfo
|
||||
import mozilla.components.concept.storage.bookmarks.InsertableBookmarkTreeNode
|
||||
import mozilla.components.concept.storage.bookmarks.InsertableBookmarkTreeRoot
|
||||
|
||||
class GeckoBookmarksApiImpl() : GeckoBookmarksApi {
|
||||
companion object {
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
/**
|
||||
* Name of the short-lived folder that loose top-level nodes pass through
|
||||
* during an import. Only visible if an import is interrupted partway.
|
||||
*/
|
||||
private const val SCRATCH_FOLDER_TITLE = "Importing bookmarks…"
|
||||
}
|
||||
|
||||
private val components by lazy {
|
||||
@@ -231,6 +242,261 @@ class GeckoBookmarksApiImpl() : GeckoBookmarksApi {
|
||||
}
|
||||
}
|
||||
|
||||
override fun insertTree(
|
||||
parentGuid: String,
|
||||
children: List<BookmarkImportNode>,
|
||||
callback: (Result<BookmarkInsertTreeResult>) -> Unit
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
// Imports can carry tens of thousands of nodes, so the whole batch runs
|
||||
// off the main thread. Only the callback returns to it, because Pigeon
|
||||
// replies must be delivered on the platform thread.
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
runCatching { insertImportNodes(parentGuid, children) }
|
||||
}
|
||||
callback(result)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends [nodes] underneath [parentGuid], handing every top-level folder to
|
||||
* storage as a single tree insertion.
|
||||
*
|
||||
* `insertTree` is the only storage call that carries timestamps, and it can
|
||||
* only create a *folder*. Loose top-level items and separators would
|
||||
* therefore lose their `ADD_DATE` if inserted with `addItem`/`addSeparator`,
|
||||
* which have no timestamp parameters — so they are staged inside a scratch
|
||||
* folder and reparented instead. See [stageLooseNodes].
|
||||
*
|
||||
* A failing top-level node is counted and skipped rather than aborting the
|
||||
* whole import, matching the per-node importer this replaced. Deliberately
|
||||
* does not emit `bookmarks.onCreated`: one event per imported node would
|
||||
* flood every installed WebExtension.
|
||||
*/
|
||||
private suspend fun insertImportNodes(
|
||||
parentGuid: String,
|
||||
nodes: List<BookmarkImportNode>
|
||||
): BookmarkInsertTreeResult {
|
||||
val storage = components.core.bookmarksStorage
|
||||
var insertedItemCount = 0L
|
||||
var failedNodeCount = 0L
|
||||
|
||||
val staged = stageLooseNodes(parentGuid, nodes)
|
||||
|
||||
for (node in nodes) {
|
||||
// Every branch appends (position = null). Walking the nodes in order
|
||||
// therefore reproduces the file's order, and merging into a folder
|
||||
// that already has children leaves those in place.
|
||||
val outcome: Result<Long> = when (node.type) {
|
||||
BookmarkNodeType.FOLDER -> {
|
||||
val folder = node.toInsertableFolder(position = null)
|
||||
storage.insertTree(InsertableBookmarkTreeRoot(parentGuid, folder))
|
||||
.map { folder.itemCount() }
|
||||
}
|
||||
|
||||
// Already written by stageLooseNodes; only the move is left.
|
||||
else -> staged.reparent(node, parentGuid)
|
||||
}
|
||||
|
||||
outcome.fold(
|
||||
{ count -> insertedItemCount += count },
|
||||
{ failedNodeCount += 1 }
|
||||
)
|
||||
}
|
||||
|
||||
staged.discardScratchFolder()
|
||||
|
||||
return BookmarkInsertTreeResult(insertedItemCount, failedNodeCount)
|
||||
}
|
||||
|
||||
override fun countBookmarksInTrees(
|
||||
guids: List<String>,
|
||||
callback: (Result<Long>) -> Unit
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
components.core.bookmarksStorage.countBookmarksInTrees(guids).toLong()
|
||||
}
|
||||
}
|
||||
callback(result)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loose top-level nodes written into a scratch folder, waiting to be moved
|
||||
* to their real parent.
|
||||
*
|
||||
* The scratch folder is created under the import destination and holds the
|
||||
* loose nodes in file order; [reparent] hands them out one at a time as the
|
||||
* caller walks the top level, and [discardScratchFolder] removes the folder
|
||||
* once it has been emptied.
|
||||
*/
|
||||
private inner class StagedLooseNodes(
|
||||
private val scratchGuid: String?,
|
||||
/** The loose nodes that made it into the scratch folder, in order. */
|
||||
private val staged: List<BookmarkImportNode>,
|
||||
/** Guid assigned to each entry of [staged], by index. */
|
||||
private val guids: List<String>,
|
||||
private val failure: Throwable?
|
||||
) {
|
||||
private var next = 0
|
||||
|
||||
/**
|
||||
* Moves the next staged node under [parentGuid].
|
||||
*
|
||||
* Reparenting preserves `dateAdded`, which is what bookmark ordering and
|
||||
* "recently added" depend on. It does refresh `lastModified` — the pair
|
||||
* cannot both survive, because the only storage call that accepts
|
||||
* timestamps creates a folder.
|
||||
*/
|
||||
suspend fun reparent(node: BookmarkImportNode, parentGuid: String): Result<Long> {
|
||||
failure?.let { return Result.failure(it) }
|
||||
|
||||
// Nodes dropped while converting (an item with no usable url) were
|
||||
// never staged, so the cursor must not advance past them.
|
||||
if (staged.getOrNull(next) !== node) {
|
||||
return Result.failure(
|
||||
IllegalArgumentException("Unusable bookmark node of type ${node.type}")
|
||||
)
|
||||
}
|
||||
|
||||
val guid = guids.getOrNull(next)
|
||||
?: return Result.failure(
|
||||
IllegalStateException("Storage did not report a guid for ${node.type}")
|
||||
)
|
||||
next++
|
||||
|
||||
// A null field means "leave unchanged"; appending (null position)
|
||||
// keeps the file's order as the caller walks the top level.
|
||||
val move = MozillaBookmarkInfo(
|
||||
parentGuid = parentGuid,
|
||||
position = null,
|
||||
title = null,
|
||||
url = null
|
||||
)
|
||||
|
||||
return components.core.bookmarksStorage
|
||||
.updateNode(guid, move)
|
||||
.map { if (node.type == BookmarkNodeType.ITEM) 1L else 0L }
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the scratch folder, but only once it is empty.
|
||||
*
|
||||
* Deleting cascades to children, so anything that failed to move is left
|
||||
* behind in a visible folder rather than being silently destroyed.
|
||||
*/
|
||||
suspend fun discardScratchFolder() {
|
||||
val guid = scratchGuid ?: return
|
||||
val storage = components.core.bookmarksStorage
|
||||
|
||||
val remaining = storage.getTree(guid, false).getOrNull()?.children
|
||||
if (remaining.isNullOrEmpty()) {
|
||||
storage.deleteNode(guid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes every loose top-level node of [nodes] into a scratch folder under
|
||||
* [parentGuid] in a single tree insertion, so their timestamps survive.
|
||||
*
|
||||
* Returns an empty staging area when the import has no loose top-level
|
||||
* nodes, which is the common case for Firefox exports and costs nothing.
|
||||
*/
|
||||
private suspend fun stageLooseNodes(
|
||||
parentGuid: String,
|
||||
nodes: List<BookmarkImportNode>
|
||||
): StagedLooseNodes {
|
||||
val empty = StagedLooseNodes(null, emptyList(), emptyList(), null)
|
||||
|
||||
val staged = ArrayList<BookmarkImportNode>()
|
||||
val insertable = ArrayList<InsertableBookmarkTreeNode>()
|
||||
for (node in nodes) {
|
||||
if (node.type == BookmarkNodeType.FOLDER) continue
|
||||
val converted = node.toInsertableNode(insertable.size.toUInt()) ?: continue
|
||||
insertable.add(converted)
|
||||
staged.add(node)
|
||||
}
|
||||
|
||||
if (staged.isEmpty()) return empty
|
||||
|
||||
val scratch = InsertableBookmarkTreeNode.Folder(
|
||||
title = SCRATCH_FOLDER_TITLE,
|
||||
dateAddedTimestamp = 0L,
|
||||
lastModifiedTimestamp = 0L,
|
||||
position = null,
|
||||
children = insertable
|
||||
)
|
||||
|
||||
val storage = components.core.bookmarksStorage
|
||||
|
||||
return storage.insertTree(InsertableBookmarkTreeRoot(parentGuid, scratch)).fold(
|
||||
{ scratchGuid ->
|
||||
// Read the assigned guids back in position order, which is the
|
||||
// order the nodes were handed to insertTree.
|
||||
val children = storage.getTree(scratchGuid, false).getOrNull()?.children
|
||||
StagedLooseNodes(
|
||||
scratchGuid = scratchGuid,
|
||||
staged = staged,
|
||||
guids = children.orEmpty().map { it.guid },
|
||||
failure = null
|
||||
)
|
||||
},
|
||||
{ error -> StagedLooseNodes(null, staged, emptyList(), error) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun BookmarkImportNode.toInsertableFolder(position: UInt?) =
|
||||
InsertableBookmarkTreeNode.Folder(
|
||||
title = this.title,
|
||||
dateAddedTimestamp = this.dateAdded,
|
||||
lastModifiedTimestamp = this.lastModified,
|
||||
position = position,
|
||||
children = this.children.toInsertableNodes()
|
||||
)
|
||||
|
||||
/**
|
||||
* Converts children to their insertable form, dropping unusable nodes and
|
||||
* assigning positions from the surviving order so no gaps are left behind.
|
||||
*/
|
||||
private fun List<BookmarkImportNode>.toInsertableNodes(): List<InsertableBookmarkTreeNode> {
|
||||
val converted = ArrayList<InsertableBookmarkTreeNode>(this.size)
|
||||
for (node in this) {
|
||||
converted.add(node.toInsertableNode(converted.size.toUInt()) ?: continue)
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
private fun BookmarkImportNode.toInsertableNode(position: UInt): InsertableBookmarkTreeNode? =
|
||||
when (this.type) {
|
||||
BookmarkNodeType.FOLDER -> this.toInsertableFolder(position)
|
||||
|
||||
BookmarkNodeType.ITEM -> this.url?.takeIf { it.isNotEmpty() }?.let { url ->
|
||||
InsertableBookmarkTreeNode.Item(
|
||||
title = this.title,
|
||||
url = url,
|
||||
dateAddedTimestamp = this.dateAdded,
|
||||
lastModifiedTimestamp = this.lastModified,
|
||||
position = position
|
||||
)
|
||||
}
|
||||
|
||||
BookmarkNodeType.SEPARATOR -> InsertableBookmarkTreeNode.Separator(
|
||||
dateAddedTimestamp = this.dateAdded,
|
||||
lastModifiedTimestamp = this.lastModified,
|
||||
position = position
|
||||
)
|
||||
}
|
||||
|
||||
/** Number of bookmark items in this subtree, excluding folders and separators. */
|
||||
private fun InsertableBookmarkTreeNode.itemCount(): Long = when (this) {
|
||||
is InsertableBookmarkTreeNode.Item -> 1L
|
||||
is InsertableBookmarkTreeNode.Folder -> this.children.sumOf { it.itemCount() }
|
||||
is InsertableBookmarkTreeNode.Separator -> 0L
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies extension `bookmarks.onCreated` listeners about a node created
|
||||
* through the app UI, so extensions (e.g. floccus) observe app-side edits to
|
||||
|
||||
+253
-41
@@ -5271,6 +5271,127 @@ data class BookmarkNode (
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A node of a bookmark tree that is about to be bulk-inserted into storage.
|
||||
*
|
||||
* Unlike [BookmarkNode] this carries no guids or parent links: the tree is
|
||||
* described purely by nesting, and storage assigns guids while inserting.
|
||||
*
|
||||
* @property type Whether this node is an item, a folder or a separator.
|
||||
* @property title The title of the item or folder. Ignored for separators.
|
||||
* @property url The URL of the item. Must be non-null for items, ignored otherwise.
|
||||
* @property dateAdded Creation timestamp in milliseconds since epoch, or 0 if unknown.
|
||||
* @property lastModified Modification timestamp in milliseconds since epoch, or 0 if unknown.
|
||||
* @property children Child nodes of a folder, in insertion order. Empty for items and separators.
|
||||
*
|
||||
* Generated class from Pigeon that represents data sent in messages.
|
||||
*/
|
||||
data class BookmarkImportNode (
|
||||
val type: BookmarkNodeType,
|
||||
val title: String? = null,
|
||||
val url: String? = null,
|
||||
val dateAdded: Long,
|
||||
val lastModified: Long,
|
||||
val children: List<BookmarkImportNode>
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): BookmarkImportNode {
|
||||
val type = pigeonVar_list[0] as BookmarkNodeType
|
||||
val title = pigeonVar_list[1] as String?
|
||||
val url = pigeonVar_list[2] as String?
|
||||
val dateAdded = pigeonVar_list[3] as Long
|
||||
val lastModified = pigeonVar_list[4] as Long
|
||||
val children = pigeonVar_list[5] as List<BookmarkImportNode>
|
||||
return BookmarkImportNode(type, title, url, dateAdded, lastModified, children)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
type,
|
||||
title,
|
||||
url,
|
||||
dateAdded,
|
||||
lastModified,
|
||||
children,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null || other.javaClass != javaClass) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
val other = other as BookmarkImportNode
|
||||
return GeckoPigeonUtils.deepEquals(this.type, other.type) && GeckoPigeonUtils.deepEquals(this.title, other.title) && GeckoPigeonUtils.deepEquals(this.url, other.url) && GeckoPigeonUtils.deepEquals(this.dateAdded, other.dateAdded) && GeckoPigeonUtils.deepEquals(this.lastModified, other.lastModified) && GeckoPigeonUtils.deepEquals(this.children, other.children)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = javaClass.hashCode()
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.type)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.title)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.url)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.dateAdded)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.lastModified)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.children)
|
||||
return result
|
||||
}
|
||||
override fun toString(): String {
|
||||
return "BookmarkImportNode(type=$type, title=$title, url=$url, dateAdded=$dateAdded, lastModified=$lastModified, children=$children)"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Outcome of a bulk bookmark tree insertion.
|
||||
*
|
||||
* @property insertedItemCount The number of bookmark items (not folders or
|
||||
* separators) that were inserted.
|
||||
* @property failedNodeCount The number of top-level nodes that could not be
|
||||
* inserted. Their subtrees are missing entirely.
|
||||
*
|
||||
* Generated class from Pigeon that represents data sent in messages.
|
||||
*/
|
||||
data class BookmarkInsertTreeResult (
|
||||
val insertedItemCount: Long,
|
||||
val failedNodeCount: Long
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): BookmarkInsertTreeResult {
|
||||
val insertedItemCount = pigeonVar_list[0] as Long
|
||||
val failedNodeCount = pigeonVar_list[1] as Long
|
||||
return BookmarkInsertTreeResult(insertedItemCount, failedNodeCount)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
insertedItemCount,
|
||||
failedNodeCount,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null || other.javaClass != javaClass) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
val other = other as BookmarkInsertTreeResult
|
||||
return GeckoPigeonUtils.deepEquals(this.insertedItemCount, other.insertedItemCount) && GeckoPigeonUtils.deepEquals(this.failedNodeCount, other.failedNodeCount)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = javaClass.hashCode()
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.insertedItemCount)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.failedNodeCount)
|
||||
return result
|
||||
}
|
||||
override fun toString(): String {
|
||||
return "BookmarkInsertTreeResult(insertedItemCount=$insertedItemCount, failedNodeCount=$failedNodeCount)"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for making alterations to any bookmark node
|
||||
*
|
||||
@@ -6613,28 +6734,32 @@ private data class GeckoPigeonInternalCodecOverflow (
|
||||
|
||||
when (type.toInt()) {
|
||||
0 ->
|
||||
return AppLinkResolutionResult.fromList(wrapped as List<Any?>)
|
||||
return AppLinkPolicySnapshot.fromList(wrapped as List<Any?>)
|
||||
1 ->
|
||||
return PwaIcon.fromList(wrapped as List<Any?>)
|
||||
return AppLinkPromptRequest.fromList(wrapped as List<Any?>)
|
||||
2 ->
|
||||
return ShareTargetFiles.fromList(wrapped as List<Any?>)
|
||||
return AppLinkResolutionResult.fromList(wrapped as List<Any?>)
|
||||
3 ->
|
||||
return ShareTargetParams.fromList(wrapped as List<Any?>)
|
||||
return PwaIcon.fromList(wrapped as List<Any?>)
|
||||
4 ->
|
||||
return ShareTarget.fromList(wrapped as List<Any?>)
|
||||
return ShareTargetFiles.fromList(wrapped as List<Any?>)
|
||||
5 ->
|
||||
return ExternalApplicationResource.fromList(wrapped as List<Any?>)
|
||||
return ShareTargetParams.fromList(wrapped as List<Any?>)
|
||||
6 ->
|
||||
return PwaManifest.fromList(wrapped as List<Any?>)
|
||||
return ShareTarget.fromList(wrapped as List<Any?>)
|
||||
7 ->
|
||||
return SandboxCaptureEntry.fromList(wrapped as List<Any?>)
|
||||
return ExternalApplicationResource.fromList(wrapped as List<Any?>)
|
||||
8 ->
|
||||
return GestureConfig.fromList(wrapped as List<Any?>)
|
||||
return PwaManifest.fromList(wrapped as List<Any?>)
|
||||
9 ->
|
||||
return PushDistributor.fromList(wrapped as List<Any?>)
|
||||
return SandboxCaptureEntry.fromList(wrapped as List<Any?>)
|
||||
10 ->
|
||||
return PushStatus.fromList(wrapped as List<Any?>)
|
||||
return GestureConfig.fromList(wrapped as List<Any?>)
|
||||
11 ->
|
||||
return PushDistributor.fromList(wrapped as List<Any?>)
|
||||
12 ->
|
||||
return PushStatus.fromList(wrapped as List<Any?>)
|
||||
13 ->
|
||||
return PushSubscription.fromList(wrapped as List<Any?>)
|
||||
}
|
||||
return null
|
||||
@@ -7230,47 +7355,47 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
|
||||
}
|
||||
246.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
BookmarkInfo.fromList(it)
|
||||
BookmarkImportNode.fromList(it)
|
||||
}
|
||||
}
|
||||
247.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
SitePermissions.fromList(it)
|
||||
BookmarkInsertTreeResult.fromList(it)
|
||||
}
|
||||
}
|
||||
248.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
TrackingProtectionException.fromList(it)
|
||||
BookmarkInfo.fromList(it)
|
||||
}
|
||||
}
|
||||
249.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
AppLinkTarget.fromList(it)
|
||||
SitePermissions.fromList(it)
|
||||
}
|
||||
}
|
||||
250.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
ProtectedTargetPattern.fromList(it)
|
||||
TrackingProtectionException.fromList(it)
|
||||
}
|
||||
}
|
||||
251.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
NativeAppLinkRule.fromList(it)
|
||||
AppLinkTarget.fromList(it)
|
||||
}
|
||||
}
|
||||
252.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
NativeContextAppLinkPolicy.fromList(it)
|
||||
ProtectedTargetPattern.fromList(it)
|
||||
}
|
||||
}
|
||||
253.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
AppLinkPolicySnapshot.fromList(it)
|
||||
NativeAppLinkRule.fromList(it)
|
||||
}
|
||||
}
|
||||
254.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
AppLinkPromptRequest.fromList(it)
|
||||
NativeContextAppLinkPolicy.fromList(it)
|
||||
}
|
||||
}
|
||||
255.toByte() -> {
|
||||
@@ -7751,102 +7876,112 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
|
||||
stream.write(245)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is BookmarkInfo -> {
|
||||
is BookmarkImportNode -> {
|
||||
stream.write(246)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is SitePermissions -> {
|
||||
is BookmarkInsertTreeResult -> {
|
||||
stream.write(247)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is TrackingProtectionException -> {
|
||||
is BookmarkInfo -> {
|
||||
stream.write(248)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is AppLinkTarget -> {
|
||||
is SitePermissions -> {
|
||||
stream.write(249)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is ProtectedTargetPattern -> {
|
||||
is TrackingProtectionException -> {
|
||||
stream.write(250)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is NativeAppLinkRule -> {
|
||||
is AppLinkTarget -> {
|
||||
stream.write(251)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is NativeContextAppLinkPolicy -> {
|
||||
is ProtectedTargetPattern -> {
|
||||
stream.write(252)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is AppLinkPolicySnapshot -> {
|
||||
is NativeAppLinkRule -> {
|
||||
stream.write(253)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is AppLinkPromptRequest -> {
|
||||
is NativeContextAppLinkPolicy -> {
|
||||
stream.write(254)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is AppLinkResolutionResult -> {
|
||||
is AppLinkPolicySnapshot -> {
|
||||
val wrap = GeckoPigeonInternalCodecOverflow(type = 0, wrapped = value.toList())
|
||||
stream.write(255)
|
||||
writeValue(stream, wrap.toList())
|
||||
}
|
||||
is PwaIcon -> {
|
||||
is AppLinkPromptRequest -> {
|
||||
val wrap = GeckoPigeonInternalCodecOverflow(type = 1, wrapped = value.toList())
|
||||
stream.write(255)
|
||||
writeValue(stream, wrap.toList())
|
||||
}
|
||||
is ShareTargetFiles -> {
|
||||
is AppLinkResolutionResult -> {
|
||||
val wrap = GeckoPigeonInternalCodecOverflow(type = 2, wrapped = value.toList())
|
||||
stream.write(255)
|
||||
writeValue(stream, wrap.toList())
|
||||
}
|
||||
is ShareTargetParams -> {
|
||||
is PwaIcon -> {
|
||||
val wrap = GeckoPigeonInternalCodecOverflow(type = 3, wrapped = value.toList())
|
||||
stream.write(255)
|
||||
writeValue(stream, wrap.toList())
|
||||
}
|
||||
is ShareTarget -> {
|
||||
is ShareTargetFiles -> {
|
||||
val wrap = GeckoPigeonInternalCodecOverflow(type = 4, wrapped = value.toList())
|
||||
stream.write(255)
|
||||
writeValue(stream, wrap.toList())
|
||||
}
|
||||
is ExternalApplicationResource -> {
|
||||
is ShareTargetParams -> {
|
||||
val wrap = GeckoPigeonInternalCodecOverflow(type = 5, wrapped = value.toList())
|
||||
stream.write(255)
|
||||
writeValue(stream, wrap.toList())
|
||||
}
|
||||
is PwaManifest -> {
|
||||
is ShareTarget -> {
|
||||
val wrap = GeckoPigeonInternalCodecOverflow(type = 6, wrapped = value.toList())
|
||||
stream.write(255)
|
||||
writeValue(stream, wrap.toList())
|
||||
}
|
||||
is SandboxCaptureEntry -> {
|
||||
is ExternalApplicationResource -> {
|
||||
val wrap = GeckoPigeonInternalCodecOverflow(type = 7, wrapped = value.toList())
|
||||
stream.write(255)
|
||||
writeValue(stream, wrap.toList())
|
||||
}
|
||||
is GestureConfig -> {
|
||||
is PwaManifest -> {
|
||||
val wrap = GeckoPigeonInternalCodecOverflow(type = 8, wrapped = value.toList())
|
||||
stream.write(255)
|
||||
writeValue(stream, wrap.toList())
|
||||
}
|
||||
is PushDistributor -> {
|
||||
is SandboxCaptureEntry -> {
|
||||
val wrap = GeckoPigeonInternalCodecOverflow(type = 9, wrapped = value.toList())
|
||||
stream.write(255)
|
||||
writeValue(stream, wrap.toList())
|
||||
}
|
||||
is PushStatus -> {
|
||||
is GestureConfig -> {
|
||||
val wrap = GeckoPigeonInternalCodecOverflow(type = 10, wrapped = value.toList())
|
||||
stream.write(255)
|
||||
writeValue(stream, wrap.toList())
|
||||
}
|
||||
is PushSubscription -> {
|
||||
is PushDistributor -> {
|
||||
val wrap = GeckoPigeonInternalCodecOverflow(type = 11, wrapped = value.toList())
|
||||
stream.write(255)
|
||||
writeValue(stream, wrap.toList())
|
||||
}
|
||||
is PushStatus -> {
|
||||
val wrap = GeckoPigeonInternalCodecOverflow(type = 12, wrapped = value.toList())
|
||||
stream.write(255)
|
||||
writeValue(stream, wrap.toList())
|
||||
}
|
||||
is PushSubscription -> {
|
||||
val wrap = GeckoPigeonInternalCodecOverflow(type = 13, wrapped = value.toList())
|
||||
stream.write(255)
|
||||
writeValue(stream, wrap.toList())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
@@ -12253,6 +12388,42 @@ interface GeckoBookmarksApi {
|
||||
* @return Whether the bookmark existed or not.
|
||||
*/
|
||||
fun deleteNode(guid: String, callback: (Result<Boolean>) -> Unit)
|
||||
/**
|
||||
* Bulk-inserts [children] underneath [parentGuid], appending them after any
|
||||
* nodes the parent already contains.
|
||||
*
|
||||
* Each top-level folder is handed to the storage layer as a single tree
|
||||
* insertion, so importing a large bookmark file costs one platform channel
|
||||
* call instead of one per node. Separators are preserved.
|
||||
*
|
||||
* Timestamps survive in full for everything nested inside a top-level
|
||||
* folder. Loose top-level items and separators keep their [dateAdded], but
|
||||
* their [lastModified] is set to the time of import: the only storage call
|
||||
* that accepts timestamps creates a folder, so nodes landing directly in
|
||||
* [parentGuid] have to be moved into place afterwards.
|
||||
*
|
||||
* Sync behavior: will add the inserted bookmarks to remote devices.
|
||||
*
|
||||
* Unlike [addItem] and [addFolder] this does *not* emit a
|
||||
* `bookmarks.onCreated` extension event per node, since a large import would
|
||||
* otherwise flood every installed WebExtension.
|
||||
*
|
||||
* @param parentGuid The guid of the existing folder to insert underneath.
|
||||
* @param children The nodes to insert, in the order they should appear.
|
||||
* @return The number of inserted bookmark items and failed top-level nodes.
|
||||
*/
|
||||
fun insertTree(parentGuid: String, children: List<BookmarkImportNode>, callback: (Result<BookmarkInsertTreeResult>) -> Unit)
|
||||
/**
|
||||
* Counts the bookmark items contained in the trees rooted at [guids].
|
||||
*
|
||||
* Folders and separators are not counted, and a guid that does not exist
|
||||
* contributes nothing. Lets the app report how much a destructive action
|
||||
* affects without loading the subtrees into Dart.
|
||||
*
|
||||
* @param guids The guids of the folders to count within.
|
||||
* @return The total number of bookmark items across all trees.
|
||||
*/
|
||||
fun countBookmarksInTrees(guids: List<String>, callback: (Result<Long>) -> Unit)
|
||||
|
||||
companion object {
|
||||
/** The codec used by GeckoBookmarksApi. */
|
||||
@@ -12452,6 +12623,47 @@ interface GeckoBookmarksApi {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.insertTree$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val parentGuidArg = args[0] as String
|
||||
val childrenArg = args[1] as List<BookmarkImportNode>
|
||||
api.insertTree(parentGuidArg, childrenArg) { result: Result<BookmarkInsertTreeResult> ->
|
||||
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<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBookmarksApi.countBookmarksInTrees$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val guidsArg = args[0] as List<String>
|
||||
api.countBookmarksInTrees(guidsArg) { result: Result<Long> ->
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,9 @@ export 'src/pigeons/gecko.g.dart'
|
||||
AppLinksMode,
|
||||
AudioHitResult,
|
||||
AutoplayStatus,
|
||||
BookmarkImportNode,
|
||||
BookmarkInfo,
|
||||
BookmarkInsertTreeResult,
|
||||
BookmarkNode,
|
||||
BookmarkNodeType,
|
||||
BounceTrackingProtectionMode,
|
||||
|
||||
@@ -129,6 +129,42 @@ class GeckoBookmarksService {
|
||||
return _api.deleteNode(guid);
|
||||
}
|
||||
|
||||
/// Bulk-inserts [children] underneath [parentGuid], appending them after any
|
||||
/// nodes the parent already contains.
|
||||
///
|
||||
/// Prefer this over looping [addItem]/[addFolder] when inserting a whole
|
||||
/// tree: the entire batch crosses the platform channel once and each
|
||||
/// top-level folder is written as a single storage operation. Separators are
|
||||
/// preserved, and no per-node `bookmarks.onCreated` extension events are
|
||||
/// emitted.
|
||||
///
|
||||
/// Timestamps survive in full for everything nested inside a top-level
|
||||
/// folder. Loose top-level items and separators keep their `dateAdded` but
|
||||
/// get a fresh `lastModified`, because the only storage call that accepts
|
||||
/// timestamps creates a folder.
|
||||
///
|
||||
/// @param parentGuid The guid of the existing folder to insert underneath.
|
||||
/// @param children The nodes to insert, in the order they should appear.
|
||||
/// @return The number of inserted bookmark items and failed top-level nodes.
|
||||
Future<BookmarkInsertTreeResult> insertTree(
|
||||
String parentGuid,
|
||||
List<BookmarkImportNode> children,
|
||||
) {
|
||||
return _api.insertTree(parentGuid, children);
|
||||
}
|
||||
|
||||
/// Counts the bookmark items contained in the trees rooted at [guids].
|
||||
///
|
||||
/// Folders and separators are not counted. Prefer this over walking a
|
||||
/// [getTree] result: the count is computed in storage, so no subtree has to
|
||||
/// be materialised in Dart.
|
||||
///
|
||||
/// @param guids The guids of the folders to count within.
|
||||
/// @return The total number of bookmark items across all trees.
|
||||
Future<int> countBookmarksInTrees(List<String> guids) {
|
||||
return _api.countBookmarksInTrees(guids);
|
||||
}
|
||||
|
||||
/// Removes ALL bookmarks from the specified root folder.
|
||||
/// The root folder itself is preserved, only its children are removed.
|
||||
Future<void> eraseEverything(BookmarkRoot root) async {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2467,6 +2467,51 @@ class BookmarkNode {
|
||||
});
|
||||
}
|
||||
|
||||
/// A node of a bookmark tree that is about to be bulk-inserted into storage.
|
||||
///
|
||||
/// Unlike [BookmarkNode] this carries no guids or parent links: the tree is
|
||||
/// described purely by nesting, and storage assigns guids while inserting.
|
||||
///
|
||||
/// @property type Whether this node is an item, a folder or a separator.
|
||||
/// @property title The title of the item or folder. Ignored for separators.
|
||||
/// @property url The URL of the item. Must be non-null for items, ignored otherwise.
|
||||
/// @property dateAdded Creation timestamp in milliseconds since epoch, or 0 if unknown.
|
||||
/// @property lastModified Modification timestamp in milliseconds since epoch, or 0 if unknown.
|
||||
/// @property children Child nodes of a folder, in insertion order. Empty for items and separators.
|
||||
class BookmarkImportNode {
|
||||
final BookmarkNodeType type;
|
||||
final String? title;
|
||||
final String? url;
|
||||
final int dateAdded;
|
||||
final int lastModified;
|
||||
final List<BookmarkImportNode> children;
|
||||
|
||||
BookmarkImportNode({
|
||||
required this.type,
|
||||
required this.title,
|
||||
required this.url,
|
||||
required this.dateAdded,
|
||||
required this.lastModified,
|
||||
required this.children,
|
||||
});
|
||||
}
|
||||
|
||||
/// Outcome of a bulk bookmark tree insertion.
|
||||
///
|
||||
/// @property insertedItemCount The number of bookmark items (not folders or
|
||||
/// separators) that were inserted.
|
||||
/// @property failedNodeCount The number of top-level nodes that could not be
|
||||
/// inserted. Their subtrees are missing entirely.
|
||||
class BookmarkInsertTreeResult {
|
||||
final int insertedItemCount;
|
||||
final int failedNodeCount;
|
||||
|
||||
BookmarkInsertTreeResult({
|
||||
required this.insertedItemCount,
|
||||
required this.failedNodeCount,
|
||||
});
|
||||
}
|
||||
|
||||
/// Class for making alterations to any bookmark node
|
||||
class BookmarkInfo {
|
||||
final String? parentGuid;
|
||||
@@ -2639,6 +2684,45 @@ abstract class GeckoBookmarksApi {
|
||||
/// @return Whether the bookmark existed or not.
|
||||
@async
|
||||
bool deleteNode(String guid);
|
||||
|
||||
/// Bulk-inserts [children] underneath [parentGuid], appending them after any
|
||||
/// nodes the parent already contains.
|
||||
///
|
||||
/// Each top-level folder is handed to the storage layer as a single tree
|
||||
/// insertion, so importing a large bookmark file costs one platform channel
|
||||
/// call instead of one per node. Separators are preserved.
|
||||
///
|
||||
/// Timestamps survive in full for everything nested inside a top-level
|
||||
/// folder. Loose top-level items and separators keep their [dateAdded], but
|
||||
/// their [lastModified] is set to the time of import: the only storage call
|
||||
/// that accepts timestamps creates a folder, so nodes landing directly in
|
||||
/// [parentGuid] have to be moved into place afterwards.
|
||||
///
|
||||
/// Sync behavior: will add the inserted bookmarks to remote devices.
|
||||
///
|
||||
/// Unlike [addItem] and [addFolder] this does *not* emit a
|
||||
/// `bookmarks.onCreated` extension event per node, since a large import would
|
||||
/// otherwise flood every installed WebExtension.
|
||||
///
|
||||
/// @param parentGuid The guid of the existing folder to insert underneath.
|
||||
/// @param children The nodes to insert, in the order they should appear.
|
||||
/// @return The number of inserted bookmark items and failed top-level nodes.
|
||||
@async
|
||||
BookmarkInsertTreeResult insertTree(
|
||||
String parentGuid,
|
||||
List<BookmarkImportNode> children,
|
||||
);
|
||||
|
||||
/// Counts the bookmark items contained in the trees rooted at [guids].
|
||||
///
|
||||
/// Folders and separators are not counted, and a guid that does not exist
|
||||
/// contributes nothing. Lets the app report how much a destructive action
|
||||
/// affects without loading the subtrees into Dart.
|
||||
///
|
||||
/// @param guids The guids of the folders to count within.
|
||||
/// @return The total number of bookmark items across all trees.
|
||||
@async
|
||||
int countBookmarksInTrees(List<String> guids);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user