feat(settings,index): per-user index sub-path, strip dead PP settings, scope duplicates
Lets a user pick a sub-folder under their library as a working index root, stored server-side (new sidecar user_prefs table). The Library tree, reindex, and both duplicate views (stacks + cross-folder scan) now re-root to it via a single userLibraryBase() helper. Also fixes the cross-folder scan/archive endpoints, which previously walked/touched the whole originals root instead of being scoped per-user (archive now rejects out-of-scope paths, 403). Removes PhotoPrism settings (Search/Maps/Server-UI/Features/Import) that only steered PhotoPrism's own bundled SPA and were never read by mulimage's UI. Also fixes the Library tree occasionally getting stuck on "Loading folders…" by dropping gcTime:0 and gating the spinner on isLoading instead of isPending. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,10 @@ export function adoptSession(resp: PpSessionResponse, cfg?: PpClientConfig): voi
|
||||
// (Hit this with the `test` user seeing the admin's library counts
|
||||
// in the left sidebar.)
|
||||
queryClient.clear();
|
||||
// The index sub-path is per-user; drop the prior identity's value so the
|
||||
// app re-roots to the new user's whole folder until the ['prefs'] query
|
||||
// rehydrates it from the sidecar.
|
||||
prefs.indexSubpath = '';
|
||||
session.id = resp.id;
|
||||
session.accessToken = resp.access_token;
|
||||
session.previewToken = (cfg ?? resp.config)?.previewToken ?? '';
|
||||
@@ -71,6 +75,7 @@ export function clearSession(): void {
|
||||
session.previewToken = null;
|
||||
session.downloadToken = null;
|
||||
session.user = null;
|
||||
prefs.indexSubpath = '';
|
||||
if (browser) localStorage.removeItem(STORAGE_KEY);
|
||||
// Same reasoning as adoptSession — wipe the cache so the next user
|
||||
// who logs in (or the login screen itself) doesn't render with the
|
||||
@@ -163,43 +168,73 @@ export function videoUrl(hash: string, format = 'avc'): string {
|
||||
/**
|
||||
* The signed-in user's library root, originals-relative, no leading/trailing
|
||||
* slash. `""` means "whole library" — used today by admin accounts whose
|
||||
* BasePath isn't configured in PhotoPrism. Non-empty values gate every place
|
||||
* that crosses the user↔server seam (sidebar tree, timeline `path:` filter,
|
||||
* folder counts, heap convert) so each user sees only their own subtree.
|
||||
* BasePath isn't configured in PhotoPrism. This is the user's *whole* folder
|
||||
* as set on their PhotoPrism account; the working library root the rest of
|
||||
* the app re-roots to is `userLibraryBase()` (BasePath + chosen sub-path).
|
||||
*/
|
||||
export function userBasePath(): string {
|
||||
return (session.user?.BasePath ?? '').replace(/^\/+|\/+$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-user "index sub-path": a folder *under* the user's BasePath that they've
|
||||
* chosen as their working library root. Stored server-side by the sidecar
|
||||
* (keyed by username) and hydrated into this reactive state at startup via the
|
||||
* `['prefs']` query. Empty string = "whole folder" (no narrowing). Normalized
|
||||
* to no leading/trailing slash.
|
||||
*/
|
||||
export const prefs = $state<{ indexSubpath: string }>({ indexSubpath: '' });
|
||||
|
||||
export function setIndexSubpathState(sub: string): void {
|
||||
prefs.indexSubpath = (sub ?? '').replace(/^\/+|\/+$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* The effective working library root, originals-relative, no leading/trailing
|
||||
* slash: the user's BasePath narrowed by their chosen index sub-path. This is
|
||||
* the single point the whole app re-roots through — `toOriginalsPath` /
|
||||
* `toUserPath` (and thus the sidebar tree, timeline `path:` filter, folder
|
||||
* counts, folder CRUD, reindex) all derive from it. When both are empty it's
|
||||
* `""` (whole library), matching the prior BasePath-only behavior.
|
||||
*/
|
||||
export function userLibraryBase(): string {
|
||||
const bp = userBasePath();
|
||||
const sub = prefs.indexSubpath;
|
||||
if (sub === '') return bp;
|
||||
return bp === '' ? sub : `${bp}/${sub}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a user-relative path (what the sidebar and URL deal in) to a
|
||||
* server-absolute, originals-relative path (what PhotoPrism's `path:`
|
||||
* operator and the sidecar's filesystem ops want).
|
||||
* operator and the sidecar's filesystem ops want). Relative to the effective
|
||||
* library root (`userLibraryBase()`), so the chosen index sub-path is folded
|
||||
* in automatically.
|
||||
*
|
||||
* "" or "/" → BasePath (user's root)
|
||||
* "2024/01" → "<basePath>/2024/01"
|
||||
* "" or "/" → libraryBase (user's working root)
|
||||
* "2024/01" → "<libraryBase>/2024/01"
|
||||
* null → "" (caller decides to omit the filter entirely)
|
||||
*/
|
||||
export function toOriginalsPath(uiPath: string | null): string {
|
||||
if (uiPath === null) return '';
|
||||
const bp = userBasePath();
|
||||
const base = userLibraryBase();
|
||||
const rel = uiPath.replace(/^\/+|\/+$/g, '');
|
||||
if (rel === '') return bp;
|
||||
return bp === '' ? rel : `${bp}/${rel}`;
|
||||
if (rel === '') return base;
|
||||
return base === '' ? rel : `${base}/${rel}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverse of `toOriginalsPath` — strips the user's BasePath prefix so the
|
||||
* UI can render `2024/01` instead of `users/alice/2024/01`. Paths that
|
||||
* are equal to the BasePath collapse to `""` (the user's root sentinel).
|
||||
* Paths outside the BasePath are returned as-is, but callers should
|
||||
* already have filtered those out via `listFolders`'s post-filter.
|
||||
* Inverse of `toOriginalsPath` — strips the effective library-root prefix so
|
||||
* the UI can render `2024/01` instead of `users/alice/2024/01`. Paths that
|
||||
* are equal to the root collapse to `""` (the user's root sentinel). Paths
|
||||
* outside the root are returned as-is, but callers should already have
|
||||
* filtered those out via `listFolders`'s post-filter.
|
||||
*/
|
||||
export function toUserPath(serverPath: string): string {
|
||||
const bp = userBasePath();
|
||||
const base = userLibraryBase();
|
||||
const sp = serverPath.replace(/^\/+|\/+$/g, '');
|
||||
if (bp === '') return sp;
|
||||
if (sp === bp) return '';
|
||||
if (sp.startsWith(bp + '/')) return sp.slice(bp.length + 1);
|
||||
if (base === '') return sp;
|
||||
if (sp === base) return '';
|
||||
if (sp.startsWith(base + '/')) return sp.slice(base.length + 1);
|
||||
return sp;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user