Replace profile targets with scene activation and searchable scene selector
Profiles now activate scene presets instead of individual targets, with configurable deactivation behavior (none/revert/fallback scene). The target checklist UI is replaced by a searchable combobox for scene selection that scales well with many scenes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -79,7 +79,7 @@ import {
|
||||
import {
|
||||
loadProfiles, openProfileEditor, closeProfileEditorModal,
|
||||
saveProfileEditor, addProfileCondition,
|
||||
toggleProfileEnabled, toggleProfileTargets, deleteProfile,
|
||||
toggleProfileEnabled, deleteProfile,
|
||||
expandAllProfileSections, collapseAllProfileSections,
|
||||
} from './features/profiles.js';
|
||||
import {
|
||||
@@ -307,7 +307,6 @@ Object.assign(window, {
|
||||
saveProfileEditor,
|
||||
addProfileCondition,
|
||||
toggleProfileEnabled,
|
||||
toggleProfileTargets,
|
||||
deleteProfile,
|
||||
expandAllProfileSections,
|
||||
collapseAllProfileSections,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { startAutoRefresh, updateTabBadge } from './tabs.js';
|
||||
import {
|
||||
getTargetTypeIcon,
|
||||
ICON_TARGET, ICON_PROFILE, ICON_CLOCK, ICON_WARNING, ICON_OK,
|
||||
ICON_STOP, ICON_STOP_PLAIN, ICON_START, ICON_AUTOSTART, ICON_HELP,
|
||||
ICON_STOP, ICON_STOP_PLAIN, ICON_START, ICON_AUTOSTART, ICON_HELP, ICON_SCENE,
|
||||
} from '../core/icons.js';
|
||||
import { loadScenePresets, renderScenePresetsSection } from './scene-presets.js';
|
||||
|
||||
@@ -269,12 +269,6 @@ function _updateProfilesInPlace(profiles) {
|
||||
badge.textContent = t('profiles.status.inactive');
|
||||
}
|
||||
}
|
||||
const metricVal = card.querySelector('.dashboard-metric-value');
|
||||
if (metricVal) {
|
||||
const cnt = p.target_ids.length;
|
||||
const active = (p.active_target_ids || []).length;
|
||||
metricVal.textContent = p.is_active ? `${active}/${cnt}` : `${cnt}`;
|
||||
}
|
||||
const btn = card.querySelector('.dashboard-target-actions .dashboard-action-btn');
|
||||
if (btn) {
|
||||
btn.className = `dashboard-action-btn ${p.enabled ? 'stop' : 'start'}`;
|
||||
@@ -490,7 +484,8 @@ export async function loadDashboard(forceFullRender = false) {
|
||||
const activeProfiles = profiles.filter(p => p.is_active);
|
||||
const inactiveProfiles = profiles.filter(p => !p.is_active);
|
||||
updateTabBadge('profiles', activeProfiles.length);
|
||||
const profileItems = [...activeProfiles, ...inactiveProfiles].map(p => renderDashboardProfile(p)).join('');
|
||||
const sceneMap = new Map(scenePresets.map(s => [s.id, s]));
|
||||
const profileItems = [...activeProfiles, ...inactiveProfiles].map(p => renderDashboardProfile(p, sceneMap)).join('');
|
||||
|
||||
dynamicHtml += `<div class="dashboard-section">
|
||||
${_sectionHeader('profiles', t('dashboard.section.profiles'), profiles.length)}
|
||||
@@ -669,7 +664,7 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
||||
}
|
||||
}
|
||||
|
||||
function renderDashboardProfile(profile) {
|
||||
function renderDashboardProfile(profile, sceneMap = new Map()) {
|
||||
const isActive = profile.is_active;
|
||||
const isDisabled = !profile.enabled;
|
||||
|
||||
@@ -693,9 +688,9 @@ function renderDashboardProfile(profile) {
|
||||
? `<span class="dashboard-badge-active">${t('profiles.status.active')}</span>`
|
||||
: `<span class="dashboard-badge-stopped">${t('profiles.status.inactive')}</span>`;
|
||||
|
||||
const targetCount = profile.target_ids.length;
|
||||
const activeCount = (profile.active_target_ids || []).length;
|
||||
const targetsInfo = isActive ? `${activeCount}/${targetCount}` : `${targetCount}`;
|
||||
// Scene info
|
||||
const scene = profile.scene_preset_id ? sceneMap.get(profile.scene_preset_id) : null;
|
||||
const sceneName = scene ? escapeHtml(scene.name) : t('profiles.scene.none_selected');
|
||||
|
||||
return `<div class="dashboard-target dashboard-profile dashboard-card-link" data-profile-id="${profile.id}" onclick="if(!event.target.closest('button')){navigateToCard('profiles',null,'profiles','data-profile-id','${profile.id}')}">
|
||||
<div class="dashboard-target-info">
|
||||
@@ -703,15 +698,10 @@ function renderDashboardProfile(profile) {
|
||||
<div>
|
||||
<div class="dashboard-target-name">${escapeHtml(profile.name)}</div>
|
||||
${condSummary ? `<div class="dashboard-target-subtitle">${escapeHtml(condSummary)}</div>` : ''}
|
||||
<div class="dashboard-target-subtitle">${ICON_SCENE} ${sceneName}</div>
|
||||
</div>
|
||||
${statusBadge}
|
||||
</div>
|
||||
<div class="dashboard-target-metrics">
|
||||
<div class="dashboard-metric">
|
||||
<div class="dashboard-metric-value">${targetsInfo}</div>
|
||||
<div class="dashboard-metric-label">${t('dashboard.targets')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-target-actions">
|
||||
<button class="dashboard-action-btn ${profile.enabled ? 'stop' : 'start'}" onclick="dashboardToggleProfile('${profile.id}', ${!profile.enabled})" title="${profile.enabled ? t('profiles.action.disable') : t('profiles.status.active')}">
|
||||
${profile.enabled ? ICON_STOP_PLAIN : ICON_START}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Profiles — profile cards, editor, condition builder, process picker.
|
||||
* Profiles — profile cards, editor, condition builder, process picker, scene selector.
|
||||
*/
|
||||
|
||||
import { apiKey, _profilesCache, set_profilesCache, _profilesLoading, set_profilesLoading } from '../core/state.js';
|
||||
@@ -9,7 +9,10 @@ import { showToast, showConfirm, setTabRefreshing } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
import { CardSection } from '../core/card-sections.js';
|
||||
import { updateTabBadge } from './tabs.js';
|
||||
import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_TARGET, ICON_PROFILE, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO } from '../core/icons.js';
|
||||
import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_PROFILE, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE } from '../core/icons.js';
|
||||
|
||||
// ===== Scene presets cache (shared by both selectors) =====
|
||||
let _scenesCache = [];
|
||||
|
||||
class ProfileEditorModal extends Modal {
|
||||
constructor() { super('profile-editor-modal'); }
|
||||
@@ -20,7 +23,9 @@ class ProfileEditorModal extends Modal {
|
||||
enabled: document.getElementById('profile-editor-enabled').checked.toString(),
|
||||
logic: document.getElementById('profile-editor-logic').value,
|
||||
conditions: JSON.stringify(getProfileEditorConditions()),
|
||||
targets: JSON.stringify(getProfileEditorTargetIds()),
|
||||
scenePresetId: document.getElementById('profile-scene-id').value,
|
||||
deactivationMode: document.getElementById('profile-deactivation-mode').value,
|
||||
deactivationScenePresetId: document.getElementById('profile-fallback-scene-id').value,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -48,24 +53,22 @@ export async function loadProfiles() {
|
||||
setTabRefreshing('profiles-content', true);
|
||||
|
||||
try {
|
||||
const [profilesResp, targetsResp] = await Promise.all([
|
||||
const [profilesResp, scenesResp] = await Promise.all([
|
||||
fetchWithAuth('/profiles'),
|
||||
fetchWithAuth('/picture-targets'),
|
||||
fetchWithAuth('/scene-presets'),
|
||||
]);
|
||||
if (!profilesResp.ok) throw new Error('Failed to load profiles');
|
||||
const data = await profilesResp.json();
|
||||
const targetsData = targetsResp.ok ? await targetsResp.json() : { targets: [] };
|
||||
const allTargets = targetsData.targets || [];
|
||||
// Batch fetch all target states in a single request
|
||||
const batchStatesResp = await fetchWithAuth('/picture-targets/batch/states');
|
||||
const allStates = batchStatesResp.ok ? (await batchStatesResp.json()).states : {};
|
||||
const runningTargetIds = new Set(
|
||||
allTargets.filter(tgt => allStates[tgt.id]?.processing).map(tgt => tgt.id)
|
||||
);
|
||||
const scenesData = scenesResp.ok ? await scenesResp.json() : { presets: [] };
|
||||
_scenesCache = scenesData.presets || [];
|
||||
|
||||
// Build scene name map for card rendering
|
||||
const sceneMap = new Map(_scenesCache.map(s => [s.id, s]));
|
||||
|
||||
set_profilesCache(data.profiles);
|
||||
const activeCount = data.profiles.filter(p => p.is_active).length;
|
||||
updateTabBadge('profiles', activeCount);
|
||||
renderProfiles(data.profiles, runningTargetIds);
|
||||
renderProfiles(data.profiles, sceneMap);
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
console.error('Failed to load profiles:', error);
|
||||
@@ -84,22 +87,21 @@ export function collapseAllProfileSections() {
|
||||
CardSection.collapseAll([csProfiles]);
|
||||
}
|
||||
|
||||
function renderProfiles(profiles, runningTargetIds = new Set()) {
|
||||
function renderProfiles(profiles, sceneMap) {
|
||||
const container = document.getElementById('profiles-content');
|
||||
|
||||
const items = csProfiles.applySortOrder(profiles.map(p => ({ key: p.id, html: createProfileCard(p, runningTargetIds) })));
|
||||
const items = csProfiles.applySortOrder(profiles.map(p => ({ key: p.id, html: createProfileCard(p, sceneMap) })));
|
||||
const toolbar = `<div class="stream-tab-bar"><span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllProfileSections()" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllProfileSections()" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startProfilesTutorial()" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
|
||||
container.innerHTML = toolbar + csProfiles.render(items);
|
||||
csProfiles.bind();
|
||||
|
||||
// Localize data-i18n elements within the profiles container only
|
||||
// (calling global updateAllText() would trigger loadProfiles() again → infinite loop)
|
||||
container.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
el.textContent = t(el.getAttribute('data-i18n'));
|
||||
});
|
||||
}
|
||||
|
||||
function createProfileCard(profile, runningTargetIds = new Set()) {
|
||||
function createProfileCard(profile, sceneMap = new Map()) {
|
||||
const statusClass = !profile.enabled ? 'disabled' : profile.is_active ? 'active' : 'inactive';
|
||||
const statusText = !profile.enabled ? t('profiles.status.disabled') : profile.is_active ? t('profiles.status.active') : t('profiles.status.inactive');
|
||||
|
||||
@@ -136,7 +138,19 @@ function createProfileCard(profile, runningTargetIds = new Set()) {
|
||||
condPills = parts.join(`<span class="profile-logic-label">${logicLabel}</span>`);
|
||||
}
|
||||
|
||||
const targetCountText = `${profile.target_ids.length} target(s)${profile.is_active ? ` (${profile.active_target_ids.length} active)` : ''}`;
|
||||
// Scene info
|
||||
const scene = profile.scene_preset_id ? sceneMap.get(profile.scene_preset_id) : null;
|
||||
const sceneName = scene ? escapeHtml(scene.name) : t('profiles.scene.none_selected');
|
||||
const sceneColor = scene ? scene.color || '#4fc3f7' : '#888';
|
||||
|
||||
// Deactivation mode label
|
||||
let deactivationLabel = '';
|
||||
if (profile.deactivation_mode === 'revert') {
|
||||
deactivationLabel = t('profiles.deactivation_mode.revert');
|
||||
} else if (profile.deactivation_mode === 'fallback_scene') {
|
||||
const fallback = profile.deactivation_scene_preset_id ? sceneMap.get(profile.deactivation_scene_preset_id) : null;
|
||||
deactivationLabel = fallback ? `${t('profiles.deactivation_mode.fallback_scene')}: ${escapeHtml(fallback.name)}` : t('profiles.deactivation_mode.fallback_scene');
|
||||
}
|
||||
|
||||
let lastActivityMeta = '';
|
||||
if (profile.last_activated_at) {
|
||||
@@ -157,20 +171,13 @@ function createProfileCard(profile, runningTargetIds = new Set()) {
|
||||
</div>
|
||||
<div class="card-subtitle">
|
||||
<span class="card-meta">${profile.condition_logic === 'and' ? t('profiles.logic.all') : t('profiles.logic.any')}</span>
|
||||
<span class="card-meta">${ICON_TARGET} ${targetCountText}</span>
|
||||
<span class="card-meta">${ICON_SCENE} <span style="color:${sceneColor}">●</span> ${sceneName}</span>
|
||||
${deactivationLabel ? `<span class="card-meta">${deactivationLabel}</span>` : ''}
|
||||
${lastActivityMeta}
|
||||
</div>
|
||||
<div class="stream-card-props">${condPills}</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-icon btn-secondary" onclick="openProfileEditor('${profile.id}')" title="${t('profiles.edit')}">${ICON_SETTINGS}</button>
|
||||
${profile.target_ids.length > 0 ? (() => {
|
||||
const anyRunning = profile.target_ids.some(id => runningTargetIds.has(id));
|
||||
return `<button class="btn btn-icon ${anyRunning ? 'btn-warning' : 'btn-success'}"
|
||||
onclick="toggleProfileTargets('${profile.id}')"
|
||||
title="${anyRunning ? t('profiles.toggle_all.stop') : t('profiles.toggle_all.start')}">
|
||||
${anyRunning ? ICON_STOP_PLAIN : ICON_START}
|
||||
</button>`;
|
||||
})() : ''}
|
||||
<button class="btn btn-icon ${profile.enabled ? 'btn-warning' : 'btn-success'}" onclick="toggleProfileEnabled('${profile.id}', ${!profile.enabled})" title="${profile.enabled ? t('profiles.action.disable') : t('profiles.status.active')}">
|
||||
${profile.enabled ? ICON_PAUSE : ICON_START}
|
||||
</button>
|
||||
@@ -191,7 +198,18 @@ export async function openProfileEditor(profileId) {
|
||||
errorEl.style.display = 'none';
|
||||
condList.innerHTML = '';
|
||||
|
||||
await loadProfileTargetChecklist([]);
|
||||
// Fetch scenes for selector
|
||||
try {
|
||||
const resp = await fetchWithAuth('/scene-presets');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
_scenesCache = data.presets || [];
|
||||
}
|
||||
} catch { /* use cached */ }
|
||||
|
||||
// Reset deactivation mode
|
||||
document.getElementById('profile-deactivation-mode').value = 'none';
|
||||
document.getElementById('profile-fallback-scene-group').style.display = 'none';
|
||||
|
||||
if (profileId) {
|
||||
titleEl.innerHTML = `${ICON_PROFILE} ${t('profiles.edit')}`;
|
||||
@@ -209,7 +227,13 @@ export async function openProfileEditor(profileId) {
|
||||
addProfileConditionRow(c);
|
||||
}
|
||||
|
||||
await loadProfileTargetChecklist(profile.target_ids);
|
||||
// Scene selector
|
||||
_initSceneSelector('profile-scene', profile.scene_preset_id);
|
||||
|
||||
// Deactivation mode
|
||||
document.getElementById('profile-deactivation-mode').value = profile.deactivation_mode || 'none';
|
||||
_onDeactivationModeChange();
|
||||
_initSceneSelector('profile-fallback-scene', profile.deactivation_scene_preset_id);
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
return;
|
||||
@@ -220,44 +244,132 @@ export async function openProfileEditor(profileId) {
|
||||
nameInput.value = '';
|
||||
enabledInput.checked = true;
|
||||
logicSelect.value = 'or';
|
||||
_initSceneSelector('profile-scene', null);
|
||||
_initSceneSelector('profile-fallback-scene', null);
|
||||
}
|
||||
|
||||
// Wire up deactivation mode change
|
||||
document.getElementById('profile-deactivation-mode').onchange = _onDeactivationModeChange;
|
||||
|
||||
profileModal.open();
|
||||
modal.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
el.textContent = t(el.getAttribute('data-i18n'));
|
||||
});
|
||||
modal.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
||||
el.placeholder = t(el.getAttribute('data-i18n-placeholder'));
|
||||
});
|
||||
profileModal.snapshot();
|
||||
}
|
||||
|
||||
function _onDeactivationModeChange() {
|
||||
const mode = document.getElementById('profile-deactivation-mode').value;
|
||||
document.getElementById('profile-fallback-scene-group').style.display = mode === 'fallback_scene' ? '' : 'none';
|
||||
}
|
||||
|
||||
export async function closeProfileEditorModal() {
|
||||
await profileModal.close();
|
||||
}
|
||||
|
||||
async function loadProfileTargetChecklist(selectedIds) {
|
||||
const container = document.getElementById('profile-targets-list');
|
||||
try {
|
||||
const resp = await fetchWithAuth('/picture-targets');
|
||||
if (!resp.ok) throw new Error('Failed to load targets');
|
||||
const data = await resp.json();
|
||||
const targets = data.targets || [];
|
||||
// ===== Scene selector logic =====
|
||||
|
||||
if (targets.length === 0) {
|
||||
container.innerHTML = `<small class="text-muted">${t('profiles.targets.empty')}</small>`;
|
||||
return;
|
||||
function _initSceneSelector(prefix, selectedId) {
|
||||
const hiddenInput = document.getElementById(`${prefix}-id`);
|
||||
const searchInput = document.getElementById(`${prefix}-search`);
|
||||
const clearBtn = document.getElementById(`${prefix}-clear`);
|
||||
const dropdown = document.getElementById(`${prefix}-dropdown`);
|
||||
|
||||
hiddenInput.value = selectedId || '';
|
||||
|
||||
// Set initial display text
|
||||
if (selectedId) {
|
||||
const scene = _scenesCache.find(s => s.id === selectedId);
|
||||
searchInput.value = scene ? scene.name : '';
|
||||
clearBtn.classList.toggle('visible', true);
|
||||
} else {
|
||||
searchInput.value = '';
|
||||
clearBtn.classList.toggle('visible', false);
|
||||
}
|
||||
|
||||
// Render dropdown items
|
||||
function renderDropdown(filter) {
|
||||
const query = (filter || '').toLowerCase();
|
||||
const filtered = query ? _scenesCache.filter(s => s.name.toLowerCase().includes(query)) : _scenesCache;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
dropdown.innerHTML = `<div class="scene-selector-empty">${t('profiles.scene.none_available')}</div>`;
|
||||
} else {
|
||||
dropdown.innerHTML = filtered.map(s => {
|
||||
const selected = s.id === hiddenInput.value ? ' selected' : '';
|
||||
return `<div class="scene-selector-item${selected}" data-scene-id="${s.id}"><span class="scene-color-dot" style="background:${escapeHtml(s.color || '#4fc3f7')}"></span>${escapeHtml(s.name)}</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
container.innerHTML = targets.map(tgt => {
|
||||
const checked = selectedIds.includes(tgt.id) ? 'checked' : '';
|
||||
return `<label class="profile-target-item">
|
||||
<input type="checkbox" value="${tgt.id}" ${checked}>
|
||||
<span>${escapeHtml(tgt.name)}</span>
|
||||
</label>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
container.innerHTML = `<small class="text-muted">${e.message}</small>`;
|
||||
// Attach click handlers
|
||||
dropdown.querySelectorAll('.scene-selector-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const id = item.dataset.sceneId;
|
||||
const scene = _scenesCache.find(s => s.id === id);
|
||||
hiddenInput.value = id;
|
||||
searchInput.value = scene ? scene.name : '';
|
||||
clearBtn.classList.toggle('visible', true);
|
||||
dropdown.classList.remove('open');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Show dropdown on focus/click
|
||||
searchInput.onfocus = () => {
|
||||
renderDropdown(searchInput.value);
|
||||
dropdown.classList.add('open');
|
||||
};
|
||||
|
||||
searchInput.oninput = () => {
|
||||
renderDropdown(searchInput.value);
|
||||
dropdown.classList.add('open');
|
||||
// If text doesn't match any scene, clear the hidden input
|
||||
const exactMatch = _scenesCache.find(s => s.name.toLowerCase() === searchInput.value.toLowerCase());
|
||||
if (!exactMatch) {
|
||||
hiddenInput.value = '';
|
||||
clearBtn.classList.toggle('visible', !!searchInput.value);
|
||||
}
|
||||
};
|
||||
|
||||
searchInput.onkeydown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
// Select first visible item
|
||||
const first = dropdown.querySelector('.scene-selector-item');
|
||||
if (first) first.click();
|
||||
} else if (e.key === 'Escape') {
|
||||
dropdown.classList.remove('open');
|
||||
searchInput.blur();
|
||||
}
|
||||
};
|
||||
|
||||
// Clear button
|
||||
clearBtn.onclick = () => {
|
||||
hiddenInput.value = '';
|
||||
searchInput.value = '';
|
||||
clearBtn.classList.remove('visible');
|
||||
dropdown.classList.remove('open');
|
||||
};
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
const selectorEl = searchInput.closest('.scene-selector');
|
||||
// Remove old listener if any (re-init)
|
||||
if (selectorEl._outsideClickHandler) {
|
||||
document.removeEventListener('click', selectorEl._outsideClickHandler);
|
||||
}
|
||||
selectorEl._outsideClickHandler = (e) => {
|
||||
if (!selectorEl.contains(e.target)) {
|
||||
dropdown.classList.remove('open');
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', selectorEl._outsideClickHandler);
|
||||
}
|
||||
|
||||
// ===== Condition editor =====
|
||||
|
||||
export function addProfileCondition() {
|
||||
addProfileConditionRow({ condition_type: 'application', apps: [], match_type: 'running' });
|
||||
}
|
||||
@@ -444,7 +556,7 @@ function renderProcessPicker(picker, processes, existing) {
|
||||
}
|
||||
listEl.innerHTML = processes.map(p => {
|
||||
const added = existing.has(p.toLowerCase());
|
||||
return `<div class="process-picker-item${added ? ' added' : ''}" data-process="${escapeHtml(p)}">${escapeHtml(p)}${added ? ' ✓' : ''}</div>`;
|
||||
return `<div class="process-picker-item${added ? ' added' : ''}" data-process="${escapeHtml(p)}">${escapeHtml(p)}${added ? ' \u2713' : ''}</div>`;
|
||||
}).join('');
|
||||
|
||||
listEl.querySelectorAll('.process-picker-item:not(.added)').forEach(item => {
|
||||
@@ -455,7 +567,7 @@ function renderProcessPicker(picker, processes, existing) {
|
||||
const current = textarea.value.trim();
|
||||
textarea.value = current ? current + '\n' + proc : proc;
|
||||
item.classList.add('added');
|
||||
item.textContent = proc + ' ✓';
|
||||
item.textContent = proc + ' \u2713';
|
||||
picker._existing.add(proc.toLowerCase());
|
||||
});
|
||||
});
|
||||
@@ -509,11 +621,6 @@ function getProfileEditorConditions() {
|
||||
return conditions;
|
||||
}
|
||||
|
||||
function getProfileEditorTargetIds() {
|
||||
const checkboxes = document.querySelectorAll('#profile-targets-list input[type="checkbox"]:checked');
|
||||
return Array.from(checkboxes).map(cb => cb.value);
|
||||
}
|
||||
|
||||
export async function saveProfileEditor() {
|
||||
const idInput = document.getElementById('profile-editor-id');
|
||||
const nameInput = document.getElementById('profile-editor-name');
|
||||
@@ -531,7 +638,9 @@ export async function saveProfileEditor() {
|
||||
enabled: enabledInput.checked,
|
||||
condition_logic: logicSelect.value,
|
||||
conditions: getProfileEditorConditions(),
|
||||
target_ids: getProfileEditorTargetIds(),
|
||||
scene_preset_id: document.getElementById('profile-scene-id').value || null,
|
||||
deactivation_mode: document.getElementById('profile-deactivation-mode').value,
|
||||
deactivation_scene_preset_id: document.getElementById('profile-fallback-scene-id').value || null,
|
||||
};
|
||||
|
||||
const profileId = idInput.value;
|
||||
@@ -557,28 +666,6 @@ export async function saveProfileEditor() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleProfileTargets(profileId) {
|
||||
try {
|
||||
const profileResp = await fetchWithAuth(`/profiles/${profileId}`);
|
||||
if (!profileResp.ok) throw new Error('Failed to load profile');
|
||||
const profile = await profileResp.json();
|
||||
// Batch fetch all target states to determine which are running
|
||||
const batchResp = await fetchWithAuth('/picture-targets/batch/states');
|
||||
const allStates = batchResp.ok ? (await batchResp.json()).states : {};
|
||||
const runningSet = new Set(
|
||||
profile.target_ids.filter(id => allStates[id]?.processing)
|
||||
);
|
||||
const shouldStop = profile.target_ids.some(id => runningSet.has(id));
|
||||
await Promise.all(profile.target_ids.map(id =>
|
||||
fetchWithAuth(`/picture-targets/${id}/${shouldStop ? 'stop' : 'start'}`, { method: 'POST' }).catch(() => {})
|
||||
));
|
||||
loadProfiles();
|
||||
} catch (e) {
|
||||
if (e.isAuth) return;
|
||||
showToast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleProfileEnabled(profileId, enable) {
|
||||
try {
|
||||
const action = enable ? 'enable' : 'disable';
|
||||
|
||||
Reference in New Issue
Block a user