feat(immich): draw a server as a folder in the library

A saved Immich server is a node on the column beside the drives: no handle,
no picker — the key lives on the backend and the page only ever calls
`/api/immich/*` on its own origin. The dialog takes the address, the name and
the key, checks the key before storing anything, and offers the albums the key
can see; a blank tick list and a blank id field both mean every album. A share
key is one album and so has no picker.

`syncImmich` turns a server's albums into the shapes the tree already draws —
rows in `photos`, names in `walked` — so the column, the wall and the strip
need nothing new. The node's menu is the disk's menu minus what a server has
no disk for.

Also fixes `saveWalkedDirs` filling its noughts in before folding the keys:
a reading that spelled its keys the way it found them (a server's album
labels) had the lower-case spelling of a counted key written in as nought,
and the fold that settles the two spellings kept the nought.
This commit is contained in:
2026-10-10 16:16:37 +07:00
parent d2e2ebe108
commit e27998d820
9 changed files with 1529 additions and 60 deletions
+7 -1
View File
@@ -316,7 +316,13 @@ export interface BackupResult extends BackupStatus {
export async function backupNow(onProgress?: (p: BackupProgress) => void): Promise<BackupResult> {
const root = await writable();
const [photos, edits, folders] = await Promise.all([listPhotos(), listEdits(), listFolders()]);
const [all, edits, allFolders] = await Promise.all([listPhotos(), listEdits(), listFolders()]);
// The copy is the disk's: a node that is really an Immich server and the frames
// read over the network from it are not on this machine, so they are not part of
// what a folder on this machine can hold. The catalogue still keeps them — see
// `immich.ts`, which reads them again on the next visit.
const photos = all.filter((p) => !p.remoteId);
const folders = allFolders.filter((f) => !f.source);
const had = await already();
const at0 = Date.now();
const cat = {