From adcc6eb67b19a55f655892a99a2269a411306350 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Mon, 12 Oct 2026 14:57:00 +0700 Subject: [PATCH] feat(library): a cloud account is changed where the cloud accounts are listed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dialog knew two shapes: an add form with the saved links above it, and an album-only view that hid the form. So a saved server could be retick'd but never renamed, and the duplicate-address warning was only readable from the add side. One shape now. The list is always drawn, every row carries SỬA beside REMOVE THE LINK, and SỬA puts that server's name and address in the fields below — prefill, edit, save. A blank key is the one that server already answers by, so a rename does not ask for it again, and the backend's single PUT probes before it stores either way. The desktop bar's button says TÀI KHOẢN CLOUD once there is an account to manage, and THÊM IMMICH while there is not. --- docker/frontend/src/Library.tsx | 68 +++++--- docker/frontend/src/i18n/en.ts | 3 + docker/frontend/src/i18n/vi.ts | 3 + docker/frontend/src/ui/ImmichDialog.tsx | 209 ++++++++++++------------ 4 files changed, 160 insertions(+), 123 deletions(-) diff --git a/docker/frontend/src/Library.tsx b/docker/frontend/src/Library.tsx index 124280b..9bc22b4 100644 --- a/docker/frontend/src/Library.tsx +++ b/docker/frontend/src/Library.tsx @@ -394,7 +394,7 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) { const [servers, setServers] = useState([]); const [serverYears, setServerYears] = useState([]); const [addImmich, setAddImmich] = useState(false); - const [editAlbums, setEditAlbums] = useState(null); + const [editServer, setEditServer] = useState(null); useEffect(() => { if (!servers.length) return; @@ -1371,24 +1371,28 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) { [folders, reload] ); - // A server that has just been saved, or one whose albums have been reticked. A - // new server becomes a folder on the column the moment it is saved: nothing - // walks it, so there is nothing to wait for. An existing node keeps its name and - // its frames, which the sync below reads again for the albums it now keeps. + // A server that has just been saved, or one whose name, address or albums have + // just been changed. A new server becomes a folder on the column the moment it is + // saved: nothing walks it, so there is nothing to wait for. An existing one keeps + // its frames, and every node reading by it is walked again for the albums it now + // keeps — which is why the folder's own copy of that list has to be written before + // the sync reads it. const serverSaved = useCallback( async (saved: ImmichServer, isNew: boolean) => { setAddImmich(false); - const edited = editAlbums; - setEditAlbums(null); + setEditServer(null); // The saved list is read back rather than patched: the backend is what // holds the key, and what it says is what the dialog will show next time. const config = await immichConfig(true); setServers(config.servers); - if (!isNew && edited) { - const next: LibraryFolder = { ...edited, albums: saved.albums }; - await putFolder(next); - setFolders((prev) => prev.map((f) => (f.name === next.name ? next : f))); - await syncServer(next); + if (!isNew) { + for (const f of await listFolders()) { + if (f.server !== saved.id) continue; + const next: LibraryFolder = { ...f, albums: saved.albums }; + await putFolder(next); + await syncServer(next); + } + setFolders(await listFolders()); return; } const current = await listFolders(); @@ -1398,7 +1402,7 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) { setNode(folder.name); await syncServer(folder); }, - [editAlbums, syncServer] + [syncServer] ); // The shelf's search box, run. What is typed is a name or a tag, and Immich is @@ -2509,7 +2513,7 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) { admin={admin} onOpenStudio={open} onAddImmich={() => setAddImmich(true)} - onEditAlbums={(folder) => setEditAlbums(folder)} + onEditAlbums={(folder) => setEditServer(folder.server ?? null)} onSyncServer={syncServer} onDisconnectServer={disconnect} onFindImmich={findImmich} @@ -2528,16 +2532,26 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) { {addImmich ? ( { + setAddImmich(false); + setEditServer(id); + }} onForget={(id) => void forgetLink(id)} onSaved={(saved, isNew) => void serverSaved(saved, isNew)} onClose={() => setAddImmich(false)} /> ) : null} - {editAlbums ? ( + {editServer ? ( s.id === editAlbums.server) ?? null} + server={servers.find((s) => s.id === editServer) ?? null} + servers={servers} + onEdit={setEditServer} + onForget={(id) => { + setEditServer(null); + void forgetLink(id); + }} onSaved={(saved, isNew) => void serverSaved(saved, isNew)} - onClose={() => setEditAlbums(null)} + onClose={() => setEditServer(null)} /> ) : null} @@ -2599,7 +2613,7 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) { data-key="lib-add-immich" onClick={() => setAddImmich(true)} > - {t('lib.immich.add')} + {servers.length ? t('lib.cloud.manage') : t('lib.immich.add')} {live || scanQueue().length ? ( + + + ); + })} + + + ) : null} + + {/* The three answers are the same at the door and behind it: a server being + added and one being changed are the same name, address and key. The one + difference is the key — a saved server already has one, so what is typed + here is the *new* one, and blank means the question was not asked. */} + + setName(e.target.value)} + /> + + setUrl(e.target.value)} + /> + + setKey(e.target.value)} + /> + + {/* Checking a key is the door's own question — a saved server has already + answered it, and the save below probes again anyway. */} + {editing ? null : ( <> - {/* What the account already reads by: one row per saved address, and - the address is what makes two rows one server. Two links to the - same Immich read the same library twice, so the row says so and - the way out is here rather than in a node's own menu — a duplicate - is a fact about the list, not about a folder. */} - {servers.length ? ( -
-

{t('lib.immich.links')}

-
    - {servers.map((s) => { - const twin = servers.some((o) => o.id !== s.id && o.url === s.url); - return ( -
  • - {s.name} · {s.url} ({s.albums?.length ?? 0}) - {twin ? {t('lib.immich.dup')} : null} - -
  • - ); - })} -
-
- ) : null} - - setName(e.target.value)} - /> - - setUrl(e.target.value)} - /> - - setKey(e.target.value)} - />