+ {#if node.size > 0}
+
+ {/if}
+
-
{value}
+
{node.value}
{#if isOpen}
-
+
{/if}
diff --git a/web/src/lib/stores/folders.svelte.ts b/web/src/lib/stores/folders.svelte.ts
index 6f3eb8b66a..f77b67bb7c 100644
--- a/web/src/lib/stores/folders.svelte.ts
+++ b/web/src/lib/stores/folders.svelte.ts
@@ -1,4 +1,5 @@
import { eventManager } from '$lib/managers/event-manager.svelte';
+import { TreeNode } from '$lib/utils/tree-utils';
import {
getAssetsByOriginalPath,
getUniqueOriginalPaths,
@@ -13,47 +14,41 @@ type AssetCache = {
};
class FoldersStore {
+ folders = $state.raw
(null);
private initialized = false;
- uniquePaths = $state([]);
- assets = $state({});
+ private assets = $state({});
constructor() {
eventManager.on('auth.logout', () => this.clearCache());
}
- async fetchUniquePaths() {
+ async fetchTree(): Promise {
if (this.initialized) {
- return;
+ return this.folders!;
}
this.initialized = true;
- const uniquePaths = await getUniqueOriginalPaths();
- this.uniquePaths.push(...uniquePaths);
+ this.folders = TreeNode.fromPaths(await getUniqueOriginalPaths());
+ this.folders.collapse();
+ return this.folders;
}
bustAssetCache() {
this.assets = {};
}
- async refreshAssetsByPath(path: string | null) {
- if (!path) {
- return;
- }
- this.assets[path] = await getAssetsByOriginalPath({ path });
+ async refreshAssetsByPath(path: string) {
+ return (this.assets[path] = await getAssetsByOriginalPath({ path }));
}
async fetchAssetsByPath(path: string) {
- if (this.assets[path]) {
- return;
- }
-
- this.assets[path] = await getAssetsByOriginalPath({ path });
+ return (this.assets[path] ??= await getAssetsByOriginalPath({ path }));
}
clearCache() {
this.initialized = false;
- this.uniquePaths = [];
this.assets = {};
+ this.folders = null;
}
}
diff --git a/web/src/lib/utils/navigation.ts b/web/src/lib/utils/navigation.ts
index c3b4d83f38..89e1f9f5f0 100644
--- a/web/src/lib/utils/navigation.ts
+++ b/web/src/lib/utils/navigation.ts
@@ -23,7 +23,7 @@ export const isAssetViewerRoute = (target?: NavigationTarget | null) =>
!!(target?.route.id?.endsWith('/[[assetId=id]]') && 'assetId' in (target?.params || {}));
export function getAssetInfoFromParam({ assetId, key }: { assetId?: string; key?: string }) {
- return assetId && getAssetInfo({ id: assetId, key });
+ return assetId ? getAssetInfo({ id: assetId, key }) : undefined;
}
function currentUrlWithoutAsset() {
diff --git a/web/src/lib/utils/tree-utils.ts b/web/src/lib/utils/tree-utils.ts
index 5a6e917079..267bb2eec7 100644
--- a/web/src/lib/utils/tree-utils.ts
+++ b/web/src/lib/utils/tree-utils.ts
@@ -1,21 +1,144 @@
-export interface RecursiveObject {
- [key: string]: RecursiveObject;
-}
+/* eslint-disable @typescript-eslint/no-this-alias */
+/* eslint-disable unicorn/no-this-assignment */
+/* eslint-disable unicorn/prefer-at */
+import type { TagResponseDto } from '@immich/sdk';
-export const normalizeTreePath = (path: string) => path.replace(/^\//, '').replace(/\/$/, '');
+export class TreeNode extends Map {
+ value: string;
+ path: string;
+ parent: TreeNode | null;
+ hasAssets: boolean;
+ id: string | undefined;
+ color: string | undefined;
+ private _parents: TreeNode[] | undefined;
+ private _children: TreeNode[] | undefined;
-export function buildTree(paths: string[]) {
- const root: RecursiveObject = {};
+ private constructor(value: string, path: string, parent: TreeNode | null) {
+ super();
+ this.value = value;
+ this.parent = parent;
+ this.path = path;
+ this.hasAssets = false;
+ }
- for (const path of paths) {
- const parts = path.split('/');
- let current = root;
+ static fromPaths(paths: string[]) {
+ const root = new TreeNode('', '', null);
+ for (const path of paths) {
+ const current = root.add(path);
+ current.hasAssets = true;
+ }
+ return root;
+ }
+
+ static fromTags(tags: TagResponseDto[]) {
+ const root = new TreeNode('', '', null);
+ for (const tag of tags) {
+ const current = root.add(tag.value);
+ current.hasAssets = true;
+ current.id = tag.id;
+ current.color = tag.color;
+ }
+ return root;
+ }
+
+ traverse(path: string) {
+ const parts = getPathParts(path);
+ let current: TreeNode = this;
+ let curPart = null;
for (const part of parts) {
- if (!current[part]) {
- current[part] = {};
+ // segments common to all subtrees can be collapsed together
+ curPart = curPart === null ? part : joinPaths(curPart, part);
+ const next = current.get(curPart);
+ if (next) {
+ current = next;
+ curPart = null;
}
- current = current[part];
+ }
+ return current;
+ }
+
+ collapse() {
+ if (this.size === 1 && !this.hasAssets && this.parent !== null) {
+ const child = this.values().next().value!;
+ child.value = joinPaths(this.value, child.value);
+ child.parent = this.parent;
+ this.parent.delete(this.value);
+ this.parent.set(child.value, child);
+ }
+
+ for (const child of this.values()) {
+ child.collapse();
}
}
- return root;
+
+ private add(path: string) {
+ let current: TreeNode = this;
+ for (const part of getPathParts(path)) {
+ let next = current.get(part);
+ if (next === undefined) {
+ next = new TreeNode(part, joinPaths(current.path, part), current);
+ current.set(part, next);
+ }
+ current = next;
+ }
+ return current;
+ }
+
+ get parents(): TreeNode[] {
+ if (this._parents) {
+ return this._parents;
+ }
+ const parents: TreeNode[] = [];
+ let current: TreeNode | null = this.parent;
+ while (current !== null && current.parent !== null) {
+ parents.push(current);
+ current = current.parent;
+ }
+ return (this._parents = parents.reverse());
+ }
+
+ get children(): TreeNode[] {
+ return (this._children ??= Array.from(this.values()));
+ }
+}
+
+export const normalizeTreePath = (path: string) =>
+ path.length > 1 && path[path.length - 1] === '/' ? path.slice(0, -1) : path;
+
+export function getPathParts(path: string) {
+ const parts = path.split('/');
+ if (path[0] === '/') {
+ parts[0] = '/';
+ }
+
+ if (path[path.length - 1] === '/') {
+ parts.pop();
+ }
+
+ return parts;
+}
+
+export function joinPaths(path1: string, path2: string) {
+ if (!path1) {
+ return path2;
+ }
+
+ if (!path2) {
+ return path1;
+ }
+
+ if (path1[path1.length - 1] === '/') {
+ return path1 + path2;
+ }
+
+ return path1 + '/' + path2;
+}
+
+export function getParentPath(path: string) {
+ const normalized = normalizeTreePath(path);
+ const last = normalized.lastIndexOf('/');
+ if (last > 0) {
+ return normalized.slice(0, last);
+ }
+ return last === 0 ? '/' : normalized;
}
diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 89df31562e..84ae328ccc 100644
--- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -1,6 +1,5 @@
@@ -99,8 +86,8 @@
@@ -108,10 +95,10 @@
{/snippet}
-
+