Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
316 changes: 116 additions & 200 deletions src/treetop/BookmarksManager.test.ts

Large diffs are not rendered by default.

72 changes: 33 additions & 39 deletions src/treetop/BookmarksManager.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import { get, writable } from 'svelte/store';

import { isBookmark, isFolder } from './bookmarktreenode-utils';
import { MOBILE_BOOKMARKS_GUID } from './constants';
import * as Treetop from './types';

/**
* Class to initialize and manage updating bookmark node stores.
* Class to initialize and manage updating bookmark nodes.
*/
export class BookmarksManager {
constructor(
private readonly nodeStoreMap: Treetop.NodeStoreMap,
private readonly folderNodeMap: Treetop.FolderNodeMap,
private readonly builtInFolderInfo: Treetop.BuiltInFolderInfo,
) {}

/**
* Load all bookmarks and initialize node stores for folders.
* Load all bookmarks and initialize nodes for folders.
* Initialize built-in folder info.
*/
async loadBookmarks(): Promise<void> {
Expand All @@ -34,12 +32,12 @@ export class BookmarksManager {
({ id }) => id,
);

// Initialize node stores for folders
// Initialize nodes for folders
while (nodes.length > 0) {
const node = nodes.pop()!;

if (isFolder(node)) {
this.buildNodeStore(node);
this.buildFolderNode(node);
nodes.push(...node.children!);
}
}
Expand Down Expand Up @@ -88,18 +86,17 @@ export class BookmarksManager {
}

/**
* Create and record a node store for the specified bookmark node.
* Create and record a node for the specified bookmark node.
*/
private buildNodeStore(node: chrome.bookmarks.BookmarkTreeNode): void {
const newNode = this.convertNode(node);
const nodeStore = writable(newNode);
this.nodeStoreMap.set(node.id, nodeStore);
private buildFolderNode(node: chrome.bookmarks.BookmarkTreeNode): void {
const folderNode = this.convertNode(node);
this.folderNodeMap.set(node.id, folderNode);
}

/**
* Update the node store for the specified bookmark ID.
* Update the node for the specified bookmark ID.
*/
private async updateNodeStore(nodeId: string): Promise<void> {
private async updateFolderNode(nodeId: string): Promise<void> {
const [node] = await chrome.bookmarks.get(nodeId);

if (!isFolder(node)) {
Expand All @@ -108,32 +105,31 @@ export class BookmarksManager {

node.children = await chrome.bookmarks.getChildren(node.id);

const newNode = this.convertNode(node);
const nodeStore = this.nodeStoreMap.get(nodeId)!;
nodeStore.set(newNode);
const folderNode = this.convertNode(node);
this.folderNodeMap.set(nodeId, folderNode);
}

/**
* Create a store for a newly created bookmark node.
* Create a node for a newly created bookmark node.
*/
async handleBookmarkCreated(
id: string,
bookmark: chrome.bookmarks.BookmarkTreeNode,
): Promise<void> {
if (isFolder(bookmark)) {
// Add node store for the new folder
// Add node for the new folder
const [node] = await chrome.bookmarks.get(id);
node.children = await chrome.bookmarks.getChildren(id);
this.buildNodeStore(node);
this.buildFolderNode(node);
}

// Update parent node of the new bookmark
await this.updateNodeStore(bookmark.parentId!);
await this.updateFolderNode(bookmark.parentId!);
}

/**
* Delete stores for removed bookmarks. When the removed bookmark is a folder,
* recursively delete stores for its children.
* Delete nodes for removed bookmarks. When the removed bookmark is a folder,
* recursively delete nodes for its children.
*
* @return Array of the removed bookmark node IDs.
*/
Expand All @@ -144,13 +140,12 @@ export class BookmarksManager {
const removedNodeIds = [];

if (isFolder(removeInfo.node)) {
const nodeStore = this.nodeStoreMap.get(id)!;
const nodes: [Treetop.FolderNode] = [get(nodeStore)];
const folderNode = this.folderNodeMap.get(id)!;
const nodes = [folderNode];

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (nodes.length) {
const node = nodes.pop()!;
this.nodeStoreMap.delete(node.id);
this.folderNodeMap.delete(node.id);
removedNodeIds.push(node.id);

// Store removed bookmark IDs
Expand All @@ -161,10 +156,9 @@ export class BookmarksManager {
}

// Enqueue child folders for removal
for (const childNodeStore of this.nodeStoreMap.values()) {
const currentNode: Treetop.FolderNode = get(childNodeStore);
if (currentNode.parentId === node.id) {
nodes.push(currentNode);
for (const childFolderNode of this.folderNodeMap.values()) {
if (childFolderNode.parentId === node.id) {
nodes.push(childFolderNode);
}
}
}
Expand All @@ -173,30 +167,30 @@ export class BookmarksManager {
}

// Update parent node of the deleted bookmark
await this.updateNodeStore(removeInfo.parentId);
await this.updateFolderNode(removeInfo.parentId);

return removedNodeIds;
}

/**
* Update the store for a modified bookmark.
* Update the node for a modified bookmark.
*/
async handleBookmarkChanged(
id: string,
_changeInfo: Treetop.BookmarkChangeInfo,
): Promise<void> {
if (this.nodeStoreMap.has(id)) {
if (this.folderNodeMap.has(id)) {
// Folder changed. Update its node.
await this.updateNodeStore(id);
await this.updateFolderNode(id);
} else {
// Bookmark changed. Update the parent folder node.
const [node] = await chrome.bookmarks.get(id);
await this.updateNodeStore(node.parentId!);
await this.updateFolderNode(node.parentId!);
}
}

/**
* Update stores when a bookmark is moved to a different folder or to a new
* Update nodes when a bookmark is moved to a different folder or to a new
* offset within its folder.
*/
async handleBookmarkMoved(
Expand All @@ -207,11 +201,11 @@ export class BookmarksManager {
const oldParentId = moveInfo.oldParentId;

// Update parent folder
await this.updateNodeStore(parentId);
await this.updateFolderNode(parentId);

// Update old parent folder
if (parentId !== oldParentId) {
await this.updateNodeStore(oldParentId);
await this.updateFolderNode(oldParentId);
}
}
}
21 changes: 10 additions & 11 deletions src/treetop/FilterManager.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint no-irregular-whitespace: ["error", { "skipComments": true }] */

import { SvelteSet } from 'svelte/reactivity';
import { writable } from 'svelte/store';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import { faker } from '@faker-js/faker';
import { beforeEach, describe, expect, it } from 'vitest';

Expand All @@ -25,7 +24,7 @@ const randomStringContaining = (str: string) => {
)}`;
};

let nodeStoreMap: Treetop.NodeStoreMap;
let folderNodeMap: Treetop.FolderNodeMap;
let filterSet: Treetop.FilterSet;
let filterManager: FilterManager;
let folderNode1: Treetop.FolderNode;
Expand All @@ -36,9 +35,9 @@ let folderNode5: Treetop.FolderNode;
let folderNode6: Treetop.FolderNode;

beforeEach(() => {
nodeStoreMap = new Map() as Treetop.NodeStoreMap;
folderNodeMap = new SvelteMap();
filterSet = new SvelteSet();
filterManager = new FilterManager(filterSet, nodeStoreMap);
filterManager = new FilterManager(filterSet, folderNodeMap);

// Create node tree:
// folderNode1
Expand Down Expand Up @@ -114,12 +113,12 @@ beforeEach(() => {
});
folderNode5.children.push(folderNode6);

nodeStoreMap.set(folderNode1.id, writable(folderNode1));
nodeStoreMap.set(folderNode2.id, writable(folderNode2));
nodeStoreMap.set(folderNode3.id, writable(folderNode3));
nodeStoreMap.set(folderNode4.id, writable(folderNode4));
nodeStoreMap.set(folderNode5.id, writable(folderNode5));
nodeStoreMap.set(folderNode6.id, writable(folderNode6));
folderNodeMap.set(folderNode1.id, folderNode1);
folderNodeMap.set(folderNode2.id, folderNode2);
folderNodeMap.set(folderNode3.id, folderNode3);
folderNodeMap.set(folderNode4.id, folderNode4);
folderNodeMap.set(folderNode5.id, folderNode5);
folderNodeMap.set(folderNode6.id, folderNode6);
});

describe('setFilter', () => {
Expand Down
22 changes: 9 additions & 13 deletions src/treetop/FilterManager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { get } from 'svelte/store';

import { isBookmark } from './bookmarktreenode-utils';
import * as Treetop from './types';

Expand All @@ -17,7 +15,7 @@ export class FilterManager {

constructor(
private readonly filterSet: Treetop.FilterSet,
private readonly nodeStoreMap: Treetop.NodeStoreMap,
private readonly folderNodeMap: Treetop.FolderNodeMap,
) {}

/**
Expand All @@ -29,10 +27,9 @@ export class FilterManager {
// Pass 1:
// Add bookmarks that match the filter string to the FilterSet.
// Additionally, add their immediate parent folders to the FilterSet.
for (const nodeStore of this.nodeStoreMap.values()) {
for (const folderNode of this.folderNodeMap.values()) {
let addedChild = false;

const folderNode: Treetop.FolderNode = get(nodeStore);
for (const child of folderNode.children) {
if (child.type === Treetop.NodeType.Bookmark) {
if (
Expand All @@ -53,12 +50,11 @@ export class FilterManager {
// Pass 2
// For each folder in the FilterSet, add the folders on the path to the root
// folder.
for (const nodeStore of this.nodeStoreMap.values()) {
const node: Treetop.FolderNode = get(nodeStore);
if (this.filterSet.has(node.id)) {
let curNode = node;
for (const folderNode of this.folderNodeMap.values()) {
if (this.filterSet.has(folderNode.id)) {
let curNode = folderNode;
while (curNode.parentId) {
curNode = get(this.nodeStoreMap.get(curNode.parentId)!);
curNode = this.folderNodeMap.get(curNode.parentId)!;
this.filterSet.add(curNode.id);
}
}
Expand Down Expand Up @@ -102,9 +98,9 @@ export class FilterManager {
// Add the folders on the path to the root folder to the FilterSet
let parentId = bookmark.parentId;
while (parentId) {
const node = get(this.nodeStoreMap.get(parentId)!);
this.filterSet.add(node.id);
parentId = node.parentId;
const folderNode = this.folderNodeMap.get(parentId)!;
this.filterSet.add(folderNode.id);
parentId = folderNode.parentId;
}
}

Expand Down
16 changes: 7 additions & 9 deletions src/treetop/Folder.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script lang="ts">
import { getContext } from 'svelte';
import { get, type Writable } from 'svelte/store';

import Bookmark from './Bookmark.svelte';
import Folder from './Folder.svelte';
Expand All @@ -15,16 +14,16 @@

const builtInFolderInfo: Treetop.BuiltInFolderInfo =
getContext('builtInFolderInfo');
const nodeStoreMap: Treetop.NodeStoreMap = getContext('nodeStoreMap');
const folderNodeMap = getContext<Treetop.FolderNodeMap>('folderNodeMap');
const filterActive = getContext<() => boolean>('filterActive');
const filterSet = getContext<Treetop.FilterSet>('filterSet');

let node: Writable<Treetop.FolderNode> = nodeStoreMap.get(nodeId)!;
const node = $derived(folderNodeMap.get(nodeId)!);

// Nodes for the folder heading.
// For the root folder, get the folder nodes from bookmarks root to the selected root.
// Otherwise, include only the folder node itself.
const folderNodes = $derived(root ? getFolderNodes($node) : [$node]);
const folderNodes = $derived(root ? getFolderNodes(node) : [node]);

/**
* Get folder nodes from bookmarks root to the selected root.
Expand All @@ -33,8 +32,7 @@
const nodes = [node];

while (node.parentId) {
const nodeStore = nodeStoreMap.get(node.parentId)!;
node = get(nodeStore);
node = folderNodeMap.get(node.parentId)!;
nodes.unshift(node);
}

Expand All @@ -59,7 +57,7 @@
// Include selected root title in document title.
// Don't use fallback title.
const documentTitle = $derived(
$node.title ? `Treetop: ${$node.title}` : 'Treetop',
node.title ? `Treetop: ${node.title}` : 'Treetop',
);
</script>

Expand Down Expand Up @@ -117,7 +115,7 @@
{/if}
</svelte:head>

{#if root || !filterActive() || filterSet.has($node.id)}
{#if root || !filterActive() || filterSet.has(node.id)}
<div class="folder" class:root>
<div class="heading">
<div class="title">
Expand All @@ -133,7 +131,7 @@
{#if root && filterActive() && !filterSet.has(nodeId)}
<em>{chrome.i18n.getMessage('noResults')}</em>
{/if}
{#each $node.children as child (child.id)}
{#each node.children as child (child.id)}
{#if child.type === Treetop.NodeType.Bookmark}
<!--
Destructure child to work around the following false positive linter
Expand Down
Loading