feat(library): smart subfolder auto-link and instant handle reconnection

This commit is contained in:
2026-10-05 10:03:09 +07:00
parent c4d904a207
commit e63183b298
4 changed files with 84 additions and 1 deletions
+2
View File
@@ -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'));
+81
View File
@@ -283,6 +283,86 @@ export async function listFolders(): Promise<LibraryFolder[]> {
}
}
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<void>((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<string, FileSystemDirectoryHandle>();
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<void>((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<LibraryFolder | null> {
@@ -292,6 +372,7 @@ export async function pickFolder(): Promise<LibraryFolder | null> {
).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;
}