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:
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user