diff --git a/docker/frontend/index.html b/docker/frontend/index.html
index ce45688..a0649c1 100644
--- a/docker/frontend/index.html
+++ b/docker/frontend/index.html
@@ -13,6 +13,10 @@
without Safari's chrome, 'black-translucent' gives it the app's black bars,
and 'title' is the name under the icon. -->
+
+
diff --git a/docker/frontend/src/Library.tsx b/docker/frontend/src/Library.tsx
index fe8cca8..d376114 100644
--- a/docker/frontend/src/Library.tsx
+++ b/docker/frontend/src/Library.tsx
@@ -153,10 +153,6 @@ export function Library() {
// holding has moved on.
const live = scanSession();
const [, bump] = useReducer((n: number) => n + 1, 0);
- // When the catalogue was last read back while a scan runs: the refresh reads the
- // whole catalogue, so it keeps to one every few hundred milliseconds rather than
- // one per frame.
- const readAt = useRef(0);
const [note, setNote] = useState(null);
// The folder menu: what a right click opens, and where. No folder means the
// right click landed on the empty part of the column, which is the way in for
@@ -186,6 +182,13 @@ export function Library() {
// one that says the scan is through — is recognisable as an end rather than
// as a screen with nothing to do.
let was: string | null = null;
+ // How much of the roll the catalogue was last read back at, and the shape the
+ // column was last drawn at: reading the whole catalogue back costs every
+ // thumbnail in it, so it waits for a batch to have landed rather than for a
+ // frame to have been read, and the column is redrawn only when a pass has
+ // walked into something new.
+ let wrote = -1;
+ let drew = '';
const watching = () => {
bump();
const session = scanSession();
@@ -202,9 +205,17 @@ export function Library() {
was = folder;
if (!session) return;
const { progress } = session;
- setDirs(progress.dirs);
- if (progress.done === 0 || Date.now() - readAt.current > 700) {
- readAt.current = Date.now();
+ const shape = progress.dirs.length ? `${progress.dirs.length}:${progress.dirs[progress.dirs.length - 1].id}` : '';
+ if (shape !== drew) {
+ drew = shape;
+ setDirs(progress.dirs);
+ }
+ // A window beside the reading is handed the progress of another build, which
+ // may have no such count: the frames it has stored are then read back on a
+ // frame, as they were before there was one.
+ const written = progress.written ?? progress.done;
+ if (written !== wrote) {
+ wrote = written;
void reload();
}
};
@@ -1140,13 +1151,38 @@ export function Library() {
// the panels paint.
function useThumbUrls(photos: LibraryPhoto[]): Record {
const [urls, setUrls] = useState>({});
+ // The URL a row was given, kept for as long as the row is on screen. A scan lands
+ // in batches, and each batch hands the screen the whole list again — so a URL
+ // made afresh per list is a picture the browser decodes afresh per batch:
+ // thousands of bitmaps over a roll, for tiles that have not moved. A row that
+ // leaves the list takes its URL with it.
+ const made = useRef(new Map());
useEffect(() => {
const next: Record = {};
- for (const photo of photos) if (photo.thumb) next[photo.id] = URL.createObjectURL(photo.thumb);
+ for (const photo of photos) {
+ if (!photo.thumb) continue;
+ const had = made.current.get(photo.id);
+ if (had) next[photo.id] = had;
+ else {
+ const url = URL.createObjectURL(photo.thumb);
+ made.current.set(photo.id, url);
+ next[photo.id] = url;
+ }
+ }
+ for (const [id, url] of made.current) {
+ if (id in next) continue;
+ URL.revokeObjectURL(url);
+ made.current.delete(id);
+ }
setUrls(next);
- return () => {
- for (const url of Object.values(next)) URL.revokeObjectURL(url);
- };
}, [photos]);
+ // The screen going away takes every URL with it.
+ useEffect(
+ () => () => {
+ for (const url of made.current.values()) URL.revokeObjectURL(url);
+ made.current.clear();
+ },
+ []
+ );
return urls;
}
diff --git a/docker/frontend/src/engine/library.ts b/docker/frontend/src/engine/library.ts
index bc12409..74b3276 100644
--- a/docker/frontend/src/engine/library.ts
+++ b/docker/frontend/src/engine/library.ts
@@ -320,6 +320,12 @@ export interface ScanProgress {
total: number;
done: number;
added: number;
+ // How many frames have reached the catalogue. `done` counts the frames the
+ // reading has got to, which is nearly all of them long before they are stored,
+ // so a screen that read the catalogue back on `done` would read it again every
+ // frame — and reading it back means every thumbnail in it again. This moves a
+ // batch at a time, which is exactly when there is something new to read.
+ written: number;
// How many frames the scan has read out of each row of the tree, keyed the way
// the tree spells a row (`folder` for the picked folder, `folder/sub/dir` for
// the rest) and counted the way the rows count: a frame sits on its own row and
@@ -478,10 +484,13 @@ export async function scanFolder(
total: 0,
done: 0,
added: 0,
+ written: 0,
dirs: [],
counts: {},
};
progress.folder = folder.name;
+ // A position written down by an older visit has no count to carry on from.
+ progress.written ??= 0;
const found = new Set(progress.dirs.map((d) => d.id));
const counts: Record = progress.counts;
// Count the frame the moment the scan gets to it, before it knows whether the
@@ -538,6 +547,7 @@ export async function scanFolder(
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
+ progress.written += rows.length;
write();
};
// One frame, end to end: its bytes, its tile, its shutter time, and the row the
@@ -848,7 +858,7 @@ export function startScan(folder: LibraryFolder): Promise {
if (live) return Promise.reject(new Error('a scan is already running'));
const session: ScanSession = {
folder: folder.name,
- progress: { folder: folder.name, total: 0, done: 0, added: 0, dirs: [], counts: {} },
+ progress: { folder: folder.name, total: 0, done: 0, added: 0, written: 0, dirs: [], counts: {} },
stop: false,
jump: null,
};