Files
ComfyAssets-ComfyUI_PromptM…/web/js/workspace/system-appearance.mjs
T
Vito Sansevero 4d554fbf40 fix(system): flag changes made while System was closed and keep embedded galleries fixed
Final review fixes for System S3 (Appearance):

- I1: the on-demand metadata deviation is D124 (TESTING.md, inspector
  markers); TESTING.md names the spec that pins it, since no concept
  comparison renders that state.
- I2: System checks the display defaults for conflicts on mount, not only
  on storage events, and a browser write refuses a draft another page
  overtook, so a draft is never saved over another tab's value without
  the conflict status first. New two-tab spec.
- I3: the layout and size defaults start the Images page only; embedded
  galleries (a prompt history's Shared gallery and dialogs) keep
  Grid/Comfortable.
- M1: host rows whose /settings read failed say "Server settings could
  not be read."
- M2: a rejected timeout shows the "Some changes were not saved" status.
- M3: the browser loads on mount and on storage events never reject
  unhandled.
- M4: the next accepted write stores the tab-only changes and clears the
  overlay.
- M5: cache off never re-reads a source-changed selection, and refresh()
  never loads a deferred one.
- M6: the System fixture refuses a config.json outside its temp root and
  restores it with os.replace.
- M7: the inspection read counts wait on a request barrier, not frames.
- M8 recorded as an open saved-views question in TESTING.md.
2026-10-23 06:55:02 -07:00

144 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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.
*
* 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 { 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. */
export function mountAppearance(container, { scope, drafts }) {
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];
}