Mulimage 2.0 #1
Reference in New Issue
Block a user
Delete Branch "new"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Sidebar - New `/` root-folder entry at the top of the Folders group. Active when the timeline is scoped to root; the photo grid post-filters to `Path === ''` because PhotoPrism's `path:` operator can't express an exact-root match. Collapsible chevron, persisted to its own localStorage key, and a kebab carrying just "New subfolder". - Per-folder count badges. `/api/v1/photos?q=path:X&count=1000` per folder in parallel via `listFolderCounts`; root count derived from `config.count.all − Σ subfolder counts`. - Folder tree starts at depth=1 under the root so nested rows indent visually relative to `/`. - Footer matches the Toolbar / action-bar h-9 height. Timeline interaction - Single click on a tile selects only that tile (clears others); the preview now lives on dblclick. Modifier clicks still go through `gridKeyNav`'s document handler (shift = range, cmd/ctrl = toggle). - `x` (archive) now actually archives — PhotoPrism's photo PUT silently drops the Archived field, so we route through /batch/photos/{archive,restore} the same way the BulkActionBar already did. Mirror for `u`. - Preview close restores the timeline focus + scrolls the last-shown photo into view via `forcedExpand`+`scrollTileIntoView` so it actually mounts (selection ring would otherwise stay invisible when the user navigated far in preview). - `applyFolderScope` only narrows the timeline to root when the active view is a folder view (no heap / search / non-default section), so label clicks / heap views / favorites no longer drop subfolder photos. Action bar - Inline `h-9` row at the bottom of the main column (not `fixed`), matching the Toolbar's visual language. Right sidebar stays full height — the bar only spans the timeline width. - Approve action wired for the review pile. Colors / Tags / Ratings drill-ins - New shared `PhotoGrid` component owning tile rendering, selection styling, single-click-selects + dblclick-previews, and `setOrder` for arrow-key nav. - Each route's drill-in `<main>` carries `use:gridKeyNav` and a trailing `<BulkActionBar />` so shift/cmd/ctrl click, arrow keys, and the keyboard shortcuts work the same as the timeline. - Tags switches from `goto('/?q=label:…')` to an in-place drill-in with a back button, mirroring `/colors`'s flow. - Category cards + drill-in photo cards honour the global `view.thumbnailSize` (XS–XL) so the timeline's size selector now reaches into all four grids. Settings - General-settings dialog merges Appearance into UI and switches free text inputs to selects for the PhotoPrism theme / language / start page / map style (the value-from-server prepends if it's outside the curated list so we never silently rewrite a custom value). Time zone uses `<datalist>` with `Intl.supportedValuesOf('timeZone')`. Sidecar - Heap convert runs reindex synchronously per source path so the client's invalidate-and-refetch sees the moved files. Inbox - New /inbox route stub for the upcoming import workflow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>The SvelteKit /login was username/password only; the legacy comment even called out 'OIDC SSO ships in M4 when the IdP is wired up'. Authentik is wired up now, so: - /api/v1/config exposes ext.oidc when the IdP is configured. Fetch it on the login page and conditionally render "Sign in with {provider}", which kicks off /api/v1/oidc/login. - After PhotoPrism completes the auth code exchange, it sets `auth_token` + `auth_session` cookies and redirects to siteUrl (/library/browse by default; the deployment's reverse proxy is expected to bounce that to /). bootstrapSessionFromCookies() reads those cookies, calls GET /api/v1/session/<id> with the cookie's token, and adopts the resulting session into the SPA store on mount. - Root layout's auth guard now waits for the bootstrap pass before punting to /login, so a fresh OIDC return doesn't get redirected away before the session is read.PhotoPrism's /api/v1/oidc/redirect handler doesn't actually set auth_token/auth_session cookies — it returns an HTML page that does: setItem("pp:<storageNamespace>:session.id", <session uid>) setItem("pp:<storageNamespace>:session.token", <X-Auth-Token value>) setItem("pp:<storageNamespace>:session.user", <user JSON>) setItem("pp:<storageNamespace>:session.provider", "oidc") window.location.href = "/library/login"; The deployment's reverse proxy is expected to bounce /library/login (and /library/*) back to `/`; the SPA then reads PhotoPrism's storageNamespace from /api/v1/config, looks up session.id and session.token under that prefix, and adopts the session. Confirmed via the M0 test instance: prior to this change, server-side sessions were created on every OIDC return (DB row present) but the browser had no way to claim them, so the user bounced back to /login.Six-item frontend performance pass on the SvelteKit app. P1 — Move per-folder photo counts to a new sidecar endpoint and defer the fetch to requestIdleCallback. The old client-side path fired one /photos?count=1000 per folder from the browser (≈1 MB JSON × N folders) on every cold sidebar mount; the new POST /api/sidecar/folders/counts fans out over loopback with bounded concurrency and returns a single {path: count} payload of a few KB. P2 — Bound the visibleRange scroll-scan around the previous visible band instead of sweeping every shell from index 0 on each scroll-rAF. Falls back to a full sweep on cache miss (filter reset, programmatic jump) so behaviour is unchanged at the edges. P3 — Adaptive thumbnail size + srcset. PhotoTile now picks the smallest PhotoPrism tile_* variant (100/224/500) that covers the user's grid preset at the current DPR. Adds decoding="async". P4 — Lift the selection check above the {#each} loop. Mostly readability — SvelteSet.has() is already per-key reactive — but keeps the hot loop body terse. P5 — Split dedupedAll / photos derivations so filter-store mutations (search-as-you-type, section toggles) don't re-walk every loaded page; only the cheap folder-scope filter re-runs. P6 — Dynamic-import PreviewOverlay on first preview.uid !== null and cache the loaded module; closing the overlay leaves the component mounted with its internal {#if} collapsing the DOM. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>filtersToQ emitted `path:<folder>` for any non-root folder, but PhotoPrism's `path:` operator is exact-by-default — so picking the "2024" node in the folder tree returned zero hits when all photos lived in date-stamped sub-folders (`2024/01`, `2024/02`, …). PP's indexer always nests photos under YYYY/MM, so every year-level folder was empty in the timeline. PhotoPrism supports a trailing `*` wildcard, so emit `path:"<folder>*"` instead: path:"2024*" → matches `2024`, `2024/01`, `2024/02/...`, … path:"2024/01*" → matches `2024/01` plus descendants — still correct for a leaf folder. Confirmed against the M0 instance: picking 2024 now returns the full year's photos; 2024/01 still returns its direct contents.GPS, Credits & notes, and File sections in the right sidebar now read and write their expanded state through the view store and persist it to localStorage. Closed by default; the user's first toggle pins their choice across subsequent photos and reloads. Switched from the previous data-driven defaults ("open if this photo has GPS / IPTC fields") to static defaults: a data-driven default would change between photos, fire a programmatic `toggle` event on the <details> element, and silently overwrite the user's persisted choice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Three small cleanups bundled: - Remove the `console.debug('[indexer]', ...)` line in the indexer store. The PhotoPrism WS protocol is now verified; the log was a development aid that no longer earns its console noise. - GeneralSettingsDialog: normalize cloned PpSettings so `ui` / `search` / `maps` are always real objects (some deployments return them unset), and re-clone the draft on each open instead of nulling it on close. The previous lifecycle let Dialog's exit animation keep the form mounted while `draft` was already null, which threw at runtime via the `bind:value={draft.ui!.theme}` getters. - Search-input placeholder string: rewrite as a JS expression so the embedded `"vacation"` quotes inside the example don't terminate the HTML attribute early. The previous form was a Svelte parse error that stopped the dev-server module from loading. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>The /review cards now share the timeline's PhotoGrid, gridKeyNav, BulkActionBar, and global selection store instead of carrying parallel implementations. The card is reduced to a header (title + count + Dismiss / Archive all), an advisory caption, and a PhotoGrid; keyboard nav, hover affordances, multi-select, and bulk verbs come from the shared machinery. - New web/src/lib/services/photoActions.ts holds the canonical dismissPhotos / archivePhotos helpers (toast wording, focus advance, undo push, ['photos'] + ['review-groups'] cache invalidation). BulkActionBar.onApprove / onArchive and gridKeyNav.approveCullTargets / toggleArchive('archive') route through it. CauseGroupCard's Dismiss / Archive-all buttons call the same helpers - one code path from any surface. - Approve verb renamed to "Dismiss" across BulkActionBar, gridKeyNav toasts ("Kept N" -> "Dismissed N"), and the new review card. The BulkActionBar Clear/Dismiss clear button is just "Clear" now so the verb only means the action. - /review sets filters.section='review' on mount and restores on unmount, which is what swings the shared action surface into review semantics; an effect clears the selection on tab change so a previously-selected photo from another cause can't be hit by a new tab's bulk verb. - The route mounts BulkActionBar at the bottom and swaps the right aside to BulkMetadataSidebar when selection.ids.size >= 2 - same as the timeline; gives the user a one-shot "apply this Date / Caption / Keyword to all selected" affordance for EXIF-stripped batches. - CauseGroupCard drops its bespoke keyboard handler, ResizeObserver, focusedIdx state, per-tile hover Approve/Archive buttons, confirm() dialogs, and toast.loading worker loop. The unused CauseBadges component is removed.PhotoPrism's OSS edition has no way to map OIDC claims to BasePath, so every freshly-registered OIDC user lands with BasePath="" and either sees the whole library (admin) or nothing (guest) — never their own subfolder. Introduces a sidecar-driven reconciler with a single env knob the admin sets in docker-compose / .env.photoprism: USER_BASEPATHS="test:test, alice:family/alice, bob:bob" (`user:originals-relative-path` pairs, comma-separated.) On boot and every 60s thereafter the sidecar: - mkdir -p's the target subdirectory under ORIGINALS_ROOT so PhotoPrism's path: ACL filter has somewhere real to point; - UPDATEs photoprism.auth_users.base_path for the matching row where it differs (idempotent, missing users skipped — they materialise on first OIDC login and the next pass catches them). The reconciler uses a separate gorm connection scoped to the `photoprism` schema with PhotoPrism's own DB user, since the existing `sidecar` user only has grants on `mule_sidecar.*`. Connection stays dormant when PP_DB_PASSWORD is empty — the feature is opt-in via env. Compose changes: thread PP_DB_* + USER_BASEPATHS through to the sidecar service. New users.go file isolates the reconciler logic; main.go calls startUserBasepathReconciler() during boot.Replace the fullscreen PreviewOverlay with an inline top pane above each grid surface. SplitGrid + InlinePreview render the focused photo/video inside the host page; a resizableVertical action drives the divider and the height persists via the view store. Applied to the timeline, /tags drill-in, /review cause tabs, /photo/[uid], and /map. selection.focused is now the single source of truth for both the inline pane and the right sidebar — preview.svelte store and PreviewOverlay are removed. Sidebar: drop the thumb; lead with icon-led filename and folder rows that match the date/place rhythm. Move dims+size to the top (below date) and camera/lens/exposure into the collapsible File section. Read-only spans share the input padding so the text column aligns across rows. Folder row sits between date and dims+size. VideoPlayer: stop forcing width/height: 100% so videos honour their intrinsic aspect ratio inside the pane. Key the player on file hash in InlinePreview so navigating between videos remounts the element and autoplay fires again. Sidebar (LeftSidebar): switch the labels badge to a dedicated countPhotos('label:*') query so it reports photos with a label rather than PhotoPrism's category roll-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Delete the old Python+React mule-image stack (backend/, frontend/, docker-compose.yml, mulita.yml, .env*) plus the one-shot migration and sample dirs (migrate/, photos-sample/, photovault-app-prompt.md). Only the PhotoPrism + Go sidecar + SvelteKit web stack remains, so drop the ".photoprism." qualifier from the compose+env filenames. Bind PhotoPrism's port to 127.0.0.1 so the user-facing surface is just the SvelteKit web/ app; admin reaches PP's UI via SSH tunnel. Flatten PHOTOPRISM_INDEX_WORKERS' nested default (podman-compose's interpolator doesn't expand ${A:-${B:-…}}). Rewrite README for the current stack. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Replaces ad-hoc "Loading…" text and bare empty messages with two shared feedback primitives that carry subtle lucide icons, consistent muted-foreground/destructive tones, and a11y signaling (role=status, aria-busy, role=alert on destructive empties). Loading copy gains context ("Loading photos/folders/heaps/metadata…") and the right- sidebar idle state moves from a "ⓘ" glyph to a MousePointerClick icon. SkeletonGrid stays as the initial-grid loader. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>label:*query fc5f30fad1PhotoPrism's q-DSL has no "has any label" predicate: `label:*` matches every photo regardless of label, `label:<slug>` only matches that one slug, and `keywords:*` behaves the same way. The prior `all:true label:*` returned a 400 (and the earlier "drop all:true" follow-up made it return the unfiltered library size, which then fed into tagsTotal and inflated the parent Tags badge to ~library_size on admin sessions). Switch labelsBadge to the precomputed `configQuery.count.labels` — the same source the Tags sub-row's `tagCategoryCount('labels')` already uses. The parent Tags badge now sums the exact same numbers the sub-rows display: labels, keywords, people (distinct slugs/keywords/subjects) plus ratings/colors (photos carrying each mark). Drop the wantScoped short-circuit since the values are all library-wide now anyway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Expand color labels from the 4-swatch Lightroom culling palette to 8 neutral colors (red/orange/yellow/green/teal/blue/purple/pink) with no attached semantics, rendered as outlines that fill in when picked. Carousel tiles now show a VIDEO badge, and the folder row in the right sidebar always renders ("/" for root) instead of disappearing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Refactor suggestDateFromPath to combine multiple signals instead of trying patterns in priority order: - Filename Y-M-D corroborated by path Y-M/Y-M-D → HIGH (filename- agrees-path). Fixes the case where a Samsung-style 20240226_xxx.jpg under 2024/02/ was returning the path-only 2024-02-01. - Filename Y-M-D with no path signal → HIGH (filename-only). - 10/13-digit Unix epoch in basename → HIGH (unix-timestamp) — covers WeChat (mmexport...) and FB saves. - Path Y-M-D → HIGH (path-ymd). - Path Y-M only → MEDIUM (path-ym-default-day, synthesised day=01). Sidebar row labels these "(estimated day)" so the user knows. Filename parser now accepts `.` and space separators (covers macOS screenshots, manual 2024.02.26 renames). Path parser accepts `.` too. OriginalName participates as a secondary filename signal when present and different from the on-disk basename. Patterns we explicitly DO NOT parse, to avoid silent date flips: DD-MM-YYYY / MM-DD-YYYY, 2-digit years, bare camera sequence numbers. Add photoNameAndDir(p) helper next to primaryFile so RightSidebar, BulkActionBar, photoActions, and gridKeyNav all derive {fileName, path} the same way — fixes the bug where photo.FileName was undefined on the single-photo detail endpoint and the basename branch was being skipped entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>RightSidebar: - Folder + Location rows gain a small ArrowUpRight icon button that deep-links into the timeline / map view focused on the photo. OSM external link removed; the in-app map nav covers the same job. Folder navigation: - New navigateToFolder(path, { focusUid, focusTakenAt }) helper in the filters store; LeftSidebar's pickFolder collapses to a one-liner that reuses it. - One-shot pending-focus stash carries both UID and TakenAt across the goto. URL-watch effect on the timeline consumes the stash so even same-folder navigations (where the filter doesn't change) get picked up. Anchor-mode timeline query: - listPhotosAround(q, takenAt, after, before) issues two parallel PhotoPrism calls (`after:<day-1>` oldest-first + `before:<day+1>` newest-first), merges + dedupes newest-first. Uses PhotoPrism's existing date-only DSL clauses — no server changes. - When a deep-link stashes a TakenAt, page 0 of the photosQuery uses the merged window so the target photo is loaded even for photos buried past the standard newest-first cursor. Pages 1+ are disabled in anchor mode (PhotoPrism's day-precision cursor would infinite-loop on dense days; users see 120 around the target, refresh to drop the anchor). - After page 0 lands, the existing scrollToIndex(targetIdx) expands the windowed render set + scrolls the tile into view. Map view: - /map honors `?lat=&lng=&zoom=&focus=` URL params, jumping to the photo's coordinates at zoom 17 instead of fitBounds-ing the full library. Params are stripped after first apply so a manual zoom-out + reload doesn't snap back. LeftSidebar root count badge: - Now matches what Cmd+A selects in the timeline. Old code used /config.count.all (library aggregate, includes archived/hidden/ review). Switched to countPhotos('', { merged: true }) which counts the actual photo entries the timeline lists. - countPhotos gains a `merged` option; with merged=true it returns the response body length instead of the X-Count header — PhotoPrism's X-Count is always the file-row count regardless of merged, so a HEIC + JPG companion pair inflated the badge to 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>