The concept's Metadata review page at #/review: the notice with the exact pending count, the queue's media cards (shared with Images through image-card.mjs), and the Resolve the association panel with the sampler context, the chosen path's prompt blocks, Leave for later and Apply association. Apply takes a fresh capture; refusals keep the selection and explain inline; an unanswered decision is resent once. SAVED VIEWS lists Needs review after Favorites with its count (retires D50), and an ended scan that recorded review items offers Review metadata in Activity (D87). Queue items carry the card's media url.
80 lines
3.9 KiB
JavaScript
80 lines
3.9 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';
|
||
|
||
function el(tag, text, className) {
|
||
const node = document.createElement(tag);
|
||
if (text !== undefined) node.textContent = text;
|
||
if (className) node.className = className;
|
||
return node;
|
||
}
|
||
|
||
/** The card's media: a video's first frame when it has no thumbnail, else the thumbnail or the image. */
|
||
export function cardMedia(image, lazy = false) {
|
||
const { resolveUrl } = window.ApiClient;
|
||
const video = image.is_video && !image.thumbnail_url;
|
||
const node = el(video ? 'video' : 'img');
|
||
node.src = resolveUrl(image.thumbnail_url || image.url);
|
||
if (video) { node.muted = true; node.playsInline = true; node.preload = 'metadata'; }
|
||
else {
|
||
// 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`), and `media: false` leaves the open button empty for a file that
|
||
* cannot be served.
|
||
*/
|
||
export function createImageCard(image, { checked = null, favoriteBusy = false, media = true } = {}) {
|
||
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') {
|
||
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)));
|
||
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, createTagButton(image));
|
||
info.append(text, actions);
|
||
card.append(art, info);
|
||
return card;
|
||
}
|