feat: add auto-name generation to all remaining creation modals
Some checks failed
Lint & Test / test (push) Failing after 27s
Some checks failed
Lint & Test / test (push) Failing after 27s
Audio sources: type + device/parent/channel/band detail Weather sources: provider + coordinates (updates on geolocation) Sync clocks: "Sync Clocks · Nx" (updates on speed slider) Automations: scene name + condition count/logic Scene presets: "Scenes · N targets" (updates on add/remove) Pattern templates: "Pattern Templates · N rects" (updates on add/remove) All follow the same pattern: name auto-generates on create, stops when user manually edits the name field.
This commit is contained in:
@@ -70,6 +70,37 @@ function _buildBandItems() {
|
||||
];
|
||||
}
|
||||
|
||||
// ── Auto-name generation ──────────────────────────────────────
|
||||
|
||||
let _asNameManuallyEdited = false;
|
||||
|
||||
function _autoGenerateAudioSourceName() {
|
||||
if (_asNameManuallyEdited) return;
|
||||
if ((document.getElementById('audio-source-id') as HTMLInputElement).value) return;
|
||||
const type = (document.getElementById('audio-source-type') as HTMLSelectElement).value;
|
||||
let name = '';
|
||||
if (type === 'multichannel') {
|
||||
const devSel = document.getElementById('audio-source-device') as HTMLSelectElement | null;
|
||||
const devName = devSel?.selectedOptions[0]?.textContent?.trim();
|
||||
name = devName || t('audio_source.type.multichannel');
|
||||
} else if (type === 'mono') {
|
||||
const parentSel = document.getElementById('audio-source-parent') as HTMLSelectElement | null;
|
||||
const parentName = parentSel?.selectedOptions[0]?.textContent?.trim() || '';
|
||||
const ch = (document.getElementById('audio-source-channel') as HTMLSelectElement).value;
|
||||
const chLabel = ch === 'left' ? 'L' : ch === 'right' ? 'R' : 'M';
|
||||
name = parentName ? `${parentName} · ${chLabel}` : t('audio_source.type.mono');
|
||||
} else if (type === 'band_extract') {
|
||||
const parentSel = document.getElementById('audio-source-band-parent') as HTMLSelectElement | null;
|
||||
const parentName = parentSel?.selectedOptions[0]?.textContent?.trim() || '';
|
||||
const band = (document.getElementById('audio-source-band') as HTMLSelectElement).value;
|
||||
const bandLabel = band === 'custom'
|
||||
? `${(document.getElementById('audio-source-freq-low') as HTMLInputElement).value}–${(document.getElementById('audio-source-freq-high') as HTMLInputElement).value} Hz`
|
||||
: t(`audio_source.band.${band}`);
|
||||
name = parentName ? `${parentName} · ${bandLabel}` : bandLabel;
|
||||
}
|
||||
(document.getElementById('audio-source-name') as HTMLInputElement).value = name;
|
||||
}
|
||||
|
||||
// ── Modal ─────────────────────────────────────────────────────
|
||||
|
||||
const _titleKeys: Record<string, Record<string, string>> = {
|
||||
@@ -99,12 +130,13 @@ export async function showAudioSourceModal(sourceType: any, editData?: any) {
|
||||
|
||||
if (editData.source_type === 'multichannel') {
|
||||
_loadAudioTemplates(editData.audio_template_id);
|
||||
(document.getElementById('audio-source-audio-template') as HTMLSelectElement).onchange = _filterDevicesBySelectedTemplate;
|
||||
(document.getElementById('audio-source-audio-template') as HTMLSelectElement).onchange = () => { _filterDevicesBySelectedTemplate(); _autoGenerateAudioSourceName(); };
|
||||
await _loadAudioDevices();
|
||||
_selectAudioDevice(editData.device_index, editData.is_loopback);
|
||||
} else if (editData.source_type === 'mono') {
|
||||
_loadMultichannelSources(editData.audio_source_id);
|
||||
(document.getElementById('audio-source-channel') as HTMLSelectElement).value = editData.channel || 'mono';
|
||||
(document.getElementById('audio-source-channel') as HTMLSelectElement).onchange = () => _autoGenerateAudioSourceName();
|
||||
} else if (editData.source_type === 'band_extract') {
|
||||
_loadBandParentSources(editData.audio_source_id);
|
||||
(document.getElementById('audio-source-band') as HTMLSelectElement).value = editData.band || 'bass';
|
||||
@@ -119,10 +151,11 @@ export async function showAudioSourceModal(sourceType: any, editData?: any) {
|
||||
|
||||
if (sourceType === 'multichannel') {
|
||||
_loadAudioTemplates();
|
||||
(document.getElementById('audio-source-audio-template') as HTMLSelectElement).onchange = _filterDevicesBySelectedTemplate;
|
||||
(document.getElementById('audio-source-audio-template') as HTMLSelectElement).onchange = () => { _filterDevicesBySelectedTemplate(); _autoGenerateAudioSourceName(); };
|
||||
await _loadAudioDevices();
|
||||
} else if (sourceType === 'mono') {
|
||||
_loadMultichannelSources();
|
||||
(document.getElementById('audio-source-channel') as HTMLSelectElement).onchange = () => _autoGenerateAudioSourceName();
|
||||
} else if (sourceType === 'band_extract') {
|
||||
_loadBandParentSources();
|
||||
(document.getElementById('audio-source-band') as HTMLSelectElement).value = 'bass';
|
||||
@@ -138,6 +171,11 @@ export async function showAudioSourceModal(sourceType: any, editData?: any) {
|
||||
_audioSourceTagsInput = new TagInput(document.getElementById('audio-source-tags-container'), { placeholder: t('tags.placeholder') });
|
||||
_audioSourceTagsInput.setValue(isEdit ? (editData.tags || []) : []);
|
||||
|
||||
// Auto-name wiring
|
||||
_asNameManuallyEdited = isEdit;
|
||||
(document.getElementById('audio-source-name') as HTMLElement).oninput = () => { _asNameManuallyEdited = true; };
|
||||
if (!isEdit) _autoGenerateAudioSourceName();
|
||||
|
||||
audioSourceModal.open();
|
||||
audioSourceModal.snapshot();
|
||||
}
|
||||
@@ -384,7 +422,7 @@ function _ensureBandIconSelect() {
|
||||
target: sel,
|
||||
items: _buildBandItems(),
|
||||
columns: 2,
|
||||
onChange: () => onBandPresetChange(),
|
||||
onChange: () => { onBandPresetChange(); _autoGenerateAudioSourceName(); },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,27 @@ import type { Automation } from '../types.ts';
|
||||
|
||||
let _automationTagsInput: any = null;
|
||||
|
||||
// ── Auto-name ──
|
||||
|
||||
let _autoNameManuallyEdited = false;
|
||||
|
||||
function _autoGenerateAutomationName() {
|
||||
if (_autoNameManuallyEdited) return;
|
||||
if ((document.getElementById('automation-editor-id') as HTMLInputElement).value) return;
|
||||
const sceneSel = document.getElementById('automation-scene-id') as HTMLSelectElement | null;
|
||||
const sceneName = sceneSel?.selectedOptions[0]?.textContent?.trim() || '';
|
||||
const logic = (document.getElementById('automation-editor-logic') as HTMLSelectElement).value;
|
||||
const condCount = document.querySelectorAll('#automation-conditions-list .condition-row').length;
|
||||
let name = '';
|
||||
if (sceneName) name = sceneName;
|
||||
if (condCount > 0) {
|
||||
const logicLabel = logic === 'and' ? 'AND' : 'OR';
|
||||
const suffix = `${condCount} ${logicLabel}`;
|
||||
name = name ? `${name} · ${suffix}` : suffix;
|
||||
}
|
||||
(document.getElementById('automation-editor-name') as HTMLInputElement).value = name || t('automations.add');
|
||||
}
|
||||
|
||||
class AutomationEditorModal extends Modal {
|
||||
constructor() { super('automation-editor-modal'); }
|
||||
|
||||
@@ -395,6 +416,12 @@ export async function openAutomationEditor(automationId?: any, cloneData?: any)
|
||||
// Wire up deactivation mode change
|
||||
(document.getElementById('automation-deactivation-mode') as HTMLSelectElement).onchange = _onDeactivationModeChange;
|
||||
|
||||
// Auto-name wiring
|
||||
_autoNameManuallyEdited = !!(automationId || cloneData);
|
||||
nameInput.oninput = () => { _autoNameManuallyEdited = true; };
|
||||
(window as any)._autoGenerateAutomationName = _autoGenerateAutomationName;
|
||||
if (!automationId && !cloneData) _autoGenerateAutomationName();
|
||||
|
||||
automationModal.open();
|
||||
modal!.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
el.textContent = t(el.getAttribute('data-i18n')!);
|
||||
@@ -453,7 +480,12 @@ function _initSceneSelector(selectId: any, selectedId: any) {
|
||||
allowNone: true,
|
||||
noneLabel: t('automations.scene.none_selected'),
|
||||
} as any);
|
||||
if (isMain) _sceneEntitySelect = es; else _fallbackSceneEntitySelect = es;
|
||||
if (isMain) {
|
||||
_sceneEntitySelect = es;
|
||||
sel.onchange = () => _autoGenerateAutomationName();
|
||||
} else {
|
||||
_fallbackSceneEntitySelect = es;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Deactivation mode IconSelect =====
|
||||
@@ -480,6 +512,7 @@ function _ensureDeactivationModeIconSelect() {
|
||||
|
||||
export function addAutomationCondition() {
|
||||
addAutomationConditionRow({ condition_type: 'application', apps: [], match_type: 'running' });
|
||||
_autoGenerateAutomationName();
|
||||
}
|
||||
|
||||
const CONDITION_TYPE_KEYS = ['always', 'startup', 'application', 'time_of_day', 'system_idle', 'display_state', 'mqtt', 'webhook'];
|
||||
@@ -577,7 +610,7 @@ function addAutomationConditionRow(condition: any) {
|
||||
<select class="condition-type-select">
|
||||
${CONDITION_TYPE_KEYS.map(k => `<option value="${k}" ${condType === k ? 'selected' : ''}>${t('automations.condition.' + k)}</option>`).join('')}
|
||||
</select>
|
||||
<button type="button" class="btn-remove-condition" onclick="this.closest('.automation-condition-row').remove()" title="Remove">✕</button>
|
||||
<button type="button" class="btn-remove-condition" onclick="this.closest('.automation-condition-row').remove(); if(window._autoGenerateAutomationName) window._autoGenerateAutomationName();" title="Remove">✕</button>
|
||||
</div>
|
||||
<div class="condition-fields-container"></div>
|
||||
`;
|
||||
|
||||
@@ -29,6 +29,20 @@ import type { PatternTemplate } from '../types.ts';
|
||||
let _patternBgEntitySelect: EntitySelect | null = null;
|
||||
let _patternTagsInput: TagInput | null = null;
|
||||
|
||||
// ── Auto-name ──
|
||||
|
||||
let _ptNameManuallyEdited = false;
|
||||
|
||||
function _autoGeneratePatternName() {
|
||||
if (_ptNameManuallyEdited) return;
|
||||
if ((document.getElementById('pattern-template-id') as HTMLInputElement).value) return;
|
||||
const count = patternEditorRects.length;
|
||||
const label = count > 0
|
||||
? `${t('targets.section.pattern_templates')} · ${count} ${count === 1 ? 'rect' : 'rects'}`
|
||||
: t('targets.section.pattern_templates');
|
||||
(document.getElementById('pattern-template-name') as HTMLInputElement).value = label;
|
||||
}
|
||||
|
||||
class PatternTemplateModal extends Modal {
|
||||
constructor() {
|
||||
super('pattern-template-modal');
|
||||
@@ -149,6 +163,11 @@ export async function showPatternTemplateEditor(templateId: string | null = null
|
||||
_patternTagsInput = new TagInput(document.getElementById('pattern-tags-container'), { placeholder: t('tags.placeholder') });
|
||||
_patternTagsInput.setValue(_editorTags);
|
||||
|
||||
// Auto-name wiring
|
||||
_ptNameManuallyEdited = !!(templateId || cloneData);
|
||||
(document.getElementById('pattern-template-name') as HTMLElement).oninput = () => { _ptNameManuallyEdited = true; };
|
||||
if (!templateId && !cloneData) _autoGeneratePatternName();
|
||||
|
||||
patternModal.snapshot();
|
||||
|
||||
renderPatternRectList();
|
||||
@@ -317,6 +336,7 @@ export function addPatternRect(): void {
|
||||
setPatternEditorSelectedIdx(patternEditorRects.length - 1);
|
||||
renderPatternRectList();
|
||||
renderPatternCanvas();
|
||||
_autoGeneratePatternName();
|
||||
}
|
||||
|
||||
export function deleteSelectedPatternRect(): void {
|
||||
@@ -325,6 +345,7 @@ export function deleteSelectedPatternRect(): void {
|
||||
setPatternEditorSelectedIdx(-1);
|
||||
renderPatternRectList();
|
||||
renderPatternCanvas();
|
||||
_autoGeneratePatternName();
|
||||
}
|
||||
|
||||
export function removePatternRect(index: number): void {
|
||||
@@ -333,6 +354,7 @@ export function removePatternRect(index: number): void {
|
||||
else if (patternEditorSelectedIdx > index) setPatternEditorSelectedIdx(patternEditorSelectedIdx - 1);
|
||||
renderPatternRectList();
|
||||
renderPatternCanvas();
|
||||
_autoGeneratePatternName();
|
||||
}
|
||||
|
||||
// ----- Pattern Canvas Visual Editor -----
|
||||
|
||||
@@ -22,6 +22,19 @@ let _editingId: string | null = null;
|
||||
let _allTargets: any[] = []; // fetched on capture open
|
||||
let _sceneTagsInput: TagInput | null = null;
|
||||
|
||||
// ── Auto-name ──
|
||||
|
||||
let _spNameManuallyEdited = false;
|
||||
|
||||
function _autoGenerateScenePresetName() {
|
||||
if (_spNameManuallyEdited) return;
|
||||
if ((document.getElementById('scene-preset-editor-id') as HTMLInputElement).value) return;
|
||||
const items = document.querySelectorAll('#scene-target-list .scene-target-item');
|
||||
const count = items.length;
|
||||
const label = count > 0 ? `${t('scenes.title')} · ${count} ${count === 1 ? 'target' : 'targets'}` : t('scenes.title');
|
||||
(document.getElementById('scene-preset-editor-name') as HTMLInputElement).value = label;
|
||||
}
|
||||
|
||||
class ScenePresetEditorModal extends Modal {
|
||||
constructor() { super('scene-preset-editor-modal'); }
|
||||
onForceClose() {
|
||||
@@ -165,6 +178,11 @@ export async function openScenePresetCapture(): Promise<void> {
|
||||
_sceneTagsInput = new TagInput(document.getElementById('scene-tags-container'), { placeholder: t('tags.placeholder') });
|
||||
_sceneTagsInput.setValue([]);
|
||||
|
||||
// Auto-name wiring
|
||||
_spNameManuallyEdited = false;
|
||||
(document.getElementById('scene-preset-editor-name') as HTMLElement).oninput = () => { _spNameManuallyEdited = true; };
|
||||
_autoGenerateScenePresetName();
|
||||
|
||||
scenePresetModal.open();
|
||||
scenePresetModal.snapshot();
|
||||
}
|
||||
@@ -319,7 +337,10 @@ export async function addSceneTarget(): Promise<void> {
|
||||
if (!picked) return;
|
||||
|
||||
const tgt = _allTargets.find(t => t.id === picked);
|
||||
if (tgt) _addTargetToList(tgt.id, tgt.name);
|
||||
if (tgt) {
|
||||
_addTargetToList(tgt.id, tgt.name);
|
||||
_autoGenerateScenePresetName();
|
||||
}
|
||||
}
|
||||
|
||||
// removeSceneTarget is now handled via event delegation on the modal
|
||||
@@ -492,6 +513,7 @@ export function initScenePresetDelegation(container: HTMLElement): void {
|
||||
if (item) {
|
||||
item.remove();
|
||||
_refreshTargetSelect();
|
||||
_autoGenerateScenePresetName();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -528,6 +550,7 @@ if (_sceneEditorModal) {
|
||||
if (item) {
|
||||
item.remove();
|
||||
_refreshTargetSelect();
|
||||
_autoGenerateScenePresetName();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,17 @@ import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import { loadPictureSources } from './streams.ts';
|
||||
import type { SyncClock } from '../types.ts';
|
||||
|
||||
// ── Auto-name ──
|
||||
|
||||
let _scNameManuallyEdited = false;
|
||||
|
||||
function _autoGenerateSyncClockName() {
|
||||
if (_scNameManuallyEdited) return;
|
||||
if ((document.getElementById('sync-clock-id') as HTMLInputElement).value) return;
|
||||
const speed = parseFloat((document.getElementById('sync-clock-speed') as HTMLInputElement).value) || 1.0;
|
||||
(document.getElementById('sync-clock-name') as HTMLInputElement).value = `${t('sync_clock.group.title')} · ${speed}x`;
|
||||
}
|
||||
|
||||
// ── Modal ──
|
||||
|
||||
let _syncClockTagsInput: TagInput | null = null;
|
||||
@@ -62,6 +73,12 @@ export async function showSyncClockModal(editData: SyncClock | null): Promise<vo
|
||||
_syncClockTagsInput = new TagInput(document.getElementById('sync-clock-tags-container'), { placeholder: t('tags.placeholder') });
|
||||
_syncClockTagsInput.setValue(isEdit ? (editData.tags || []) : []);
|
||||
|
||||
// Auto-name wiring
|
||||
_scNameManuallyEdited = isEdit;
|
||||
(document.getElementById('sync-clock-name') as HTMLElement).oninput = () => { _scNameManuallyEdited = true; };
|
||||
(document.getElementById('sync-clock-speed') as HTMLElement).oninput = () => _autoGenerateSyncClockName();
|
||||
if (!isEdit) _autoGenerateSyncClockName();
|
||||
|
||||
syncClockModal.open();
|
||||
syncClockModal.snapshot();
|
||||
}
|
||||
|
||||
@@ -24,6 +24,20 @@ function _getProviderItems() {
|
||||
];
|
||||
}
|
||||
|
||||
// ── Auto-name ──
|
||||
|
||||
let _wsNameManuallyEdited = false;
|
||||
|
||||
function _autoGenerateWeatherSourceName() {
|
||||
if (_wsNameManuallyEdited) return;
|
||||
if ((document.getElementById('weather-source-id') as HTMLInputElement).value) return;
|
||||
const provider = (document.getElementById('weather-source-provider') as HTMLSelectElement).value;
|
||||
const lat = (document.getElementById('weather-source-latitude') as HTMLInputElement).value;
|
||||
const lon = (document.getElementById('weather-source-longitude') as HTMLInputElement).value;
|
||||
const providerLabel = provider === 'open_meteo' ? 'Open-Meteo' : provider;
|
||||
(document.getElementById('weather-source-name') as HTMLInputElement).value = `${providerLabel} · ${lat}, ${lon}`;
|
||||
}
|
||||
|
||||
// ── Modal ──
|
||||
|
||||
let _weatherSourceTagsInput: TagInput | null = null;
|
||||
@@ -92,6 +106,13 @@ export async function showWeatherSourceModal(editData: WeatherSource | null = nu
|
||||
columns: 1,
|
||||
});
|
||||
|
||||
// Auto-name wiring
|
||||
_wsNameManuallyEdited = isEdit;
|
||||
(document.getElementById('weather-source-name') as HTMLElement).oninput = () => { _wsNameManuallyEdited = true; };
|
||||
(document.getElementById('weather-source-latitude') as HTMLElement).oninput = () => _autoGenerateWeatherSourceName();
|
||||
(document.getElementById('weather-source-longitude') as HTMLElement).oninput = () => _autoGenerateWeatherSourceName();
|
||||
if (!isEdit) _autoGenerateWeatherSourceName();
|
||||
|
||||
// Show/hide test button based on edit mode
|
||||
const testBtn = document.getElementById('weather-source-test-btn');
|
||||
if (testBtn) testBtn.style.display = isEdit ? '' : 'none';
|
||||
@@ -230,6 +251,7 @@ export function weatherSourceGeolocate(): void {
|
||||
(pos) => {
|
||||
(document.getElementById('weather-source-latitude') as HTMLInputElement).value = pos.coords.latitude.toFixed(4);
|
||||
(document.getElementById('weather-source-longitude') as HTMLInputElement).value = pos.coords.longitude.toFixed(4);
|
||||
_autoGenerateWeatherSourceName();
|
||||
if (btn) btn.classList.remove('loading');
|
||||
showToast(t('weather_source.geo.success'), 'success');
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user