feat(web): fold duplicates+inbox into /review; sidebar UX cleanup
- /duplicates and /inbox routes removed and folded into /review as additional tabs alongside cause tabs; /duplicates keeps a redirect for bookmarks. - LeftSidebar: drop import/inbox tile and favorites; show per-user BasePath label at the folder root. - RightSidebar: split file header into read-only path over editable basename (matches sidecar rename contract); date field switches to plain-text ISO YYYY-MM-DD (no native datetime picker) with strict validation and revert-on-invalid-blur; preserves original hour. - BulkMetadataSidebar: same ISO-only date input with invalid-state styling and apply-button gating. - BulkActionBar: drop redundant Restore and Undo buttons; ⌘Z still reachable via gridKeyNav. - gridKeyNav: remove favorite toggle (F) alongside the favorites view retirement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,6 @@
|
||||
duplicateHeap,
|
||||
getAllMarks,
|
||||
getConfig,
|
||||
getImportInfo,
|
||||
heapDownloadUrl,
|
||||
listFolderCounts,
|
||||
listFolders,
|
||||
@@ -24,7 +23,6 @@
|
||||
scanCrossFolderDuplicates,
|
||||
triggerDownload,
|
||||
type CrossFolderScanResult,
|
||||
type ImportInfo,
|
||||
type PhotoMarksMap,
|
||||
type PpAlbum,
|
||||
type PpClientConfig,
|
||||
@@ -73,27 +71,17 @@
|
||||
enabled: isAuthenticated()
|
||||
}));
|
||||
|
||||
// Import staging area (PhotoPrism's `/import` root). Polled at a leisurely
|
||||
// 60s — the inbox only changes when files are uploaded or the indexer
|
||||
// runs, neither of which happens often enough to justify a tighter cadence.
|
||||
const importQuery = createQuery<ImportInfo>(() => ({
|
||||
queryKey: ['import'],
|
||||
queryFn: getImportInfo,
|
||||
enabled: isAuthenticated(),
|
||||
staleTime: 60_000
|
||||
}));
|
||||
|
||||
// View counts come from PhotoPrism's `/config` response, which carries a
|
||||
// precomputed counter for every common bucket (all/favorites/archived/
|
||||
// labels/places/…) updated incrementally on every mutation. Cheap to
|
||||
// refetch, and gives us a stable total — `/photos` only returns
|
||||
// per-page row counts via `X-Count`, never a total.
|
||||
// precomputed counter for every common bucket (all/archived/labels/
|
||||
// places/…) updated incrementally on every mutation. Cheap to refetch,
|
||||
// and gives us a stable total — `/photos` only returns per-page row
|
||||
// counts via `X-Count`, never a total.
|
||||
//
|
||||
// The key sits under the `['photos', …]` prefix so it inherits the
|
||||
// existing `invalidateQueries({ queryKey: ['photos'] })` calls scattered
|
||||
// across mutations (favorite, archive, restore, delete, heap add) — the
|
||||
// counter map refreshes whenever the photo list does. Marks-derived
|
||||
// counts (ratings/colors) react through the shared `['marks']` cache.
|
||||
// across mutations (archive, restore, delete, heap add) — the counter
|
||||
// map refreshes whenever the photo list does. Marks-derived counts
|
||||
// (ratings/colors) react through the shared `['marks']` cache.
|
||||
const configQuery = createQuery<PpClientConfig>(() => ({
|
||||
queryKey: ['photos', 'config'],
|
||||
queryFn: getConfig,
|
||||
@@ -267,6 +255,13 @@
|
||||
const rootActive = $derived(filters.folderPath === '/');
|
||||
const hasSubfolders = $derived((foldersQuery.data ?? []).length > 0);
|
||||
|
||||
// Root-folder label. Every account (admins included) gets a
|
||||
// BasePath named after them on disk, so surface that identity
|
||||
// here instead of an opaque "/".
|
||||
const rootLabel = $derived(
|
||||
session.user?.DisplayName?.trim() || session.user?.Name || '/'
|
||||
);
|
||||
|
||||
async function onSignOut() {
|
||||
await logout();
|
||||
await goto('/login', { replaceState: true });
|
||||
@@ -390,9 +385,9 @@
|
||||
// "everything visible in the main timeline" tally), so it matches what
|
||||
// the All photos view actually renders. `places` is the count of
|
||||
// geocoded locations — semantically what the Map view groups by.
|
||||
// Duplicates sums stacks + cross-folder groups; cross-folder only
|
||||
// contributes once its tab has been visited (the scan is opt-in
|
||||
// per-visit, not eager from the sidebar).
|
||||
// Review rolls in the duplicates tabs hosted under /review — stacks
|
||||
// always contributes; cross-folder only contributes once its tab has
|
||||
// been opened (the scan is lazy, not eager from the sidebar).
|
||||
type ViewItem =
|
||||
| { kind: 'section'; id: Section; label: string; getCount: () => number | undefined }
|
||||
| { kind: 'route'; href: string; label: string; getCount: () => number | undefined };
|
||||
@@ -402,8 +397,6 @@
|
||||
// separate "everything regardless of folder" destination would just
|
||||
// duplicate it for users whose photos live under the root.
|
||||
const views: ViewItem[] = [
|
||||
{ kind: 'route', href: '/inbox', label: 'Inbox', getCount: () => importQuery.data?.files },
|
||||
{ kind: 'section', id: 'favorites', label: 'Favorites', getCount: () => configQuery.data?.count?.favorites },
|
||||
{ kind: 'route', href: '/map', label: 'Map', getCount: () => configQuery.data?.count?.places },
|
||||
// Tags hosts four tabs (Labels (auto) / Keywords / Ratings / Colors);
|
||||
// the badge shows total labels + keywords + ratings + colors so the
|
||||
@@ -421,15 +414,14 @@
|
||||
];
|
||||
|
||||
const manageViews: ViewItem[] = [
|
||||
{ kind: 'route', href: '/review', label: 'Review', getCount: () => configQuery.data?.count?.review },
|
||||
{
|
||||
kind: 'route',
|
||||
href: '/duplicates',
|
||||
label: 'Duplicates',
|
||||
href: '/review',
|
||||
label: 'Review',
|
||||
getCount: () => {
|
||||
const stacks = stacksQuery.data?.length;
|
||||
if (stacks === undefined) return undefined;
|
||||
return stacks + (crossFolderQuery.data?.groups.length ?? 0);
|
||||
const review = configQuery.data?.count?.review;
|
||||
if (review === undefined) return undefined;
|
||||
return review + (stacksQuery.data?.length ?? 0) + (crossFolderQuery.data?.groups.length ?? 0);
|
||||
}
|
||||
},
|
||||
{ kind: 'section', id: 'hidden', label: 'Hidden', getCount: () => configQuery.data?.count?.hidden },
|
||||
@@ -560,9 +552,9 @@
|
||||
class="flex min-w-0 flex-1 items-center text-left"
|
||||
class:px-1={hasSubfolders}
|
||||
onclick={() => pickFolder('/')}
|
||||
title="Photos directly under originals/"
|
||||
title={userBasePath() === '' ? 'Your library' : `Your library (${userBasePath()})`}
|
||||
>
|
||||
<span class="truncate">/</span>
|
||||
<span class="truncate">{rootLabel}</span>
|
||||
{#if configQuery.data}
|
||||
<span
|
||||
class="ml-auto shrink-0 rounded px-1 text-[10px] tabular-nums {rootActive
|
||||
|
||||
Reference in New Issue
Block a user