Add "Always" condition type to profiles
- Add AlwaysCondition model and evaluation (always returns true) - Add condition type selector (Always/Application) in profile editor - Show condition type pill on profile cards - Fix misleading empty-conditions text (was "never activate", actually always active) - Add i18n keys for Always condition (en + ru) - Add CSS for condition type selector and description Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ from datetime import datetime, timezone
|
||||
from typing import Dict, Optional, Set
|
||||
|
||||
from wled_controller.core.profiles.platform_detector import PlatformDetector
|
||||
from wled_controller.storage.profile import ApplicationCondition, Condition, Profile
|
||||
from wled_controller.storage.profile import AlwaysCondition, ApplicationCondition, Condition, Profile
|
||||
from wled_controller.storage.profile_store import ProfileStore
|
||||
from wled_controller.utils import get_logger
|
||||
|
||||
@@ -158,6 +158,8 @@ class ProfileEngine:
|
||||
topmost_proc: Optional[str], topmost_fullscreen: bool,
|
||||
fullscreen_procs: Set[str],
|
||||
) -> bool:
|
||||
if isinstance(condition, AlwaysCondition):
|
||||
return True
|
||||
if isinstance(condition, ApplicationCondition):
|
||||
return self._evaluate_app_condition(condition, running_procs, topmost_proc, topmost_fullscreen, fullscreen_procs)
|
||||
return False
|
||||
|
||||
@@ -48,6 +48,22 @@
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.condition-type-select {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.condition-always-desc {
|
||||
display: block;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.btn-remove-condition {
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
@@ -92,6 +92,9 @@ function createProfileCard(profile, runningTargetIds = new Set()) {
|
||||
condPills = `<span class="stream-card-prop">${t('profiles.conditions.empty')}</span>`;
|
||||
} else {
|
||||
const parts = profile.conditions.map(c => {
|
||||
if (c.condition_type === 'always') {
|
||||
return `<span class="stream-card-prop">✅ ${t('profiles.condition.always')}</span>`;
|
||||
}
|
||||
if (c.condition_type === 'application') {
|
||||
const apps = (c.apps || []).join(', ');
|
||||
const matchLabel = t('profiles.condition.application.match_type.' + (c.match_type || 'running'));
|
||||
@@ -233,18 +236,33 @@ function addProfileConditionRow(condition) {
|
||||
const list = document.getElementById('profile-conditions-list');
|
||||
const row = document.createElement('div');
|
||||
row.className = 'profile-condition-row';
|
||||
|
||||
const appsValue = (condition.apps || []).join('\n');
|
||||
const matchType = condition.match_type || 'running';
|
||||
const condType = condition.condition_type || 'application';
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="condition-header">
|
||||
<span class="condition-type-label">${t('profiles.condition.application')}</span>
|
||||
<select class="condition-type-select">
|
||||
<option value="always" ${condType === 'always' ? 'selected' : ''}>${t('profiles.condition.always')}</option>
|
||||
<option value="application" ${condType === 'application' ? 'selected' : ''}>${t('profiles.condition.application')}</option>
|
||||
</select>
|
||||
<button type="button" class="btn-remove-condition" onclick="this.closest('.profile-condition-row').remove()" title="Remove">✕</button>
|
||||
</div>
|
||||
<div class="condition-fields-container"></div>
|
||||
`;
|
||||
|
||||
const typeSelect = row.querySelector('.condition-type-select');
|
||||
const container = row.querySelector('.condition-fields-container');
|
||||
|
||||
function renderFields(type, data) {
|
||||
if (type === 'always') {
|
||||
container.innerHTML = `<small class="condition-always-desc">${t('profiles.condition.always.hint')}</small>`;
|
||||
return;
|
||||
}
|
||||
const appsValue = (data.apps || []).join('\n');
|
||||
const matchType = data.match_type || 'running';
|
||||
container.innerHTML = `
|
||||
<div class="condition-fields">
|
||||
<div class="condition-field">
|
||||
<label data-i18n="profiles.condition.application.match_type">${t('profiles.condition.application.match_type')}</label>
|
||||
<label>${t('profiles.condition.application.match_type')}</label>
|
||||
<select class="condition-match-type">
|
||||
<option value="running" ${matchType === 'running' ? 'selected' : ''}>${t('profiles.condition.application.match_type.running')}</option>
|
||||
<option value="topmost" ${matchType === 'topmost' ? 'selected' : ''}>${t('profiles.condition.application.match_type.topmost')}</option>
|
||||
@@ -254,7 +272,7 @@ function addProfileConditionRow(condition) {
|
||||
</div>
|
||||
<div class="condition-field">
|
||||
<div class="condition-apps-header">
|
||||
<label data-i18n="profiles.condition.application.apps">${t('profiles.condition.application.apps')}</label>
|
||||
<label>${t('profiles.condition.application.apps')}</label>
|
||||
<button type="button" class="btn-browse-apps" title="${t('profiles.condition.application.browse')}">${t('profiles.condition.application.browse')}</button>
|
||||
</div>
|
||||
<textarea class="condition-apps" rows="3" placeholder="firefox.exe chrome.exe">${escapeHtml(appsValue)}</textarea>
|
||||
@@ -265,13 +283,17 @@ function addProfileConditionRow(condition) {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const browseBtn = row.querySelector('.btn-browse-apps');
|
||||
const picker = row.querySelector('.process-picker');
|
||||
const browseBtn = container.querySelector('.btn-browse-apps');
|
||||
const picker = container.querySelector('.process-picker');
|
||||
browseBtn.addEventListener('click', () => toggleProcessPicker(picker, row));
|
||||
|
||||
const searchInput = row.querySelector('.process-picker-search');
|
||||
const searchInput = container.querySelector('.process-picker-search');
|
||||
searchInput.addEventListener('input', () => filterProcessPicker(picker));
|
||||
}
|
||||
|
||||
renderFields(condType, condition);
|
||||
typeSelect.addEventListener('change', () => {
|
||||
renderFields(typeSelect.value, { apps: [], match_type: 'running' });
|
||||
});
|
||||
|
||||
list.appendChild(row);
|
||||
}
|
||||
@@ -340,10 +362,16 @@ function getProfileEditorConditions() {
|
||||
const rows = document.querySelectorAll('#profile-conditions-list .profile-condition-row');
|
||||
const conditions = [];
|
||||
rows.forEach(row => {
|
||||
const typeSelect = row.querySelector('.condition-type-select');
|
||||
const condType = typeSelect ? typeSelect.value : 'application';
|
||||
if (condType === 'always') {
|
||||
conditions.push({ condition_type: 'always' });
|
||||
} else {
|
||||
const matchType = row.querySelector('.condition-match-type').value;
|
||||
const appsText = row.querySelector('.condition-apps').value.trim();
|
||||
const apps = appsText ? appsText.split('\n').map(a => a.trim()).filter(Boolean) : [];
|
||||
conditions.push({ condition_type: 'application', apps, match_type: matchType });
|
||||
}
|
||||
});
|
||||
return conditions;
|
||||
}
|
||||
|
||||
@@ -519,7 +519,9 @@
|
||||
"profiles.conditions": "Conditions:",
|
||||
"profiles.conditions.hint": "Rules that determine when this profile activates",
|
||||
"profiles.conditions.add": "Add Condition",
|
||||
"profiles.conditions.empty": "No conditions \u2014 profile will never activate automatically",
|
||||
"profiles.conditions.empty": "No conditions \u2014 profile is always active when enabled",
|
||||
"profiles.condition.always": "Always",
|
||||
"profiles.condition.always.hint": "Profile activates immediately when enabled and stays active. Use this to auto-start targets on server startup.",
|
||||
"profiles.condition.application": "Application",
|
||||
"profiles.condition.application.apps": "Applications:",
|
||||
"profiles.condition.application.apps.hint": "Process names, one per line (e.g. firefox.exe)",
|
||||
|
||||
@@ -519,7 +519,9 @@
|
||||
"profiles.conditions": "Условия:",
|
||||
"profiles.conditions.hint": "Правила, определяющие когда профиль активируется",
|
||||
"profiles.conditions.add": "Добавить условие",
|
||||
"profiles.conditions.empty": "Нет условий \u2014 профиль не активируется автоматически",
|
||||
"profiles.conditions.empty": "Нет условий \u2014 профиль всегда активен когда включён",
|
||||
"profiles.condition.always": "Всегда",
|
||||
"profiles.condition.always.hint": "Профиль активируется сразу при включении и остаётся активным. Используйте для автозапуска целей при старте сервера.",
|
||||
"profiles.condition.application": "Приложение",
|
||||
"profiles.condition.application.apps": "Приложения:",
|
||||
"profiles.condition.application.apps.hint": "Имена процессов, по одному на строку (например firefox.exe)",
|
||||
|
||||
@@ -18,11 +18,24 @@ class Condition:
|
||||
def from_dict(cls, data: dict) -> "Condition":
|
||||
"""Factory: dispatch to the correct subclass."""
|
||||
ct = data.get("condition_type", "")
|
||||
if ct == "always":
|
||||
return AlwaysCondition.from_dict(data)
|
||||
if ct == "application":
|
||||
return ApplicationCondition.from_dict(data)
|
||||
raise ValueError(f"Unknown condition type: {ct}")
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlwaysCondition(Condition):
|
||||
"""Always-true condition — profile activates unconditionally when enabled."""
|
||||
|
||||
condition_type: str = "always"
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "AlwaysCondition":
|
||||
return cls()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ApplicationCondition(Condition):
|
||||
"""Activate when specified applications are running or topmost."""
|
||||
|
||||
Reference in New Issue
Block a user