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:
@@ -175,8 +175,6 @@ export interface UpdatePhotoBody {
|
||||
OriginalName?: string;
|
||||
Caption?: string;
|
||||
CaptionSrc?: 'manual' | '';
|
||||
Favorite?: boolean;
|
||||
Private?: boolean;
|
||||
Archived?: boolean;
|
||||
/** TakenAt + TakenAtLocal + Year/Month/Day must move in lockstep —
|
||||
* use `buildTakenAtPatch` to assemble all five fields from one ISO. */
|
||||
@@ -195,6 +193,16 @@ export interface UpdatePhotoBody {
|
||||
Details?: Partial<import('$lib/types/photoprism').PpDetails>;
|
||||
}
|
||||
|
||||
/** True when `s` is a real calendar date in strict `YYYY-MM-DD` form. Rejects
|
||||
* shape mismatches AND out-of-range parts that `Date` would silently roll
|
||||
* over (e.g. `2026-02-30` → Mar 2). */
|
||||
export function isValidISODate(s: string): boolean {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return false;
|
||||
const d = new Date(`${s}T00:00:00Z`);
|
||||
if (Number.isNaN(d.getTime())) return false;
|
||||
return d.toISOString().slice(0, 10) === s;
|
||||
}
|
||||
|
||||
export function buildTakenAtPatch(iso: string): UpdatePhotoBody {
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return {};
|
||||
@@ -264,18 +272,6 @@ export async function approvePhoto(uid: string): Promise<void> {
|
||||
await http.post(`/photos/${uid}/approve`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the heart/favorite flag. PhotoPrism has dedicated like/unlike
|
||||
* routes that are atomic; preferred over PUT for this one field.
|
||||
*/
|
||||
export async function likePhoto(uid: string): Promise<void> {
|
||||
await http.post(`/photos/${uid}/like`);
|
||||
}
|
||||
|
||||
export async function unlikePhoto(uid: string): Promise<void> {
|
||||
await http.delete(`/photos/${uid}/like`);
|
||||
}
|
||||
|
||||
// ── Stack file operations ───────────────────────────────────────────────────
|
||||
// PhotoPrism's stacks pack multiple file variants (RAW + JPG + Live + …) into
|
||||
// a single Photo entity. The duplicate-resolution flow needs two ops, both
|
||||
@@ -324,8 +320,6 @@ export interface PpFolder {
|
||||
Root: string;
|
||||
Title: string;
|
||||
FileCount?: number;
|
||||
Favorite?: boolean;
|
||||
Private?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,39 +348,6 @@ export async function listFolders(): Promise<PpFolder[]> {
|
||||
.filter((f) => f.Path !== '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Inbox / import staging area. PhotoPrism keeps uploaded-but-not-yet-indexed
|
||||
* files in a separate `/photoprism/import` root, exposed via
|
||||
* `/folders/import`. The endpoint returns the same `PpFolder[]` shape as
|
||||
* `/folders/originals`, but the photo counts come from `X-Files` and
|
||||
* `X-Folders` response headers since the body only lists subfolders.
|
||||
*
|
||||
* BasePath does NOT apply: `/folders/import` is a separate root from
|
||||
* originals (PhotoPrism's `import.path`, not under originals/), so we
|
||||
* don't filter the result by the signed-in user's BasePath. If/when
|
||||
* per-user inbox isolation is needed, that's a PhotoPrism-side feature.
|
||||
*/
|
||||
export interface ImportInfo {
|
||||
files: number;
|
||||
folders: number;
|
||||
subfolders: PpFolder[];
|
||||
}
|
||||
|
||||
export async function getImportInfo(): Promise<ImportInfo> {
|
||||
const res = await http.get<{ folders?: PpFolder[] }>('/folders/import', {
|
||||
params: { recursive: true, uncached: true, files: false }
|
||||
});
|
||||
const num = (h: unknown) => {
|
||||
const n = typeof h === 'string' ? parseInt(h, 10) : NaN;
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
};
|
||||
return {
|
||||
files: num(res.headers['x-files'] ?? res.headers['X-Files']),
|
||||
folders: num(res.headers['x-folders'] ?? res.headers['X-Folders']),
|
||||
subfolders: res.data.folders ?? []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-folder photo count for each `paths[]` entry. PhotoPrism's `/folders`
|
||||
* endpoint reports `FileCount: 0` even when populated, so the count has
|
||||
@@ -466,7 +427,6 @@ export interface PpLabel {
|
||||
Slug: string;
|
||||
CustomSlug?: string;
|
||||
Name: string;
|
||||
Favorite?: boolean;
|
||||
Priority?: number;
|
||||
Description?: string;
|
||||
PhotoCount?: number;
|
||||
@@ -551,7 +511,6 @@ export interface PpAlbum {
|
||||
Type: string;
|
||||
Title: string;
|
||||
Description?: string;
|
||||
Favorite?: boolean;
|
||||
PhotoCount?: number;
|
||||
CreatedAt?: string;
|
||||
UpdatedAt?: string;
|
||||
|
||||
Reference in New Issue
Block a user