* fix(images): background jobs skip files hidden by the removal mark; walkers skip trash folders inside output roots (#501) - Thumbnail jobs and the legacy scan_output_dir listing leave library-removed files out (one mark check, mark_hides). - Missing-file links also forgets removal marks that hide nothing any more (file gone from a folder that exists, or changed), only under a readable, non-empty source, re-checked at apply. - The sidebar Images, Favorites and Needs review counts and the collection counts are read again when an image_delete job ends. - utils/media_folders.py: one skip for thumbnails and trash folders (.Trash, .Trash-<uid>, .Trashes, $RECYCLE.BIN) used by every walker, the image monitor and the delete review. * feat(images): Show removed images and Return to library (#498) - GET /images/output?mode=workspace&removed=1 lists only files a removal mark hides; every workspace page carries removed_total. - POST /images/removed/return {items | filter} starts a scan job, "Return images to the library": the marks that still hide their file go in one transaction, then exactly those files are scanned, so their prompt links come back from their metadata. Tags, categories, collections and favorites do not come back. - The marks generation moves again after the commit, so a reader that cached the marks mid-transaction reloads them. - Images: Show removed images (N) heading toggle (D162), Removed chips, a bar with only Return to library and Clear, Return to library in the side panel; the view is never saved or restored. * test(images): record the return route in the legacy contract snapshot; removed_total in the workspace keys (#498) The scan runner passes returned only when set, so a stand-in events function without that keyword still runs. * test(browser): wait for the answers two specs raced in the merge queue (#501) - System diagnostics "level filter": wait for each level's own log answer, and poll the shown levels; the CRITICAL line is in the previous level's answer too, so it proved nothing (firefox). - Library editor "restored storage after a confirmed save": wait for the PATCH answer, which waits for a whole restore in the test's route, instead of the 5 s expect default (webkit). * fix(monitor): skip trash folders only below the watched folder (#501 review) A source named .Trash-1000 (or another trash name) was ignored by the image monitor while every walker still listed it; the monitor now tests the folders below the longest watched folder holding the new file. * fix(images): no default removedView function in the selection bar (#498) The default arrow was never called, so images-selection.mjs fell under its function coverage floor (97.29 < 98). * fix(images): a category or tags chosen in Show removed images ends the view (#498 review) The toolbar's Category and Refine's Tags stayed usable in the removed view, and the listing then refused the combination with a page error. Choosing one now leaves the view, as a removed file has neither.
100 lines
5.2 KiB
JavaScript
100 lines
5.2 KiB
JavaScript
/**
|
||
* The concept media card (docs/concepts/workspace/app.js `mediaCard` a396–399), shared by Images, every
|
||
* Library-embedded gallery and the Needs review queue: `article.media-card` > `.media-art` (the `.image-open` button
|
||
* holding the media, then `.image-overlays`) and `.media-info` (title, "W × H · EXT · Seed N", then
|
||
* `.image-card-actions` with Info and Tag). #249 D7–D11, D19.
|
||
*/
|
||
import { createButton } from '../shared/toolkit/core.js';
|
||
import { cardTitle, cardSubtitle, imageKey, mediaRatio } from './images-state.mjs';
|
||
import { createWorkspaceIcon } from './icons.mjs';
|
||
import { createTagButton } from './image-tagging.mjs';
|
||
import { paintStar } from './images-favorites.mjs';
|
||
import { fallBackToOriginal, replaceOnError, showsThumbnail, thumbnailSource } from './thumbnail-support.mjs';
|
||
|
||
function el(tag, text, className) {
|
||
const node = document.createElement(tag);
|
||
if (text !== undefined) node.textContent = text;
|
||
if (className) node.className = className;
|
||
return node;
|
||
}
|
||
|
||
/** A video's own element (its first frame), for a video without a usable preview. */
|
||
function videoMedia(image) {
|
||
const node = el('video');
|
||
node.src = window.ApiClient.resolveUrl(image.url);
|
||
node.muted = true; node.playsInline = true; node.preload = 'metadata';
|
||
return node;
|
||
}
|
||
|
||
/**
|
||
* The card's media: the thumbnail (a video's is its preview frame), else the image, else a video's own element.
|
||
* #249: a video whose preview cannot be loaded swaps back to its own element. #317: a JPEG XL thumbnail this browser
|
||
* cannot show loads the original, and an image's thumbnail that fails to load falls back to the original.
|
||
*/
|
||
export function cardMedia(image, lazy = false) {
|
||
const { resolveUrl } = window.ApiClient;
|
||
if (!showsThumbnail(image)) return videoMedia(image);
|
||
const node = el('img');
|
||
node.src = resolveUrl(thumbnailSource(image));
|
||
// replaceWith does nothing for a card no longer in the page.
|
||
if (image.is_video) replaceOnError(node, () => videoMedia(image));
|
||
else fallBackToOriginal(node, resolveUrl(image.url));
|
||
// The concept card image: alt is the card title, with the listing's intrinsic size when known.
|
||
node.alt = cardTitle(image); node.decoding = 'async'; if (lazy) node.loading = 'lazy';
|
||
if ([image.width, image.height].every(value => Number.isFinite(value) && value > 0)) {
|
||
node.width = image.width; node.height = image.height;
|
||
}
|
||
return node;
|
||
}
|
||
|
||
/**
|
||
* One card for `image`. `checked` (a boolean) adds the D8 selection checkbox, `favoriteBusy` paints a pending star
|
||
* (D7, shown wherever the image reports `favorite`), `media: false` leaves the open button empty for a file that
|
||
* cannot be served, and `showInfo: false` hides the title and file line (D97). A `linked_only` image (LoRA media in
|
||
* LoraManager's folders, #355) has no Tag action.
|
||
*/
|
||
export function createImageCard(image, { checked = null, favoriteBusy = false, media = true, showInfo = true, removed = false } = {}) {
|
||
const card = el('article', undefined, 'media-card');
|
||
card.dataset.imageKey = imageKey(image); card.setAttribute('role', 'listitem');
|
||
card.style.setProperty('--media-ratio', mediaRatio(image));
|
||
const art = el('div', undefined, 'media-art');
|
||
const open = createButton('', 'image-open');
|
||
open.setAttribute('aria-label', `Open ${image.filename}`);
|
||
if (media) open.append(cardMedia(image, true));
|
||
// D8: the selection checkbox (Images only: Library-embedded media has no selection bar), then D7: the star,
|
||
// wherever the listing reports favorites (Images and Library-embedded media).
|
||
const overlays = el('div', undefined, 'image-overlays');
|
||
if (typeof checked === 'boolean') {
|
||
const label = el('label'), box = el('input');
|
||
box.type = 'checkbox'; box.checked = checked;
|
||
box.setAttribute('aria-label', `Select ${image.filename}`);
|
||
label.append(box);
|
||
overlays.append(label);
|
||
}
|
||
if (typeof image.favorite === 'boolean' && !removed) {
|
||
const star = createButton('', 'star-button');
|
||
star.setAttribute('aria-label', `Favorite ${image.filename}`);
|
||
paintStar(star, image.favorite, favoriteBusy);
|
||
overlays.append(star);
|
||
}
|
||
art.append(open, overlays);
|
||
const info = el('div', undefined, 'media-info');
|
||
const text = el('div');
|
||
const title = el('h3', cardTitle(image)); if (image.path) title.title = image.path;
|
||
text.append(title, el('p', cardSubtitle(image)));
|
||
// #498 (D162): a card in Show removed images carries the concept's `.chip`.
|
||
if (removed) text.append(el('span', 'Removed', 'chip'));
|
||
// D97: `showInfo` false (System › Appearance) hides the title and file line; Info and Tag stay.
|
||
if (!showInfo) text.hidden = true;
|
||
const actions = el('div', undefined, 'image-card-actions');
|
||
const inspectButton = createButton('', 'icon-btn');
|
||
inspectButton.append(createWorkspaceIcon('info'));
|
||
inspectButton.setAttribute('aria-label', `Inspect ${image.filename}`);
|
||
actions.append(inspectButton);
|
||
// #355: LoRA-linked media lives in LoraManager's folders; tags are kept for output files only.
|
||
if (image.linked_only !== true && !removed) actions.append(createTagButton(image));
|
||
info.append(text, actions);
|
||
card.append(art, info);
|
||
return card;
|
||
}
|