From a730441918eac371f2ee3fae9c903927b22448f8 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Mon, 5 Oct 2026 10:03:09 +0700 Subject: [PATCH] feat(library): smart subfolder auto-link and instant handle reconnection --- docker/frontend/src/Library.tsx | 2 + docker/frontend/src/engine/library.ts | 81 ++++++++++++++++++++++++++ docker/recipescam-images.tar.gz.sha256 | 2 +- 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/docker/frontend/src/Library.tsx b/docker/frontend/src/Library.tsx index 5861543..a8016c1 100644 --- a/docker/frontend/src/Library.tsx +++ b/docker/frontend/src/Library.tsx @@ -20,6 +20,7 @@ import { pickFolder, readPhotoFile, readPhotos, + reconnectPhotosForFolders, removeFolder, renameFolder, scanAsked, @@ -713,6 +714,7 @@ export function Library() { async (folder: LibraryFolder) => { if (await ensurePermission(folder.handle)) { setReady((prev) => new Set(prev).add(folder.name)); + await reconnectPhotosForFolders(folder.handle); await scan(folder); } else { setNote(t('lib.failed')); diff --git a/docker/frontend/src/engine/library.ts b/docker/frontend/src/engine/library.ts index 9c09fce..67769ec 100644 --- a/docker/frontend/src/engine/library.ts +++ b/docker/frontend/src/engine/library.ts @@ -283,6 +283,86 @@ export async function listFolders(): Promise { } } +export async function reconnectPhotosForFolders( + newFolderHandle: FileSystemDirectoryHandle +): Promise<{ reconnectedFolders: number; reconnectedPhotos: number }> { + const existingFolders = await listFolders(); + const allPhotos = (await readPhotos()) ?? []; + let reconnectedFolders = 0; + let reconnectedPhotos = 0; + + const folderTargets: { name: string; handle: FileSystemDirectoryHandle }[] = []; + folderTargets.push({ name: newFolderHandle.name, handle: newFolderHandle }); + + const knownFolderNames = new Set([ + ...existingFolders.map((f) => f.name), + ...allPhotos.map((p) => p.folder), + ]); + + for (const name of knownFolderNames) { + if (folderTargets.some((t) => t.name === name)) continue; + try { + const sub = await openAt(newFolderHandle, name); + if (sub) { + folderTargets.push({ name, handle: sub }); + } + } catch {} + } + + const db = await openDb(); + await new Promise((resolve, reject) => { + const tx = db.transaction(FOLDERS, 'readwrite'); + const store = tx.objectStore(FOLDERS); + for (const target of folderTargets) { + store.put({ name: target.name, handle: target.handle }); + reconnectedFolders++; + } + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + + const photosToUpdate: LibraryPhoto[] = []; + for (const target of folderTargets) { + const matchingPhotos = allPhotos.filter((p) => p.folder === target.name && !p.handle); + if (!matchingPhotos.length) continue; + + const dirCache = new Map(); + dirCache.set('', target.handle); + + for (const p of matchingPhotos) { + try { + let dirHandle = dirCache.get(p.dir); + if (!dirHandle) { + const opened = await openAt(target.handle, p.dir); + if (!opened) continue; + dirCache.set(p.dir, opened); + dirHandle = opened; + } + const fileHandle = await dirHandle.getFileHandle(p.name); + if (fileHandle) { + p.handle = fileHandle; + photosToUpdate.push(p); + reconnectedPhotos++; + } + } catch {} + } + } + + if (photosToUpdate.length) { + await new Promise((resolve, reject) => { + const tx = db.transaction(PHOTOS, 'readwrite'); + const store = tx.objectStore(PHOTOS); + for (const p of photosToUpdate) { + store.put(p); + } + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + } + + return { reconnectedFolders, reconnectedPhotos }; +} + // The picker's own dialog; `id` makes the browser reopen at the folder this page // was last pointed at, which is what makes a second visit one click. export async function pickFolder(): Promise { @@ -292,6 +372,7 @@ export async function pickFolder(): Promise { ).showDirectoryPicker({ mode: 'readwrite', id: 'recipescam-library' }); const folder: LibraryFolder = { name: handle.name, handle }; await ask(FOLDERS, 'readwrite', (s) => s.put(folder)); + await reconnectPhotosForFolders(handle); return folder; } diff --git a/docker/recipescam-images.tar.gz.sha256 b/docker/recipescam-images.tar.gz.sha256 index b0b2830..66891a2 100644 --- a/docker/recipescam-images.tar.gz.sha256 +++ b/docker/recipescam-images.tar.gz.sha256 @@ -1 +1 @@ -873957f0d5504c2b72e317760f884fad27902555542e7374874ff4afee38057d recipescam-images.tar.gz +78b3b64e0d6600e68a92938f21540f5e7e5c7d1212d90de36050c139031790cd recipescam-images.tar.gz