* fix(inspector): give image tags their own labelled section (#449) The inspector drew a file's tags as a bare chip row right under the negative prompt, so they read as part of it. The side panel, the viewer details and History's current file now draw them in labelled sections in the prompt sections' frame: "Image tags · N" for the file's own tags, then "Prompt tags · N" or "Recipe tags · N" for the linked tags. Each count is true: repeats are removed within a source only. Recorded as D154. * feat(inspect): choosing a file opens the viewer with its details (#448) On Inspect a file, choosing or dropping a file now opens the shared viewer on it at once, details showing, instead of previewing it and waiting for "Open file metadata". Details show on this page even when "Collapse viewer details by default" is on. An unreadable file or an AVI/WMV note stays on the page; the heading action reopens any selection. A new selection that starts while the viewer shows the previous file closes it first, so its blob URL is never revoked under an open element. Recorded as D155. * feat(viewer): full-screen toggle beside the close button, and a System default (#450) Every viewer gets a button before the close button that switches between Centered (today's framed dialog) and Full screen (the whole browser window, edge to edge); it names the view it switches to. In full screen the details stay toggleable. System > Appearance gains a Viewer surface with "Open viewer in: Centered / Full screen" and the moved "Collapse viewer details by default"; the choice is a browser display preference (viewerOpenMode). The button changes only the current view and hides at 700px and below, where the viewer is already full-bleed. Recorded as D156. * fix(inspect): open a video's viewer only once its preview settles (#448) Replacing a video while both the inline preview and the auto-opened viewer's video were still starting deadlocked WebKit (D62). The viewer now opens for a video once its inline preview reports metadata or an error (or after 15 s). * test(viewer): pin the D97 view-switch spec to the desktop width (#450) * test(inspect): cover the AVI note and an open dialog holding the auto-open back (#448) Frontend coverage for inspect.mjs fell below its floor (branches 91.04 < 92, functions 92.85 < 93): no spec reached the AVI toast or the open-dialog guard, and the video wait had a page-end callback no spec reached. The wait now ends through its own callback at page end, and two specs cover the AVI note and a selection landing under another dialog.
167 lines
9.9 KiB
JavaScript
167 lines
9.9 KiB
JavaScript
/**
|
||
* System › Appearance (#249 System S3 Task 8): the concept's `renderSystem` "appearance" body (app.js 676–678), one
|
||
* `section.surface` "Browsing & display" of `.setting-row`s whose values are the drafts of the one Save row.
|
||
*
|
||
* - The browser rows are the display defaults this browser keeps in the shared `gallerySettings` object
|
||
* (preferences.mjs, Q3): Default layout and Thumbnail size (`workspaceLayout`, `workspaceDensity`), the metadata,
|
||
* quality (Optimized→`medium`, Original→`high`, Fast→`low`), video and path rows. Images, the inspector and the
|
||
* viewer read them on their next visit; a toolbar change still applies to the current view only.
|
||
* - "Open from ComfyUI" (New tab→`newtab`, Popup window→`popup`) and "Node search auto-hide (seconds)" (a whole number
|
||
* 0–300) are the server's `webui_display_mode` and `result_timeout`, saved with the settings revision (#220).
|
||
* - "Continuous browsing" is the concept's fixed row: there is no paging or column control (README ruling).
|
||
* - D97: added rows for the legacy gallery settings the concept leaves out: "Show image details on cards"
|
||
* (`showImageInfo`), "Collapse viewer details by default" (`sidebarCollapsedByDefault`) and the operation "Reset
|
||
* display defaults", which drafts the gallery's defaults for these browser rows and touches no other key.
|
||
*
|
||
* - #450 (D156, amends D97): a "Viewer" surface after it holds "Open viewer in" (Centered→`centered`, Full
|
||
* screen→`fullscreen`, `viewerOpenMode`) and the moved "Collapse viewer details by default"; "Reset display defaults"
|
||
* covers both surfaces' browser rows.
|
||
* - #317: a further surface, "Thumbnails", holds the server's thumbnail format, quality and size limit
|
||
* (system-thumbnails.mjs).
|
||
*
|
||
* Controls follow the store: a value read later (the host settings, another tab's change) repaints them. A host row
|
||
* whose value could not be read stays disabled and says so ("Server settings could not be read.").
|
||
*/
|
||
import { settingRow, operationRow } from './system-controls.mjs';
|
||
import { mountThumbnailSettings } from './system-thumbnails.mjs';
|
||
import { preferenceDefaults } from './preferences.mjs';
|
||
|
||
export const RESULT_TIMEOUT_MAX = 300;
|
||
/** A host row's line while the server settings could not be read (its control stays disabled). */
|
||
export const HOST_UNAVAILABLE = 'Server settings could not be read.';
|
||
const BOOLEAN = Object.freeze({ type: 'toggle' });
|
||
|
||
/**
|
||
* The concept's rows in its order, then the D97 rows: `[id, label, description, domain, key, control]`. A select's
|
||
* options are `[stored value, concept label]`.
|
||
*/
|
||
export const APPEARANCE_ROWS = Object.freeze([
|
||
['default-view', 'Default layout', 'Choose your starting image layout.', 'browser', 'workspaceLayout',
|
||
{ type: 'select', options: [['grid', 'Grid'], ['list', 'List'], ['masonry', 'Masonry']] }],
|
||
['density', 'Thumbnail size', 'Readable previews by default; no fixed column-count setting.', 'browser', 'workspaceDensity',
|
||
{ type: 'select', options: [['comfortable', 'Comfortable'], ['large', 'Large'], ['compact', 'Compact']] }],
|
||
'continuous',
|
||
['metadata-auto', 'Load metadata on selection', 'Use the same inspector in the gallery and image viewer.', 'browser', 'autoLoadMetadata', BOOLEAN],
|
||
['metadata-cache', 'Cache metadata in this browser', 'Reuse fetched metadata until it changes or you clear this cache.', 'browser', 'cacheMetadata', BOOLEAN],
|
||
['image-quality', 'Image loading quality', 'Preserve original / optimized / fast image-loading preferences.', 'browser', 'imageQuality',
|
||
{ type: 'select', options: [['medium', 'Optimized'], ['high', 'Original'], ['low', 'Fast']] }],
|
||
['video-auto', 'Autoplay videos', 'Videos stay within the same gallery and use native playback controls.', 'browser', 'videoAutoplay', BOOLEAN],
|
||
['video-muted', 'Mute videos by default', 'Keep playback quiet while browsing.', 'browser', 'videoMute', BOOLEAN],
|
||
['video-loop', 'Loop videos', 'Repeat a selected clip until you navigate away.', 'browser', 'videoLoop', BOOLEAN],
|
||
['paths', 'Show file paths', 'Paths appear under File & provenance, not across every card.', 'browser', 'showFilePaths', BOOLEAN],
|
||
['open-mode', 'Open from ComfyUI', 'Keep the node pack’s popup / new-tab preference.', 'host', 'webui_display_mode',
|
||
{ type: 'select', options: [['newtab', 'New tab'], ['popup', 'Popup window']] }],
|
||
['timeout', 'Node search auto-hide (seconds)', 'Node search behavior only. Use 0 to keep results visible.', 'host', 'result_timeout',
|
||
{ type: 'number' }],
|
||
// D97: the legacy gallery's card setting (its viewer setting moved to VIEWER_ROWS, D156).
|
||
['card-details', 'Show image details on cards', 'Show each card’s title and file details under the preview.', 'browser', 'showImageInfo', BOOLEAN],
|
||
]);
|
||
|
||
/** #450 (D156): the "Viewer" surface's rows, in the same shape. */
|
||
export const VIEWER_ROWS = Object.freeze([
|
||
['viewer-mode', 'Open viewer in', 'Full screen fills the browser window; Centered is the framed viewer.', 'browser', 'viewerOpenMode',
|
||
{ type: 'select', options: [['centered', 'Centered'], ['fullscreen', 'Full screen']] }],
|
||
['viewer-details', 'Collapse viewer details by default', 'Open the image viewer with its details panel hidden.', 'browser',
|
||
'sidebarCollapsedByDefault', BOOLEAN],
|
||
]);
|
||
|
||
/** The browser keys "Reset display defaults" restores (D97), on both surfaces. */
|
||
export const DISPLAY_KEYS = Object.freeze([...APPEARANCE_ROWS, ...VIEWER_ROWS].filter(row => Array.isArray(row) && row[3] === 'browser').map(row => row[4]));
|
||
|
||
/** A whole number of seconds from 0 to 300 (the server's `validate_result_timeout`), else null. */
|
||
export function resultTimeout(text) {
|
||
const value = String(text).trim();
|
||
if (!/^\d+$/.test(value)) return null;
|
||
const seconds = Number(value);
|
||
return seconds <= RESULT_TIMEOUT_MAX ? seconds : null;
|
||
}
|
||
|
||
function el(tag, text, className) {
|
||
const node = document.createElement(tag);
|
||
if (text !== undefined) node.textContent = text;
|
||
if (className) node.className = className;
|
||
return node;
|
||
}
|
||
|
||
/** The concept's fixed "Continuous browsing" row: `div > h3 + p` and the "Infinite scroll" chip. */
|
||
function continuousRow() {
|
||
const row = el('div', undefined, 'setting-row');
|
||
const text = el('div');
|
||
text.append(el('h3', 'Continuous browsing'),
|
||
el('p', 'Images load automatically as you scroll in Grid, List and Masonry. Batch sizing is handled internally.'));
|
||
row.append(text, el('span', 'Infinite scroll', 'chip blue'));
|
||
return row;
|
||
}
|
||
|
||
/** The row's control (`data-setting`), wherever the Toolkit switch nests it. */
|
||
function controlOf(node, id) {
|
||
if (node.dataset?.setting === id) return node;
|
||
for (const child of node.children ?? []) {
|
||
const found = controlOf(child, id);
|
||
if (found) return found;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** Show a stored value in its control; a value not read yet leaves the control disabled. */
|
||
function paint(input, control, value) {
|
||
input.disabled = value === undefined;
|
||
if (value === undefined) return;
|
||
if (control.type === 'toggle') input.checked = value === true;
|
||
else if (document.activeElement !== input || control.type === 'select') input.value = String(value);
|
||
}
|
||
|
||
/**
|
||
* Mount Appearance in `container` (System's `.settings-layout`); returns the section's nodes: "Browsing & display",
|
||
* "Viewer" (D156), then the server's thumbnail settings (#317, system-thumbnails.mjs).
|
||
*/
|
||
export function mountAppearance(container, { scope, drafts, api, activity }) {
|
||
const surface = el('section', undefined, 'surface');
|
||
surface.append(el('h2', 'Browsing & display'), el('p', 'One set of defaults; toolbar changes apply to the current view.'));
|
||
const controls = [];
|
||
function addRows(target, rows) {
|
||
for (const row of rows) {
|
||
if (row === 'continuous') { target.append(continuousRow()); continue; }
|
||
const [id, label, description, domain, key, control] = row;
|
||
const onChange = value => {
|
||
if (key === 'result_timeout') {
|
||
const seconds = resultTimeout(input.value);
|
||
if (seconds === null) { drafts.reject(domain, key); return; }
|
||
value = seconds;
|
||
}
|
||
drafts.set(domain, key, value);
|
||
};
|
||
const node = settingRow({ id, label, description, control, scope, onChange });
|
||
target.append(node);
|
||
const input = controlOf(node, id);
|
||
input.dataset.key = key;
|
||
controls.push({ input, control, domain, key, text: node.children[0], note: null });
|
||
}
|
||
}
|
||
addRows(surface, APPEARANCE_ROWS);
|
||
// D156: the viewer's defaults in their own surface.
|
||
const viewer = el('section', undefined, 'surface');
|
||
viewer.append(el('h2', 'Viewer'), el('p', 'How the image viewer opens; the button beside ✕ switches the current view.'));
|
||
addRows(viewer, VIEWER_ROWS);
|
||
surface.append(operationRow({ title: 'Reset display defaults',
|
||
description: 'Restore this browser’s display defaults, then save. Other settings are kept.', label: 'Reset', scope,
|
||
onClick: () => {
|
||
const defaults = preferenceDefaults();
|
||
for (const key of DISPLAY_KEYS) drafts.set('browser', key, defaults[key]);
|
||
} }));
|
||
const repaint = () => {
|
||
const unavailable = drafts.hostState?.() === 'failed';
|
||
for (const row of controls) {
|
||
const value = drafts.get(row.domain, row.key);
|
||
paint(row.input, row.control, value);
|
||
if (row.domain !== 'host') continue;
|
||
if (unavailable && value === undefined) {
|
||
if (!row.note) { row.note = el('p', HOST_UNAVAILABLE, 'small'); row.text.append(row.note); }
|
||
} else if (row.note) { row.note.remove(); row.note = null; }
|
||
}
|
||
};
|
||
scope.cleanup(drafts.subscribe(repaint));
|
||
repaint();
|
||
return [surface, viewer, mountThumbnailSettings(container, { scope, drafts, api, activity })];
|
||
}
|