feat: the camera's own colour, a reading that keeps its place, and cloud links in Settings
- RAW opens at the colour the camera wrote into it: the develop carries the difference against the file's own preview (previewResidual) instead of the per-channel cubic fit, dE00 6.04 -> 0.54; previewMatch.ts keeps only the grid. - Immich reading: keyset position kept across visits, one reading per node, a page in flight retried, timeline buckets for the years on offer. - The wall draws a server frame at the address the proxy serves it (no blob minted for it), and the 80-thumbnail pre-cache on open is gone. - Settings lists the cloud links, flags two that answer at the same address, and forgets one. - Custom frames imported from a file (engine/customFrame.ts, IndexedDB). - Upload an edited frame from Studio to Immich Cloud.
This commit is contained in:
@@ -25,7 +25,7 @@ const PHOTOS = 'photos';
|
||||
const THUMBS = 'thumbs';
|
||||
const EDITS = 'edits';
|
||||
const DIRS = 'dirs';
|
||||
const DB_VERSION = 4;
|
||||
const DB_VERSION = 5;
|
||||
|
||||
// The tile the catalogue keeps is the same tile the grid cell and the strip both
|
||||
// paint, and there is one of them per frame of a roll that reaches six figures.
|
||||
@@ -112,6 +112,17 @@ export interface LibraryFolder {
|
||||
// the name is the store's key — but a label the reader may rename is no place to
|
||||
// keep a query, and the sync asks with this.
|
||||
query?: string;
|
||||
// Where a server's reading stopped, on a node whose frames are an Immich
|
||||
// server's. A node of a hundred thousand frames is not one visit's to read, and
|
||||
// the keyset the reading pages by is a value the page cannot hold across a
|
||||
// reload: without this, every visit read the newest three pages of the node
|
||||
// again and nothing below them was ever read at all. `before` is the instant the
|
||||
// next page starts from, `page` the page within that instant — Immich keeps a
|
||||
// frame's time to the millisecond and a roll imported in one go is hundreds of
|
||||
// frames at the same one, so one instant is several pages — and `done` says the
|
||||
// whole node has been read, which is the only state in which a frame that is no
|
||||
// longer there may be dropped from the catalogue.
|
||||
cursor?: { before: string | null; page?: number; done: boolean };
|
||||
}
|
||||
|
||||
export interface LibraryPhoto {
|
||||
@@ -322,11 +333,14 @@ function openDb(): Promise<IDBDatabase> {
|
||||
// to be made anyway.
|
||||
if (db.objectStoreNames.contains(THUMBS)) db.deleteObjectStore(THUMBS);
|
||||
if (!db.objectStoreNames.contains(FOLDERS)) db.createObjectStore(FOLDERS, { keyPath: 'name' });
|
||||
let photosStore: IDBObjectStore;
|
||||
if (!db.objectStoreNames.contains(PHOTOS)) {
|
||||
const photos = db.createObjectStore(PHOTOS, { keyPath: 'id' });
|
||||
photos.createIndex('folder', 'folder');
|
||||
photos.createIndex('taken', 'taken');
|
||||
photosStore = db.createObjectStore(PHOTOS, { keyPath: 'id' });
|
||||
} else {
|
||||
photosStore = req.transaction!.objectStore(PHOTOS);
|
||||
}
|
||||
if (!photosStore.indexNames.contains('folder')) photosStore.createIndex('folder', 'folder');
|
||||
if (!photosStore.indexNames.contains('taken')) photosStore.createIndex('taken', 'taken');
|
||||
if (!db.objectStoreNames.contains(THUMBS)) {
|
||||
db.createObjectStore(THUMBS, { keyPath: 'id' });
|
||||
}
|
||||
@@ -2047,9 +2061,9 @@ function pump(): void {
|
||||
let cached: LibraryPhoto[] | null = null;
|
||||
|
||||
export function cachedPhotos(): LibraryPhoto[] {
|
||||
if (cached) return cached;
|
||||
if (cached && cached.length > 0) return cached;
|
||||
try {
|
||||
const raw = sessionStorage.getItem('rc_cached_photos');
|
||||
const raw = sessionStorage.getItem('rc_cached_photos') || localStorage.getItem('rc_cached_photos');
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
@@ -2061,6 +2075,26 @@ export function cachedPhotos(): LibraryPhoto[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
export function saveCachedPhotos(photos: LibraryPhoto[]): void {
|
||||
try {
|
||||
const slim = photos.slice(0, 300).map((p) => ({
|
||||
id: p.id,
|
||||
folder: p.folder,
|
||||
name: p.name,
|
||||
taken: p.taken,
|
||||
addedAt: p.addedAt,
|
||||
dir: p.dir,
|
||||
star: p.star,
|
||||
server: p.server,
|
||||
remoteId: p.remoteId,
|
||||
rot: p.rot,
|
||||
}));
|
||||
const json = JSON.stringify(slim);
|
||||
sessionStorage.setItem('rc_cached_photos', json);
|
||||
localStorage.setItem('rc_cached_photos', json);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export function clearLibraryCache(): void {
|
||||
cached = null;
|
||||
dirHandles.clear();
|
||||
@@ -2385,6 +2419,10 @@ export async function makeThumbs(
|
||||
const dir = normPath(rel);
|
||||
const rows = Array.isArray(folder) ? folder : (await readPhotos(normPath(folder.name))) ?? [];
|
||||
const want = rows.filter((p) => {
|
||||
// A server's frame has no tile to mint here: the wall draws the address the
|
||||
// proxy serves it at (see `tile` in Library.tsx), so reading the bytes into
|
||||
// the store is the same picture fetched twice and a copy kept for nothing.
|
||||
if (p.remoteId) return false;
|
||||
const pDir = normPath(p.dir ?? '');
|
||||
if (pDir === dir) return true;
|
||||
// `''` is the picked folder itself, so a row asked of the whole roll takes
|
||||
@@ -2413,7 +2451,10 @@ export async function makeThumbs(
|
||||
return made;
|
||||
}
|
||||
|
||||
export async function readPhotos(folder?: string): Promise<LibraryPhoto[] | null> {
|
||||
export async function readPhotos(
|
||||
folder?: string,
|
||||
onBatch?: (photos: LibraryPhoto[]) => void
|
||||
): Promise<LibraryPhoto[] | null> {
|
||||
try {
|
||||
const db = await openDb();
|
||||
// Every row is filed under `normPath(folder.name)`, and a drive root is the
|
||||
@@ -2431,6 +2472,8 @@ export async function readPhotos(folder?: string): Promise<LibraryPhoto[] | null
|
||||
: IDBKeyRange.bound(last ?? `${root}/`, `${root}/\uffff`, last !== null, false);
|
||||
const rows: LibraryPhoto[] = [];
|
||||
let last: string | null = null;
|
||||
let emittedFirst = false;
|
||||
|
||||
for (;;) {
|
||||
const chunk = await new Promise<LibraryPhoto[]>((resolve, reject) => {
|
||||
const req = db.transaction(PHOTOS, 'readonly').objectStore(PHOTOS).getAll(at(last), READ_CHUNK);
|
||||
@@ -2446,26 +2489,19 @@ export async function readPhotos(folder?: string): Promise<LibraryPhoto[] | null
|
||||
delete (row as { thumb?: unknown }).thumb;
|
||||
}
|
||||
rows.push(...chunk);
|
||||
|
||||
if (onBatch && !emittedFirst && rows.length >= 400) {
|
||||
emittedFirst = true;
|
||||
const initial = [...rows].sort((a, b) => b.taken - a.taken);
|
||||
onBatch(initial);
|
||||
}
|
||||
|
||||
if (chunk.length < READ_CHUNK) break;
|
||||
}
|
||||
rows.sort((a, b) => b.taken - a.taken);
|
||||
if (!folder) {
|
||||
cached = rows;
|
||||
try {
|
||||
const slim = rows.slice(0, 300).map((p) => ({
|
||||
id: p.id,
|
||||
folder: p.folder,
|
||||
name: p.name,
|
||||
taken: p.taken,
|
||||
addedAt: p.addedAt,
|
||||
dir: p.dir,
|
||||
star: p.star,
|
||||
server: p.server,
|
||||
remoteId: p.remoteId,
|
||||
rot: p.rot,
|
||||
}));
|
||||
sessionStorage.setItem('rc_cached_photos', JSON.stringify(slim));
|
||||
} catch {}
|
||||
saveCachedPhotos(rows);
|
||||
}
|
||||
return rows;
|
||||
} catch {
|
||||
@@ -2473,6 +2509,23 @@ export async function readPhotos(folder?: string): Promise<LibraryPhoto[] | null
|
||||
}
|
||||
}
|
||||
|
||||
export async function putPhotosFast(rows: LibraryPhoto[]): Promise<void> {
|
||||
if (!rows.length) return;
|
||||
const db = await openDb();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const tx = db.transaction(PHOTOS, 'readwrite');
|
||||
const store = tx.objectStore(PHOTOS);
|
||||
for (const row of rows) {
|
||||
const r = { ...row };
|
||||
delete (r as { handle?: unknown }).handle;
|
||||
delete (r as { thumb?: unknown }).thumb;
|
||||
store.put(r);
|
||||
}
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function listPhotos(folder?: string): Promise<LibraryPhoto[]> {
|
||||
return (await readPhotos(folder)) ?? [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user