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:
2026-05-18 21:48:38 +02:00
parent a38c3c6e9b
commit d70244f17e
15 changed files with 294 additions and 703 deletions

View File

@@ -4,14 +4,13 @@
* string PhotoPrism's `q` parameter accepts.
*
* Sections behave like saved searches: picking a section sets a default
* filter shape (favorites → `favorite:true`, archive → `archived:true`,
* etc.), and the search box on top stacks an additional `q` term.
* filter shape (archive → `archived:true`, etc.), and the search box on
* top stacks an additional `q` term.
*/
import { toOriginalsPath, userBasePath } from '$lib/stores/session.svelte';
export type Section =
| 'all-photos'
| 'favorites'
| 'review'
| 'archive'
| 'hidden'
@@ -30,7 +29,7 @@ export interface FilterState {
// Default landing = root folder (`/`). The Folders group sits at the top
// of the sidebar; landing inside it gives users a stable starting point
// instead of dumping them into the full library. Picking any other view
// (favorites, a heap, "All photos") clears `folderPath` to `null`.
// (a heap, "All photos") clears `folderPath` to `null`.
export const filters = $state<FilterState>({
section: 'all-photos',
heapUid: null,
@@ -69,9 +68,6 @@ function quoteIfNeeded(v: string): string {
export function filtersToQ(f: FilterState = filters): string {
const parts: string[] = [];
switch (f.section) {
case 'favorites':
parts.push('favorite:true');
break;
case 'review':
// PhotoPrism's review pile: photos the indexer flagged as
// uncertain (low quality score). Cleared per-photo via the
@@ -127,7 +123,7 @@ export function filtersToQ(f: FilterState = filters): string {
export function parseUrlParams(params: URLSearchParams): Partial<FilterState> {
const sectionRaw = params.get('section') as Section | null;
const section: Section =
sectionRaw && ['all-photos', 'favorites', 'review', 'archive', 'hidden', 'heap'].includes(sectionRaw)
sectionRaw && ['all-photos', 'review', 'archive', 'hidden', 'heap'].includes(sectionRaw)
? sectionRaw
: 'all-photos';
// Bare URL (no section/folder/heap/q params) lands on the root folder