System > Appearance gains a Thumbnails surface: format (WebP, JPEG XL, JPEG), quality and size limit, saved to the server with the settings revision. JPEG XL is offered only when the server has an encoder, else the option is disabled with the server's reason (D130); a note says when this browser cannot show JPEG XL. Saving a new format converts nothing: the row then says previews keep their old format. Thumbnail maintenance gains the Convert outdated scope and names the server's settings instead of its own quality and size fields (D131). Overview's thumbnail row reads N missing, M outdated, K failed and offers Convert outdated while previews are outdated. The workspace decodes a tiny JPEG XL once: where it cannot, cards, the Library and the inspector cover load the original for a .jxl preview, and any preview that fails to load falls back to the original. The viewer and downloads keep using the original (regression test). README explains enabling JPEG XL; TESTING.md records D130 and D131.
151 lines
8.7 KiB
JavaScript
151 lines
8.7 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.
|
||
*
|
||
* - #317: a second 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 and viewer settings.
|
||
['card-details', 'Show image details on cards', 'Show each card’s title and file details under the preview.', 'browser', 'showImageInfo', BOOLEAN],
|
||
['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). */
|
||
export const DISPLAY_KEYS = Object.freeze(APPEARANCE_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",
|
||
* 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 = [];
|
||
for (const row of APPEARANCE_ROWS) {
|
||
if (row === 'continuous') { surface.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 });
|
||
surface.append(node);
|
||
const input = controlOf(node, id);
|
||
input.dataset.key = key;
|
||
controls.push({ input, control, domain, key, text: node.children[0], note: null });
|
||
}
|
||
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, mountThumbnailSettings(container, { scope, drafts, api, activity })];
|
||
}
|