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.
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 })];
|
||
}
|