web: keep a saved photo's look in EXIF and its own row
EXPORT no longer burns the caption strip: the pixels stay the photo's own and the look travels as metadata — ImageDescription (0x010e) for the tag, UserComment (0x9286, ASCII header) for the recipe JSON. SAVE PHOTO now stores the look with the frame (photos.recipe) and the uploader's consent for the community film strip (photos.consent, PATCH /api/photos/:id for the owner). The landing reel skips non-consented frames, and a new MY PHOTOS tab lists the account's saves, reopens one with the settings it was stored with, and carries the two consent switches.
This commit is contained in:
@@ -46,6 +46,12 @@ export interface Photo {
|
||||
tag: string | null;
|
||||
title: string | null;
|
||||
meta: string | null;
|
||||
// The uploader's consent to show this frame on the landing film strip. Own
|
||||
// rows carry it back; the public reel only returns consented ones anyway.
|
||||
consent?: boolean;
|
||||
// The look the photo was saved with, on the owner's own listing only — the
|
||||
// studio reads it back so a saved photo can be reopened for editing.
|
||||
recipe?: Recipe | null;
|
||||
}
|
||||
|
||||
// What a caller may attach to an upload. Same three labels.
|
||||
@@ -137,17 +143,24 @@ export const api = {
|
||||
// labels ride the query string, since the body is the image itself.
|
||||
listPhotos: () => call<{ photos: Photo[] }>('/photos'),
|
||||
listMyPhotos: () => call<{ photos: Photo[] }>('/photos/mine'),
|
||||
uploadPhoto: async (file: File, labels?: PhotoLabels) => {
|
||||
uploadPhoto: async (file: File, labels?: PhotoLabels, opts?: { recipe?: Recipe; consent?: boolean }) => {
|
||||
const q = new URLSearchParams();
|
||||
if (labels?.tag) q.set('tag', labels.tag);
|
||||
if (labels?.title) q.set('title', labels.title);
|
||||
if (labels?.meta) q.set('meta', labels.meta);
|
||||
// The look rides the query string too, so the photo reopens with its own
|
||||
// settings. The server caps the length and drops anything not an object.
|
||||
if (opts?.recipe) q.set('recipe', JSON.stringify(opts.recipe));
|
||||
if (opts?.consent === false) q.set('consent', '0');
|
||||
const suffix = q.size > 0 ? `?${q}` : '';
|
||||
const res = await uploadBytes(`/api/photos${suffix}`, file, MAX_PHOTO_DIM, MAX_PHOTO_UPLOAD);
|
||||
const body = await readJson(res);
|
||||
if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
|
||||
return body as unknown as { photo: Photo };
|
||||
},
|
||||
// Toggle this photo's place on the landing film strip. Owner-only.
|
||||
setPhotoConsent: (id: number, consent: boolean) =>
|
||||
call<{ id: number; consent: boolean }>(`/photos/${id}`, { method: 'PATCH', body: JSON.stringify({ consent }) }),
|
||||
// The owner's own delete; an admin may pass any id here too.
|
||||
deletePhoto: (id: number) => call<void>(`/photos/${id}`, { method: 'DELETE' }),
|
||||
photoUrl: (id: number) => `/api/photos/${id}/file`,
|
||||
|
||||
Reference in New Issue
Block a user