feat(library): RESET SETTING puts a frame back to the tile it came in with
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
|||||||
remoteUrl,
|
remoteUrl,
|
||||||
removeFolder,
|
removeFolder,
|
||||||
renameFolder,
|
renameFolder,
|
||||||
|
resetEdit,
|
||||||
scanAsked,
|
scanAsked,
|
||||||
scanBusy,
|
scanBusy,
|
||||||
scanQueue,
|
scanQueue,
|
||||||
@@ -1277,6 +1278,23 @@ function Catalogue({ admin }: { admin: boolean }) {
|
|||||||
go(`/app?lib=${encodeURIComponent(photo.id)}`);
|
go(`/app?lib=${encodeURIComponent(photo.id)}`);
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
|
// Put a frame back: the recipe goes, the graded tile goes with it, and the frame
|
||||||
|
// is drawn again from the negative. Everything this screen holds a copy of goes
|
||||||
|
// too — the URL it is painting and the mark that says the frame was ever touched
|
||||||
|
// — which is what makes the wall draw the tile it is about to be handed.
|
||||||
|
const resetSetting = useCallback(async (photo: LibraryPhoto) => {
|
||||||
|
const blob = await resetEdit(photo).catch(() => null);
|
||||||
|
dropThumbUrl(photo.id);
|
||||||
|
if (blob) cacheObjectUrl(photo.id, blob);
|
||||||
|
setEdited((prev) => {
|
||||||
|
if (!prev.has(photo.id)) return prev;
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(photo.id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setTilesAt((n) => n + 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const rootFolder = useMemo(() => {
|
const rootFolder = useMemo(() => {
|
||||||
if (!node) return folders[0] ?? null;
|
if (!node) return folders[0] ?? null;
|
||||||
const normNode = normPath(node);
|
const normNode = normPath(node);
|
||||||
@@ -3163,6 +3181,24 @@ function Catalogue({ admin }: { admin: boolean }) {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{/* The frame's own look, taken back: what the studio filed for it and the
|
||||||
|
tile it drew from it, so the shelf shows the picture the frame came in
|
||||||
|
with — the tile as it was first drawn, and the preview the file's own
|
||||||
|
develop gives. */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="lib-menu-item"
|
||||||
|
data-key="lib-photo-reset"
|
||||||
|
onClick={() => {
|
||||||
|
const photo = photoMenu.photo;
|
||||||
|
setPhotoMenu(null);
|
||||||
|
void resetSetting(photo);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="lib-menu-icon" aria-hidden="true">↺</span>
|
||||||
|
{t('lib.reset')}
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="lib-menu-item"
|
className="lib-menu-item"
|
||||||
@@ -3378,6 +3414,16 @@ function cacheObjectUrl(id: string, blob: Blob): string {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A tile the shelf is to draw again from the negative — the studio's grading taken
|
||||||
|
// back — is dropped here, the URL with it: the row is keyed on the tile the store
|
||||||
|
// holds, and a copy kept would answer every read after with the graded one.
|
||||||
|
function dropThumbUrl(id: string): void {
|
||||||
|
const held = globalThumbUrls.get(id);
|
||||||
|
if (!held) return;
|
||||||
|
URL.revokeObjectURL(held.url);
|
||||||
|
globalThumbUrls.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
function useThumbUrls(photos: LibraryPhoto[], also?: LibraryPhoto | null, stamp?: number): Record<string, string> {
|
function useThumbUrls(photos: LibraryPhoto[], also?: LibraryPhoto | null, stamp?: number): Record<string, string> {
|
||||||
const list = useMemo(() => (also && !photos.some((p) => p.id === also.id) ? [...photos, also] : photos), [photos, also]);
|
const list = useMemo(() => (also && !photos.some((p) => p.id === also.id) ? [...photos, also] : photos), [photos, also]);
|
||||||
|
|
||||||
|
|||||||
@@ -2249,6 +2249,14 @@ export function makeThumb(photo: LibraryPhoto): Promise<Blob | null> {
|
|||||||
async function makeOneThumb(photo: LibraryPhoto): Promise<Blob | null> {
|
async function makeOneThumb(photo: LibraryPhoto): Promise<Blob | null> {
|
||||||
const kept = await getPhotoThumb(photo.id);
|
const kept = await getPhotoThumb(photo.id);
|
||||||
if (kept) return kept;
|
if (kept) return kept;
|
||||||
|
return tileFrom(photo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tile the frame's own source gives, with neither keeper of a graded tile
|
||||||
|
// asked. The shelf is not the caller: a frame put back wears the picture it was
|
||||||
|
// first drawn with, and both the store and the backup still hold the look that
|
||||||
|
// was just taken back.
|
||||||
|
async function tileFrom(photo: LibraryPhoto): Promise<Blob | null> {
|
||||||
// A server's frame has no file to open: its tile is the one Immich drew, asked
|
// A server's frame has no file to open: its tile is the one Immich drew, asked
|
||||||
// for here and kept, so the second look comes out of the store and the sync's
|
// for here and kept, so the second look comes out of the store and the sync's
|
||||||
// own download is only paid once.
|
// own download is only paid once.
|
||||||
@@ -2516,6 +2524,34 @@ export async function saveEdit(id: string, state: StudioState): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Put a frame back to the picture it was first drawn with: the recipe the studio
|
||||||
|
// filed goes, and the tile it graded from the negative goes with it — the store's
|
||||||
|
// copy and the backup's, which is the same look. The tile is made here rather than
|
||||||
|
// left to the shelf to make, because the shelf's way in asks the backup first and
|
||||||
|
// the backup is holding the look just taken back. Written without `tell`: the
|
||||||
|
// reader's own click is what redraws this frame, and the shelf has no business
|
||||||
|
// being told about a rewrite it asked for.
|
||||||
|
export async function resetEdit(photo: LibraryPhoto): Promise<Blob | null> {
|
||||||
|
try {
|
||||||
|
const db = await openDb();
|
||||||
|
const hasThumbs = db.objectStoreNames.contains(THUMBS);
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const tx = db.transaction([EDITS, ...(hasThumbs ? [THUMBS] : [])], 'readwrite');
|
||||||
|
tx.objectStore(EDITS).delete(photo.id);
|
||||||
|
if (hasThumbs) tx.objectStore(THUMBS).delete(photo.id);
|
||||||
|
tx.oncomplete = () => resolve();
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Private mode, or nothing filed for this frame: there is no look to take
|
||||||
|
// back, and the tile below is made all the same.
|
||||||
|
}
|
||||||
|
thumbCache.delete(photo.id);
|
||||||
|
const made = await tileFrom(photo);
|
||||||
|
if (made) await writeThumb(photo.id, made, false);
|
||||||
|
return made;
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadEdit(id: string): Promise<LibraryEdit | null> {
|
export async function loadEdit(id: string): Promise<LibraryEdit | null> {
|
||||||
try {
|
try {
|
||||||
// A row filed before the whole look was kept carries the recipe alone: lift
|
// A row filed before the whole look was kept carries the recipe alone: lift
|
||||||
|
|||||||
@@ -276,6 +276,7 @@ export const en: Dict = {
|
|||||||
'lib.mergeHdr': 'HDR Merge (Anti-ghosting)…',
|
'lib.mergeHdr': 'HDR Merge (Anti-ghosting)…',
|
||||||
'lib.copiedPath': 'Copied location to clipboard: {path}',
|
'lib.copiedPath': 'Copied location to clipboard: {path}',
|
||||||
'lib.edited': 'edited',
|
'lib.edited': 'edited',
|
||||||
|
'lib.reset': 'Reset to default',
|
||||||
'lib.size': '{mb} MB',
|
'lib.size': '{mb} MB',
|
||||||
'lib.count': '{n} photos',
|
'lib.count': '{n} photos',
|
||||||
// What the thumbnail view is read through. The score a frame is given is the
|
// What the thumbnail view is read through. The score a frame is given is the
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ export const vi = {
|
|||||||
'lib.mergeHdr': 'Ghép HDR (Chống nhòe)…',
|
'lib.mergeHdr': 'Ghép HDR (Chống nhòe)…',
|
||||||
'lib.copiedPath': 'Đã sao chép đường dẫn: {path}',
|
'lib.copiedPath': 'Đã sao chép đường dẫn: {path}',
|
||||||
'lib.edited': 'đã chỉnh',
|
'lib.edited': 'đã chỉnh',
|
||||||
|
'lib.reset': 'Đặt lại ảnh về mặc định',
|
||||||
'lib.size': '{mb} MB',
|
'lib.size': '{mb} MB',
|
||||||
'lib.count': '{n} ảnh',
|
'lib.count': '{n} ảnh',
|
||||||
// Cách đọc bày ảnh: điểm sao là của người xem, nên chữ nói đúng thế — "từ n
|
// Cách đọc bày ảnh: điểm sao là của người xem, nên chữ nói đúng thế — "từ n
|
||||||
|
|||||||
Reference in New Issue
Block a user