Extract Modal base class and fix target editor defaults

Add core/modal.js with reusable Modal class that handles open/close,
body locking, backdrop close, dirty checking, error display, and a
static stack for ESC key management. Migrate all 13 modals across 8
feature files to use the base class, eliminating ~200 lines of
duplicated boilerplate. Replace manual ESC handler list in app.js
with Modal.closeTopmost(), fixing 3 modals that were previously
unreachable via ESC. Remove 5 unused initialValues variables from
state.js. Fix target editor to auto-select first device/source and
auto-generate name like the KC editor does.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 17:49:42 +03:00
parent 20d5a42e47
commit ed220a97e7
11 changed files with 341 additions and 329 deletions

View File

@@ -3,14 +3,50 @@
*/
import {
calibrationInitialValues, setCalibrationInitialValues,
calibrationTestState, EDGE_TEST_COLORS,
} from '../core/state.js';
import { API_BASE, getHeaders, handle401Error } from '../core/api.js';
import { t } from '../core/i18n.js';
import { lockBody, unlockBody, showToast, showConfirm } from '../core/ui.js';
import { showToast } from '../core/ui.js';
import { Modal } from '../core/modal.js';
import { closeTutorial, startCalibrationTutorial } from './tutorials.js';
/* ── CalibrationModal subclass ────────────────────────────────── */
class CalibrationModal extends Modal {
constructor() {
super('calibration-modal');
}
snapshotValues() {
return {
start_position: this.$('cal-start-position').value,
layout: this.$('cal-layout').value,
offset: this.$('cal-offset').value,
top: this.$('cal-top-leds').value,
right: this.$('cal-right-leds').value,
bottom: this.$('cal-bottom-leds').value,
left: this.$('cal-left-leds').value,
spans: JSON.stringify(window.edgeSpans),
skip_start: this.$('cal-skip-start').value,
skip_end: this.$('cal-skip-end').value,
border_width: this.$('cal-border-width').value,
};
}
onForceClose() {
closeTutorial();
const deviceId = this.$('calibration-device-id').value;
if (deviceId) clearTestMode(deviceId);
if (window._calibrationResizeObserver) window._calibrationResizeObserver.disconnect();
const error = this.$('calibration-error');
if (error) error.style.display = 'none';
}
}
const calibModal = new CalibrationModal();
/* ── Public API (exported names unchanged) ────────────────────── */
export async function showCalibration(deviceId) {
try {
const [response, displaysResponse] = await Promise.all([
@@ -63,27 +99,12 @@ export async function showCalibration(deviceId) {
left: { start: calibration.span_left_start ?? 0, end: calibration.span_left_end ?? 1 },
};
setCalibrationInitialValues({
start_position: calibration.start_position,
layout: calibration.layout,
offset: String(calibration.offset || 0),
top: String(calibration.leds_top || 0),
right: String(calibration.leds_right || 0),
bottom: String(calibration.leds_bottom || 0),
left: String(calibration.leds_left || 0),
spans: JSON.stringify(window.edgeSpans),
skip_start: String(calibration.skip_leds_start || 0),
skip_end: String(calibration.skip_leds_end || 0),
border_width: String(calibration.border_width || 10),
});
calibrationTestState[device.id] = new Set();
updateCalibrationPreview();
const modal = document.getElementById('calibration-modal');
modal.style.display = 'flex';
lockBody();
calibModal.snapshot();
calibModal.open();
initSpanDrag();
requestAnimationFrame(() => {
@@ -109,40 +130,15 @@ export async function showCalibration(deviceId) {
}
function isCalibrationDirty() {
return (
document.getElementById('cal-start-position').value !== calibrationInitialValues.start_position ||
document.getElementById('cal-layout').value !== calibrationInitialValues.layout ||
document.getElementById('cal-offset').value !== calibrationInitialValues.offset ||
document.getElementById('cal-top-leds').value !== calibrationInitialValues.top ||
document.getElementById('cal-right-leds').value !== calibrationInitialValues.right ||
document.getElementById('cal-bottom-leds').value !== calibrationInitialValues.bottom ||
document.getElementById('cal-left-leds').value !== calibrationInitialValues.left ||
JSON.stringify(window.edgeSpans) !== calibrationInitialValues.spans ||
document.getElementById('cal-skip-start').value !== calibrationInitialValues.skip_start ||
document.getElementById('cal-skip-end').value !== calibrationInitialValues.skip_end ||
document.getElementById('cal-border-width').value !== calibrationInitialValues.border_width
);
return calibModal.isDirty();
}
export function forceCloseCalibrationModal() {
closeTutorial();
const deviceId = document.getElementById('calibration-device-id').value;
if (deviceId) clearTestMode(deviceId);
if (window._calibrationResizeObserver) window._calibrationResizeObserver.disconnect();
const modal = document.getElementById('calibration-modal');
const error = document.getElementById('calibration-error');
modal.style.display = 'none';
error.style.display = 'none';
unlockBody();
setCalibrationInitialValues({});
calibModal.forceClose();
}
export async function closeCalibrationModal() {
if (isCalibrationDirty()) {
const confirmed = await showConfirm(t('modal.discard_changes'));
if (!confirmed) return;
}
forceCloseCalibrationModal();
calibModal.close();
}
export function updateOffsetSkipLock() {
@@ -681,7 +677,7 @@ export async function saveCalibration() {
if (response.status === 401) { handle401Error(); return; }
if (response.ok) {
showToast('Calibration saved', 'success');
forceCloseCalibrationModal();
calibModal.forceClose();
window.loadDevices();
} else {
const errorData = await response.json();

View File

@@ -5,13 +5,15 @@
import {
_discoveryScanRunning, set_discoveryScanRunning,
_discoveryCache, set_discoveryCache,
settingsInitialValues,
} from '../core/state.js';
import { API_BASE, getHeaders, isSerialDevice, escapeHtml, handle401Error } from '../core/api.js';
import { t } from '../core/i18n.js';
import { lockBody, unlockBody, showToast } from '../core/ui.js';
import { showToast } from '../core/ui.js';
import { Modal } from '../core/modal.js';
import { _computeMaxFps, _renderFpsHint } from './devices.js';
const addDeviceModal = new Modal('add-device-modal');
export function onDeviceTypeChanged() {
const deviceType = document.getElementById('device-type').value;
const urlGroup = document.getElementById('device-url-group');
@@ -154,7 +156,6 @@ export function onSerialPortFocus() {
}
export function showAddDevice() {
const modal = document.getElementById('add-device-modal');
const form = document.getElementById('add-device-form');
const error = document.getElementById('add-device-error');
form.reset();
@@ -172,16 +173,13 @@ export function showAddDevice() {
document.getElementById('device-serial-port').innerHTML = '';
const scanBtn = document.getElementById('scan-network-btn');
if (scanBtn) scanBtn.disabled = false;
modal.style.display = 'flex';
lockBody();
addDeviceModal.open();
onDeviceTypeChanged();
setTimeout(() => document.getElementById('device-name').focus(), 100);
}
export function closeAddDeviceModal() {
const modal = document.getElementById('add-device-modal');
modal.style.display = 'none';
unlockBody();
addDeviceModal.forceClose();
}
export async function scanForDevices(forceType) {

View File

@@ -3,12 +3,38 @@
*/
import {
settingsInitialValues, setSettingsInitialValues,
_deviceBrightnessCache,
} from '../core/state.js';
import { API_BASE, getHeaders, escapeHtml, isSerialDevice, handle401Error } from '../core/api.js';
import { t } from '../core/i18n.js';
import { lockBody, unlockBody, showToast, showConfirm } from '../core/ui.js';
import { showToast } from '../core/ui.js';
import { Modal } from '../core/modal.js';
class DeviceSettingsModal extends Modal {
constructor() { super('device-settings-modal'); }
deviceType = '';
capabilities = [];
snapshotValues() {
return {
name: this.$('settings-device-name').value,
url: this._getUrl(),
state_check_interval: this.$('settings-health-interval').value,
auto_shutdown: this.$('settings-auto-shutdown').checked,
led_count: this.$('settings-led-count').value,
};
}
_getUrl() {
if (isSerialDevice(this.deviceType)) {
return this.$('settings-serial-port').value;
}
return this.$('settings-device-url').value.trim();
}
}
const settingsModal = new DeviceSettingsModal();
export function createDeviceCard(device) {
const state = device.state || {};
@@ -186,20 +212,10 @@ export async function showSettings(deviceId) {
document.getElementById('settings-auto-shutdown').checked = !!device.auto_shutdown;
setSettingsInitialValues({
name: device.name,
url: device.url,
led_count: String(device.led_count || ''),
baud_rate: String(device.baud_rate || '115200'),
device_type: device.device_type,
capabilities: caps,
state_check_interval: '30',
auto_shutdown: !!device.auto_shutdown,
});
const modal = document.getElementById('device-settings-modal');
modal.style.display = 'flex';
lockBody();
settingsModal.deviceType = device.device_type;
settingsModal.capabilities = caps;
settingsModal.snapshot();
settingsModal.open();
setTimeout(() => {
document.getElementById('settings-device-name').focus();
@@ -211,61 +227,27 @@ export async function showSettings(deviceId) {
}
}
function _getSettingsUrl() {
if (isSerialDevice(settingsInitialValues.device_type)) {
return document.getElementById('settings-serial-port').value;
}
return document.getElementById('settings-device-url').value.trim();
}
export function isSettingsDirty() {
const ledCountDirty = (settingsInitialValues.capabilities || []).includes('manual_led_count')
&& document.getElementById('settings-led-count').value !== settingsInitialValues.led_count;
return (
document.getElementById('settings-device-name').value !== settingsInitialValues.name ||
_getSettingsUrl() !== settingsInitialValues.url ||
document.getElementById('settings-health-interval').value !== settingsInitialValues.state_check_interval ||
document.getElementById('settings-auto-shutdown').checked !== settingsInitialValues.auto_shutdown ||
ledCountDirty
);
}
export function forceCloseDeviceSettingsModal() {
const modal = document.getElementById('device-settings-modal');
const error = document.getElementById('settings-error');
modal.style.display = 'none';
error.style.display = 'none';
unlockBody();
setSettingsInitialValues({});
}
export async function closeDeviceSettingsModal() {
if (isSettingsDirty()) {
const confirmed = await showConfirm(t('modal.discard_changes'));
if (!confirmed) return;
}
forceCloseDeviceSettingsModal();
}
export function isSettingsDirty() { return settingsModal.isDirty(); }
export function forceCloseDeviceSettingsModal() { settingsModal.forceClose(); }
export function closeDeviceSettingsModal() { settingsModal.close(); }
export async function saveDeviceSettings() {
const deviceId = document.getElementById('settings-device-id').value;
const name = document.getElementById('settings-device-name').value.trim();
const url = _getSettingsUrl();
const error = document.getElementById('settings-error');
const url = settingsModal._getUrl();
if (!name || !url) {
error.textContent = 'Please fill in all fields correctly';
error.style.display = 'block';
settingsModal.showError('Please fill in all fields correctly');
return;
}
try {
const body = { name, url, auto_shutdown: document.getElementById('settings-auto-shutdown').checked };
const ledCountInput = document.getElementById('settings-led-count');
if ((settingsInitialValues.capabilities || []).includes('manual_led_count') && ledCountInput.value) {
if (settingsModal.capabilities.includes('manual_led_count') && ledCountInput.value) {
body.led_count = parseInt(ledCountInput.value, 10);
}
if (isSerialDevice(settingsInitialValues.device_type)) {
if (isSerialDevice(settingsModal.deviceType)) {
const baudVal = document.getElementById('settings-baud-rate').value;
if (baudVal) body.baud_rate = parseInt(baudVal, 10);
}
@@ -279,18 +261,16 @@ export async function saveDeviceSettings() {
if (!deviceResponse.ok) {
const errorData = await deviceResponse.json();
error.textContent = `Failed to update device: ${errorData.detail}`;
error.style.display = 'block';
settingsModal.showError(`Failed to update device: ${errorData.detail}`);
return;
}
showToast(t('settings.saved'), 'success');
forceCloseDeviceSettingsModal();
settingsModal.forceClose();
window.loadDevices();
} catch (err) {
console.error('Failed to save device settings:', err);
error.textContent = 'Failed to save settings';
error.style.display = 'block';
settingsModal.showError('Failed to save settings');
}
}
@@ -406,7 +386,7 @@ export function updateSettingsBaudFpsHint() {
const hintEl = document.getElementById('settings-baud-fps-hint');
const baudRate = parseInt(document.getElementById('settings-baud-rate').value, 10);
const ledCount = parseInt(document.getElementById('settings-led-count').value, 10);
_renderFpsHint(hintEl, baudRate, ledCount, settingsInitialValues.device_type);
_renderFpsHint(hintEl, baudRate, ledCount, settingsModal.deviceType);
}
// Settings serial port population (used from showSettings)
@@ -419,7 +399,7 @@ async function _populateSettingsSerialPorts(currentUrl) {
select.appendChild(loadingOpt);
try {
const discoverType = settingsInitialValues.device_type || 'adalight';
const discoverType = settingsModal.deviceType || 'adalight';
const resp = await fetch(`${API_BASE}/devices/discover?timeout=2&device_type=${encodeURIComponent(discoverType)}`, {
headers: getHeaders()
});

View File

@@ -5,14 +5,33 @@
import {
kcTestAutoRefresh, setKcTestAutoRefresh,
kcTestTargetId, setKcTestTargetId,
kcEditorInitialValues, setKcEditorInitialValues,
_kcNameManuallyEdited, set_kcNameManuallyEdited,
kcWebSockets,
PATTERN_RECT_BORDERS,
} from '../core/state.js';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, handle401Error } from '../core/api.js';
import { t } from '../core/i18n.js';
import { lockBody, unlockBody, showToast, showConfirm, setupBackdropClose } from '../core/ui.js';
import { lockBody, showToast, showConfirm } from '../core/ui.js';
import { Modal } from '../core/modal.js';
class KCEditorModal extends Modal {
constructor() {
super('kc-editor-modal');
}
snapshotValues() {
return {
name: document.getElementById('kc-editor-name').value,
source: document.getElementById('kc-editor-source').value,
fps: document.getElementById('kc-editor-fps').value,
interpolation: document.getElementById('kc-editor-interpolation').value,
smoothing: document.getElementById('kc-editor-smoothing').value,
patternTemplateId: document.getElementById('kc-editor-pattern-template').value,
};
}
}
const kcEditorModal = new KCEditorModal();
export function createKCTargetCard(target, sourceMap, patternTemplateMap) {
const state = target.state || {};
@@ -391,19 +410,8 @@ export async function showKCEditor(targetId = null) {
patSelect.onchange = () => _autoGenerateKCName();
if (!targetId) _autoGenerateKCName();
setKcEditorInitialValues({
name: document.getElementById('kc-editor-name').value,
source: sourceSelect.value,
fps: document.getElementById('kc-editor-fps').value,
interpolation: document.getElementById('kc-editor-interpolation').value,
smoothing: document.getElementById('kc-editor-smoothing').value,
patternTemplateId: patSelect.value,
});
const modal = document.getElementById('kc-editor-modal');
modal.style.display = 'flex';
lockBody();
setupBackdropClose(modal, closeKCEditorModal);
kcEditorModal.snapshot();
kcEditorModal.open();
document.getElementById('kc-editor-error').style.display = 'none';
setTimeout(() => document.getElementById('kc-editor-name').focus(), 100);
@@ -414,29 +422,15 @@ export async function showKCEditor(targetId = null) {
}
export function isKCEditorDirty() {
return (
document.getElementById('kc-editor-name').value !== kcEditorInitialValues.name ||
document.getElementById('kc-editor-source').value !== kcEditorInitialValues.source ||
document.getElementById('kc-editor-fps').value !== kcEditorInitialValues.fps ||
document.getElementById('kc-editor-interpolation').value !== kcEditorInitialValues.interpolation ||
document.getElementById('kc-editor-smoothing').value !== kcEditorInitialValues.smoothing ||
document.getElementById('kc-editor-pattern-template').value !== kcEditorInitialValues.patternTemplateId
);
return kcEditorModal.isDirty();
}
export async function closeKCEditorModal() {
if (isKCEditorDirty()) {
const confirmed = await showConfirm(t('modal.discard_changes'));
if (!confirmed) return;
}
forceCloseKCEditorModal();
await kcEditorModal.close();
}
export function forceCloseKCEditorModal() {
document.getElementById('kc-editor-modal').style.display = 'none';
document.getElementById('kc-editor-error').style.display = 'none';
unlockBody();
setKcEditorInitialValues({});
kcEditorModal.forceClose();
}
export async function saveKCEditor() {
@@ -447,17 +441,14 @@ export async function saveKCEditor() {
const interpolation = document.getElementById('kc-editor-interpolation').value;
const smoothing = parseFloat(document.getElementById('kc-editor-smoothing').value);
const patternTemplateId = document.getElementById('kc-editor-pattern-template').value;
const errorEl = document.getElementById('kc-editor-error');
if (!name) {
errorEl.textContent = t('kc.error.required');
errorEl.style.display = 'block';
kcEditorModal.showError(t('kc.error.required'));
return;
}
if (!patternTemplateId) {
errorEl.textContent = t('kc.error.no_pattern');
errorEl.style.display = 'block';
kcEditorModal.showError(t('kc.error.no_pattern'));
return;
}
@@ -497,13 +488,12 @@ export async function saveKCEditor() {
}
showToast(targetId ? t('kc.updated') : t('kc.created'), 'success');
forceCloseKCEditorModal();
kcEditorModal.forceClose();
// Use window.* to avoid circular import with targets.js
if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab();
} catch (error) {
console.error('Error saving KC target:', error);
errorEl.textContent = error.message;
errorEl.style.display = 'block';
kcEditorModal.showError(error.message);
}
}

View File

@@ -6,7 +6,6 @@ import {
patternEditorRects, setPatternEditorRects,
patternEditorSelectedIdx, setPatternEditorSelectedIdx,
patternEditorBgImage, setPatternEditorBgImage,
patternEditorInitialValues, setPatternEditorInitialValues,
patternCanvasDragMode, setPatternCanvasDragMode,
patternCanvasDragStart, setPatternCanvasDragStart,
patternCanvasDragOrigRect, setPatternCanvasDragOrigRect,
@@ -17,7 +16,30 @@ import {
} from '../core/state.js';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, handle401Error } from '../core/api.js';
import { t } from '../core/i18n.js';
import { lockBody, unlockBody, showToast, showConfirm, setupBackdropClose } from '../core/ui.js';
import { showToast, showConfirm } from '../core/ui.js';
import { Modal } from '../core/modal.js';
class PatternTemplateModal extends Modal {
constructor() {
super('pattern-template-modal');
}
snapshotValues() {
return {
name: document.getElementById('pattern-template-name').value,
description: document.getElementById('pattern-template-description').value,
rectangles: JSON.stringify(patternEditorRects),
};
}
onForceClose() {
setPatternEditorRects([]);
setPatternEditorSelectedIdx(-1);
setPatternEditorBgImage(null);
}
}
const patternModal = new PatternTemplateModal();
export function createPatternTemplateCard(pt) {
const rectCount = (pt.rectangles || []).length;
@@ -77,20 +99,13 @@ export async function showPatternTemplateEditor(templateId = null) {
setPatternEditorRects([]);
}
setPatternEditorInitialValues({
name: document.getElementById('pattern-template-name').value,
description: document.getElementById('pattern-template-description').value,
rectangles: JSON.stringify(patternEditorRects),
});
patternModal.snapshot();
renderPatternRectList();
renderPatternCanvas();
_attachPatternCanvasEvents();
const modal = document.getElementById('pattern-template-modal');
modal.style.display = 'flex';
lockBody();
setupBackdropClose(modal, closePatternTemplateModal);
patternModal.open();
document.getElementById('pattern-template-error').style.display = 'none';
setTimeout(() => document.getElementById('pattern-template-name').focus(), 100);
@@ -101,40 +116,24 @@ export async function showPatternTemplateEditor(templateId = null) {
}
export function isPatternEditorDirty() {
return (
document.getElementById('pattern-template-name').value !== patternEditorInitialValues.name ||
document.getElementById('pattern-template-description').value !== patternEditorInitialValues.description ||
JSON.stringify(patternEditorRects) !== patternEditorInitialValues.rectangles
);
return patternModal.isDirty();
}
export async function closePatternTemplateModal() {
if (isPatternEditorDirty()) {
const confirmed = await showConfirm(t('modal.discard_changes'));
if (!confirmed) return;
}
forceClosePatternTemplateModal();
await patternModal.close();
}
export function forceClosePatternTemplateModal() {
document.getElementById('pattern-template-modal').style.display = 'none';
document.getElementById('pattern-template-error').style.display = 'none';
unlockBody();
setPatternEditorRects([]);
setPatternEditorSelectedIdx(-1);
setPatternEditorBgImage(null);
setPatternEditorInitialValues({});
patternModal.forceClose();
}
export async function savePatternTemplate() {
const templateId = document.getElementById('pattern-template-id').value;
const name = document.getElementById('pattern-template-name').value.trim();
const description = document.getElementById('pattern-template-description').value.trim();
const errorEl = document.getElementById('pattern-template-error');
if (!name) {
errorEl.textContent = t('pattern.error.required');
errorEl.style.display = 'block';
patternModal.showError(t('pattern.error.required'));
return;
}
@@ -165,13 +164,12 @@ export async function savePatternTemplate() {
}
showToast(templateId ? t('pattern.updated') : t('pattern.created'), 'success');
forceClosePatternTemplateModal();
patternModal.forceClose();
// Use window.* to avoid circular import with targets.js
if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab();
} catch (error) {
console.error('Error saving pattern template:', error);
errorEl.textContent = error.message;
errorEl.style.display = 'block';
patternModal.showError(error.message);
}
}

View File

@@ -5,7 +5,10 @@
import { _profilesCache, set_profilesCache } from '../core/state.js';
import { API_BASE, getHeaders, escapeHtml, handle401Error } from '../core/api.js';
import { t, updateAllText } from '../core/i18n.js';
import { lockBody, unlockBody, showToast, showConfirm } from '../core/ui.js';
import { showToast, showConfirm } from '../core/ui.js';
import { Modal } from '../core/modal.js';
const profileModal = new Modal('profile-editor-modal');
export async function loadProfiles() {
const container = document.getElementById('profiles-content');
@@ -137,14 +140,12 @@ export async function openProfileEditor(profileId) {
logicSelect.value = 'or';
}
modal.style.display = 'flex';
lockBody();
profileModal.open();
updateAllText();
}
export function closeProfileEditorModal() {
document.getElementById('profile-editor-modal').style.display = 'none';
unlockBody();
profileModal.forceClose();
}
async function loadProfileTargetChecklist(selectedIds) {
@@ -304,12 +305,10 @@ export async function saveProfileEditor() {
const nameInput = document.getElementById('profile-editor-name');
const enabledInput = document.getElementById('profile-editor-enabled');
const logicSelect = document.getElementById('profile-editor-logic');
const errorEl = document.getElementById('profile-editor-error');
const name = nameInput.value.trim();
if (!name) {
errorEl.textContent = 'Name is required';
errorEl.style.display = 'block';
profileModal.showError('Name is required');
return;
}
@@ -336,12 +335,11 @@ export async function saveProfileEditor() {
throw new Error(err.detail || 'Failed to save profile');
}
closeProfileEditorModal();
profileModal.forceClose();
showToast(isEdit ? 'Profile updated' : 'Profile created', 'success');
loadProfiles();
} catch (e) {
errorEl.textContent = e.message;
errorEl.style.display = 'block';
profileModal.showError(e.message);
}
}

View File

@@ -21,9 +21,19 @@ import {
} from '../core/state.js';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, handle401Error } from '../core/api.js';
import { t } from '../core/i18n.js';
import { setupBackdropClose, lockBody, unlockBody, showToast, showConfirm, openLightbox, openFullImageLightbox, showOverlaySpinner, hideOverlaySpinner } from '../core/ui.js';
import { Modal } from '../core/modal.js';
import { showToast, showConfirm, openLightbox, openFullImageLightbox, showOverlaySpinner, hideOverlaySpinner } from '../core/ui.js';
import { openDisplayPicker, formatDisplayLabel } from './displays.js';
// ===== Modal instances =====
const templateModal = new Modal('template-modal');
const testTemplateModal = new Modal('test-template-modal');
const streamModal = new Modal('stream-modal');
const testStreamModal = new Modal('test-stream-modal');
const ppTemplateModal = new Modal('pp-template-modal');
const testPPTemplateModal = new Modal('test-pp-template-modal');
// ===== Capture Templates =====
async function loadCaptureTemplates() {
@@ -55,9 +65,7 @@ export async function showAddTemplateModal() {
await loadAvailableEngines();
const modal = document.getElementById('template-modal');
modal.style.display = 'flex';
setupBackdropClose(modal, closeTemplateModal);
templateModal.open();
}
export async function editTemplate(templateId) {
@@ -83,9 +91,7 @@ export async function editTemplate(templateId) {
if (testResults) testResults.style.display = 'none';
document.getElementById('template-error').style.display = 'none';
const modal = document.getElementById('template-modal');
modal.style.display = 'flex';
setupBackdropClose(modal, closeTemplateModal);
templateModal.open();
} catch (error) {
console.error('Error loading template:', error);
showToast(t('templates.error.load') + ': ' + error.message, 'error');
@@ -93,7 +99,7 @@ export async function editTemplate(templateId) {
}
export function closeTemplateModal() {
document.getElementById('template-modal').style.display = 'none';
templateModal.forceClose();
setCurrentEditingTemplateId(null);
}
@@ -125,13 +131,11 @@ export async function showTestTemplateModal(templateId) {
await loadDisplaysForTest();
restoreCaptureDuration();
const modal = document.getElementById('test-template-modal');
modal.style.display = 'flex';
setupBackdropClose(modal, closeTestTemplateModal);
testTemplateModal.open();
}
export function closeTestTemplateModal() {
document.getElementById('test-template-modal').style.display = 'none';
testTemplateModal.forceClose();
window.currentTestingTemplate = null;
}
@@ -707,10 +711,7 @@ export async function showAddStreamModal(presetType) {
await populateStreamModalDropdowns();
const modal = document.getElementById('stream-modal');
modal.style.display = 'flex';
lockBody();
setupBackdropClose(modal, closeStreamModal);
streamModal.open();
}
export async function editStream(streamId) {
@@ -754,10 +755,7 @@ export async function editStream(streamId) {
if (stream.image_source) validateStaticImage();
}
const modal = document.getElementById('stream-modal');
modal.style.display = 'flex';
lockBody();
setupBackdropClose(modal, closeStreamModal);
streamModal.open();
} catch (error) {
console.error('Error loading stream:', error);
showToast(t('streams.error.load') + ': ' + error.message, 'error');
@@ -896,9 +894,8 @@ export async function deleteStream(streamId) {
}
export function closeStreamModal() {
document.getElementById('stream-modal').style.display = 'none';
streamModal.forceClose();
document.getElementById('stream-type').disabled = false;
unlockBody();
}
async function validateStaticImage() {
@@ -956,15 +953,11 @@ export async function showTestStreamModal(streamId) {
set_currentTestStreamId(streamId);
restoreStreamTestDuration();
const modal = document.getElementById('test-stream-modal');
modal.style.display = 'flex';
lockBody();
setupBackdropClose(modal, closeTestStreamModal);
testStreamModal.open();
}
export function closeTestStreamModal() {
document.getElementById('test-stream-modal').style.display = 'none';
unlockBody();
testStreamModal.forceClose();
set_currentTestStreamId(null);
}
@@ -1032,15 +1025,11 @@ export async function showTestPPTemplateModal(templateId) {
select.value = lastStream;
}
const modal = document.getElementById('test-pp-template-modal');
modal.style.display = 'flex';
lockBody();
setupBackdropClose(modal, closeTestPPTemplateModal);
testPPTemplateModal.open();
}
export function closeTestPPTemplateModal() {
document.getElementById('test-pp-template-modal').style.display = 'none';
unlockBody();
testPPTemplateModal.forceClose();
set_currentTestPPTemplateId(null);
}
@@ -1296,10 +1285,7 @@ export async function showAddPPTemplateModal() {
_populateFilterSelect();
renderModalFilterList();
const modal = document.getElementById('pp-template-modal');
modal.style.display = 'flex';
lockBody();
setupBackdropClose(modal, closePPTemplateModal);
ppTemplateModal.open();
}
export async function editPPTemplate(templateId) {
@@ -1324,10 +1310,7 @@ export async function editPPTemplate(templateId) {
_populateFilterSelect();
renderModalFilterList();
const modal = document.getElementById('pp-template-modal');
modal.style.display = 'flex';
lockBody();
setupBackdropClose(modal, closePPTemplateModal);
ppTemplateModal.open();
} catch (error) {
console.error('Error loading PP template:', error);
showToast(t('postprocessing.error.load') + ': ' + error.message, 'error');
@@ -1386,9 +1369,8 @@ export async function deletePPTemplate(templateId) {
}
export function closePPTemplateModal() {
document.getElementById('pp-template-modal').style.display = 'none';
ppTemplateModal.forceClose();
set_modalFilters([]);
unlockBody();
}
// Exported helpers used by other modules

View File

@@ -3,20 +3,53 @@
*/
import {
targetEditorInitialValues, setTargetEditorInitialValues,
_targetEditorDevices, set_targetEditorDevices,
_deviceBrightnessCache,
kcWebSockets,
} from '../core/state.js';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, handle401Error } from '../core/api.js';
import { t } from '../core/i18n.js';
import { lockBody, unlockBody, showToast, showConfirm, setupBackdropClose } from '../core/ui.js';
import { showToast, showConfirm } from '../core/ui.js';
import { Modal } from '../core/modal.js';
import { createDeviceCard, attachDeviceListeners, fetchDeviceBrightness } from './devices.js';
import { createKCTargetCard, connectKCWebSocket, disconnectKCWebSocket } from './kc-targets.js';
// createPatternTemplateCard is imported via window.* to avoid circular deps
// (pattern-templates.js calls window.loadTargetsTab)
class TargetEditorModal extends Modal {
constructor() {
super('target-editor-modal');
}
snapshotValues() {
return {
name: document.getElementById('target-editor-name').value,
device: document.getElementById('target-editor-device').value,
source: document.getElementById('target-editor-source').value,
fps: document.getElementById('target-editor-fps').value,
interpolation: document.getElementById('target-editor-interpolation').value,
smoothing: document.getElementById('target-editor-smoothing').value,
standby_interval: document.getElementById('target-editor-standby-interval').value,
};
}
}
const targetEditorModal = new TargetEditorModal();
let _targetNameManuallyEdited = false;
function _autoGenerateTargetName() {
if (_targetNameManuallyEdited) return;
if (document.getElementById('target-editor-id').value) return;
const deviceSelect = document.getElementById('target-editor-device');
const sourceSelect = document.getElementById('target-editor-source');
const deviceName = deviceSelect.selectedOptions[0]?.dataset?.name || '';
const sourceName = sourceSelect.selectedOptions[0]?.dataset?.name || '';
if (!deviceName || !sourceName) return;
document.getElementById('target-editor-name').value = `${deviceName} \u00b7 ${sourceName}`;
}
function _updateStandbyVisibility() {
const deviceSelect = document.getElementById('target-editor-device');
const standbyGroup = document.getElementById('target-editor-standby-group');
@@ -43,12 +76,12 @@ export async function showTargetEditor(targetId = null) {
devices.forEach(d => {
const opt = document.createElement('option');
opt.value = d.id;
opt.dataset.name = d.name;
const shortUrl = d.url ? d.url.replace(/^https?:\/\//, '') : '';
const devType = (d.device_type || 'wled').toUpperCase();
opt.textContent = `${d.name} [${devType}]${shortUrl ? ' (' + shortUrl + ')' : ''}`;
deviceSelect.appendChild(opt);
});
deviceSelect.onchange = _updateStandbyVisibility;
// Populate source select
const sourceSelect = document.getElementById('target-editor-source');
@@ -56,6 +89,7 @@ export async function showTargetEditor(targetId = null) {
sources.forEach(s => {
const opt = document.createElement('option');
opt.value = s.id;
opt.dataset.name = s.name;
const typeIcon = s.stream_type === 'raw' ? '\uD83D\uDDA5\uFE0F' : s.stream_type === 'static_image' ? '\uD83D\uDDBC\uFE0F' : '\uD83C\uDFA8';
opt.textContent = `${typeIcon} ${s.name}`;
sourceSelect.appendChild(opt);
@@ -80,11 +114,9 @@ export async function showTargetEditor(targetId = null) {
document.getElementById('target-editor-standby-interval-value').textContent = target.settings?.standby_interval ?? 1.0;
document.getElementById('target-editor-title').textContent = t('targets.edit');
} else {
// Creating new target
// Creating new target — first option is selected by default
document.getElementById('target-editor-id').value = '';
document.getElementById('target-editor-name').value = '';
deviceSelect.value = '';
sourceSelect.value = '';
document.getElementById('target-editor-fps').value = 30;
document.getElementById('target-editor-fps-value').textContent = '30';
document.getElementById('target-editor-interpolation').value = 'average';
@@ -95,23 +127,18 @@ export async function showTargetEditor(targetId = null) {
document.getElementById('target-editor-title').textContent = t('targets.add');
}
// Auto-name generation
_targetNameManuallyEdited = !!targetId;
document.getElementById('target-editor-name').oninput = () => { _targetNameManuallyEdited = true; };
deviceSelect.onchange = () => { _updateStandbyVisibility(); _autoGenerateTargetName(); };
sourceSelect.onchange = () => _autoGenerateTargetName();
if (!targetId) _autoGenerateTargetName();
// Show/hide standby interval based on selected device capabilities
_updateStandbyVisibility();
setTargetEditorInitialValues({
name: document.getElementById('target-editor-name').value,
device: deviceSelect.value,
source: sourceSelect.value,
fps: document.getElementById('target-editor-fps').value,
interpolation: document.getElementById('target-editor-interpolation').value,
smoothing: document.getElementById('target-editor-smoothing').value,
standby_interval: document.getElementById('target-editor-standby-interval').value,
});
const modal = document.getElementById('target-editor-modal');
modal.style.display = 'flex';
lockBody();
setupBackdropClose(modal, closeTargetEditorModal);
targetEditorModal.snapshot();
targetEditorModal.open();
document.getElementById('target-editor-error').style.display = 'none';
setTimeout(() => document.getElementById('target-editor-name').focus(), 100);
@@ -122,30 +149,15 @@ export async function showTargetEditor(targetId = null) {
}
export function isTargetEditorDirty() {
return (
document.getElementById('target-editor-name').value !== targetEditorInitialValues.name ||
document.getElementById('target-editor-device').value !== targetEditorInitialValues.device ||
document.getElementById('target-editor-source').value !== targetEditorInitialValues.source ||
document.getElementById('target-editor-fps').value !== targetEditorInitialValues.fps ||
document.getElementById('target-editor-interpolation').value !== targetEditorInitialValues.interpolation ||
document.getElementById('target-editor-smoothing').value !== targetEditorInitialValues.smoothing ||
document.getElementById('target-editor-standby-interval').value !== targetEditorInitialValues.standby_interval
);
return targetEditorModal.isDirty();
}
export async function closeTargetEditorModal() {
if (isTargetEditorDirty()) {
const confirmed = await showConfirm(t('modal.discard_changes'));
if (!confirmed) return;
}
forceCloseTargetEditorModal();
await targetEditorModal.close();
}
export function forceCloseTargetEditorModal() {
document.getElementById('target-editor-modal').style.display = 'none';
document.getElementById('target-editor-error').style.display = 'none';
unlockBody();
setTargetEditorInitialValues({});
targetEditorModal.forceClose();
}
export async function saveTargetEditor() {
@@ -157,11 +169,9 @@ export async function saveTargetEditor() {
const interpolation = document.getElementById('target-editor-interpolation').value;
const smoothing = parseFloat(document.getElementById('target-editor-smoothing').value);
const standbyInterval = parseFloat(document.getElementById('target-editor-standby-interval').value);
const errorEl = document.getElementById('target-editor-error');
if (!name) {
errorEl.textContent = t('targets.error.name_required');
errorEl.style.display = 'block';
targetEditorModal.showError(t('targets.error.name_required'));
return;
}
@@ -202,12 +212,11 @@ export async function saveTargetEditor() {
}
showToast(targetId ? t('targets.updated') : t('targets.created'), 'success');
forceCloseTargetEditorModal();
targetEditorModal.forceClose();
await loadTargetsTab();
} catch (error) {
console.error('Error saving target:', error);
errorEl.textContent = error.message;
errorEl.style.display = 'block';
targetEditorModal.showError(error.message);
}
}