Merge commit '640413af' into fix/512-delete-review-polish

This commit is contained in:
Vito Sansevero
2026-10-29 20:53:52 -07:00
42 changed files with 2551 additions and 144 deletions
+13 -4
View File
@@ -317,6 +317,9 @@ export function createImageTaggingController({api, target, source = 'images', on
// A save in flight: closing hands the capture release to its completion.
let saving = false;
let observedOnce = false;
// #515: the user typed since the last observation started (or before the first one answered), so the answer
// keeps that text.
let typed = false;
function set(patch) {
if (closed) return;
@@ -345,7 +348,8 @@ export function createImageTaggingController({api, target, source = 'images', on
async function open({keepDraft = false} = {}) {
if (closed) return;
const serial = ++openSerial;
const draft = keepDraft ? state.manual : null;
// A reopen after an observation starts from its answer; text typed before any observation is kept (#515).
if (observedOnce) typed = false;
const seenRevision = state.tagRevision;
stopGenerating();
releaseCapture();
@@ -385,15 +389,19 @@ export function createImageTaggingController({api, target, source = 'images', on
const savedModel = !chosen && !modelPicked && read.model ? {model: read.model} : {};
observedOnce = true;
const {width, height, media_type: mediaType, preview_url: previewUrl, generate} = value.capture;
// A kept draft stays as typed unless another writer changed the set meanwhile: then the set leads it (I2).
const changed = draft !== null && value.tag_revision !== seenRevision;
// The draft is read now, so text typed while observing is kept (#515). A kept draft stays as typed unless
// another writer changed the set meanwhile: then the set leads it (I2). Text typed before the user saw any
// saved tags follows them, so a Save never drops tags it never showed.
const draft = keepDraft || typed ? state.manual : null;
const changed = keepDraft && value.tag_revision !== seenRevision;
const unseen = draft !== null && !keepDraft;
const reason = generate.supported ? '' : (generate.reason || '');
set({
phase: 'ready',
image: {filename: value.filename, title: value.title, width, height, mediaType, previewUrl,
dimensions: width && height ? `${width} × ${height}` : null},
savedTags: value.tags.slice(), tagRevision: value.tag_revision,
manual: draft === null ? value.tags.join(', ') : changed ? mergeCurrent(value.tags, draft) : draft,
manual: draft === null ? value.tags.join(', ') : changed || unseen ? mergeCurrent(value.tags, draft) : draft,
draftRevision: state.draftRevision + 1,
...savedModel, options: {...defaults, ...chosen?.options}, optionsRevision: state.optionsRevision + 1,
generateSupported: generate.supported, generateReason: generate.reason,
@@ -402,6 +410,7 @@ export function createImageTaggingController({api, target, source = 'images', on
}
function setManual(text) {
typed = true;
set({manual: typeof text === 'string' ? text : ''});
}
+1 -1
View File
@@ -32,7 +32,7 @@ export const REVIEW_FAILED = 'The review failed. Nothing was changed.';
export const SOURCE_CHANGED = 'The sources changed. Refresh the images and try again.';
export const STORAGE_CHANGED = 'Prompt storage was restored. Reopen Delete.';
export const USE_LIBRARY_ONLY = 'To remove these files from the library anyway, choose “Remove from the library only”.';
export const NO_TRASH_HERE = 'Moving to the trash is not available on this system yet.';
export const NO_TRASH_HERE = 'Moving to the trash is not available for these files; each one lists why.';
const SENTENCES = Object.freeze({
libraryBackup: 'Removed images stay hidden unless the file is modified or replaced. Their tags, category, collections '
+ 'and favorite can only be recovered by restoring the safety backup, which also undoes every later change.',
+13 -8
View File
@@ -1,7 +1,8 @@
import '../api-client.js';
import { createButton, createScope } from '../shared/toolkit/core.js';
import { routes, mountRouter, summarizeStats, routeTitle, loadNavCounts, navigateTo, COLLECTION_ROUTE,
LIBRARY_WRITE_COUNTS, NAV_COUNT_ROUTES, FAVORITE_WRITE_COUNTS, FAVORITE_COUNT_DELAY_MS, REVIEW_WRITE_COUNTS } from './state.mjs';
LIBRARY_WRITE_COUNTS, NAV_COUNT_ROUTES, FAVORITE_WRITE_COUNTS, FAVORITE_COUNT_DELAY_MS, REVIEW_WRITE_COUNTS,
JOB_END_COUNTS } from './state.mjs';
import { mountWorkspaceTheme } from './theme.mjs';
import { forgetImagesVisit, mountImages, onFavoriteChange } from './images.mjs';
import { forgetLibraryVisit, mountLibrary, mountNewPromptAction, onLibraryWrite } from './library.mjs';
@@ -196,16 +197,20 @@ function mountWorkspace() {
scope.cleanup(() => window.removeEventListener(type, revive, true));
}
scope.listen(document, 'visibilitychange', () => { if (document.visibilityState === 'visible') revive(); });
// A metadata scan that ends may have added items to Needs review: re-read its count once per ended scan.
const runningScans = new Set();
// A job that ends re-reads the counts it can change, once per ended job: a metadata scan may have added items to
// Needs review; deleting images (#480) hides or trashes them, with their favorites, review items and collection
// memberships (#501).
const runningJobs = new Map();
scope.cleanup(activity.subscribe(() => {
let ended = false;
const ended = new Set();
for (const job of activity.jobs()) {
if (job.kind !== 'scan') continue;
if (job.state === 'running') runningScans.add(job.id);
else if (runningScans.delete(job.id)) ended = true;
if (!JOB_END_COUNTS[job.kind]) continue;
if (job.state === 'running') runningJobs.set(job.id, job.kind);
else if (runningJobs.delete(job.id)) ended.add(job.kind);
}
if (ended) refreshCounts(REVIEW_WRITE_COUNTS);
if (!ended.size) return;
refreshCounts([...new Set([...ended].flatMap(kind => JOB_END_COUNTS[kind]))]);
if (ended.has('image_delete') && !leaving) collections.load();
}));
refreshCounts(NAV_COUNT_ROUTES);
// S9: a finished restore replaced the library. The cached visits are dropped, the counts and saved views read
+5
View File
@@ -129,6 +129,11 @@ export const LIBRARY_WRITE_COUNTS = Object.freeze(['library', 'tags']);
export const FAVORITE_WRITE_COUNTS = Object.freeze(['favorites']);
/** The count a review decision or a finished metadata scan can change. */
export const REVIEW_WRITE_COUNTS = Object.freeze(['review']);
/** Job kind -> the counts re-read once when such a job ends (a scan feeds Needs review; #501: a delete hides images). */
export const JOB_END_COUNTS = Object.freeze({
scan: REVIEW_WRITE_COUNTS,
image_delete: Object.freeze(['images', 'favorites', 'review']),
});
// Trailing delay that coalesces quick star toggles into one Favorites count read (#249 M1).
export const FAVORITE_COUNT_DELAY_MS = 300;