Unify process picker, improve notification CSS editor, remove notification led_count
- Extract shared process picker module (core/process-picker.js) used by both automation conditions and notification CSS app filter - Remove led_count property from notification CSS source (backend + frontend) - Replace comma-separated app filter with newline-separated textarea + browse - Inline color cycle add button (+) into the color row - Fix notification app color layout to horizontal rows Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -70,8 +70,8 @@ class NotificationColorStripStream(ColorStripStream):
|
|||||||
self._app_colors = dict(getattr(source, "app_colors", {}))
|
self._app_colors = dict(getattr(source, "app_colors", {}))
|
||||||
self._app_filter_mode = getattr(source, "app_filter_mode", "off")
|
self._app_filter_mode = getattr(source, "app_filter_mode", "off")
|
||||||
self._app_filter_list = list(getattr(source, "app_filter_list", []))
|
self._app_filter_list = list(getattr(source, "app_filter_list", []))
|
||||||
self._auto_size = not source.led_count
|
self._auto_size = not getattr(source, "led_count", 0)
|
||||||
self._led_count = source.led_count if source.led_count and source.led_count > 0 else 1
|
self._led_count = getattr(source, "led_count", 0) if getattr(source, "led_count", 0) > 0 else 1
|
||||||
with self._colors_lock:
|
with self._colors_lock:
|
||||||
self._colors: Optional[np.ndarray] = np.zeros((self._led_count, 3), dtype=np.uint8)
|
self._colors: Optional[np.ndarray] = np.zeros((self._led_count, 3), dtype=np.uint8)
|
||||||
|
|
||||||
|
|||||||
@@ -912,6 +912,48 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.color-cycle-add-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 28px;
|
||||||
|
min-width: unset;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Notification app color mappings ─────────────────────────── */
|
||||||
|
|
||||||
|
.notif-app-color-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notif-app-color-row .notif-app-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notif-app-color-row .notif-app-color {
|
||||||
|
width: 36px;
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notif-app-color-remove {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0 4px;
|
||||||
|
min-width: unset;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Composite layer editor ────────────────────────────────────── */
|
/* ── Composite layer editor ────────────────────────────────────── */
|
||||||
|
|
||||||
#composite-layers-list {
|
#composite-layers-list {
|
||||||
|
|||||||
97
server/src/wled_controller/static/js/core/process-picker.js
Normal file
97
server/src/wled_controller/static/js/core/process-picker.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Shared process picker — reusable UI for browsing running processes
|
||||||
|
* and adding them to a textarea (one app per line).
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { attachProcessPicker } from '../core/process-picker.js';
|
||||||
|
* attachProcessPicker(containerEl, textareaEl);
|
||||||
|
*
|
||||||
|
* The container must already contain:
|
||||||
|
* <button class="btn-browse-apps">Browse</button>
|
||||||
|
* <div class="process-picker" style="display:none">
|
||||||
|
* <input class="process-picker-search" placeholder="..." autocomplete="off">
|
||||||
|
* <div class="process-picker-list"></div>
|
||||||
|
* </div>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { fetchWithAuth } from './api.js';
|
||||||
|
import { t } from './i18n.js';
|
||||||
|
import { escapeHtml } from './api.js';
|
||||||
|
|
||||||
|
function renderList(picker, processes, existing) {
|
||||||
|
const listEl = picker.querySelector('.process-picker-list');
|
||||||
|
if (processes.length === 0) {
|
||||||
|
listEl.innerHTML = `<div class="process-picker-loading">${t('automations.condition.application.no_processes')}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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 ? ' \u2713' : ''}</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
listEl.querySelectorAll('.process-picker-item:not(.added)').forEach(item => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
const proc = item.dataset.process;
|
||||||
|
const textarea = picker._textarea;
|
||||||
|
const current = textarea.value.trim();
|
||||||
|
textarea.value = current ? current + '\n' + proc : proc;
|
||||||
|
item.classList.add('added');
|
||||||
|
item.textContent = proc + ' \u2713';
|
||||||
|
picker._existing.add(proc.toLowerCase());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggle(picker) {
|
||||||
|
if (picker.style.display !== 'none') {
|
||||||
|
picker.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listEl = picker.querySelector('.process-picker-list');
|
||||||
|
const searchEl = picker.querySelector('.process-picker-search');
|
||||||
|
searchEl.value = '';
|
||||||
|
listEl.innerHTML = `<div class="process-picker-loading">${t('common.loading')}</div>`;
|
||||||
|
picker.style.display = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetchWithAuth('/system/processes');
|
||||||
|
if (!resp.ok) throw new Error('Failed to fetch processes');
|
||||||
|
const data = await resp.json();
|
||||||
|
|
||||||
|
const existing = new Set(
|
||||||
|
picker._textarea.value.split('\n').map(a => a.trim().toLowerCase()).filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
|
picker._processes = data.processes;
|
||||||
|
picker._existing = existing;
|
||||||
|
renderList(picker, data.processes, existing);
|
||||||
|
searchEl.focus();
|
||||||
|
} catch (e) {
|
||||||
|
listEl.innerHTML = `<div class="process-picker-loading" style="color:var(--danger-color)">${e.message}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filter(picker) {
|
||||||
|
const query = picker.querySelector('.process-picker-search').value.toLowerCase();
|
||||||
|
const filtered = (picker._processes || []).filter(p => p.includes(query));
|
||||||
|
renderList(picker, filtered, picker._existing || new Set());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wire up a process picker inside `containerEl` to feed into `textareaEl`.
|
||||||
|
* containerEl must contain .btn-browse-apps, .process-picker, .process-picker-search.
|
||||||
|
*/
|
||||||
|
export function attachProcessPicker(containerEl, textareaEl) {
|
||||||
|
const browseBtn = containerEl.querySelector('.btn-browse-apps');
|
||||||
|
const picker = containerEl.querySelector('.process-picker');
|
||||||
|
if (!browseBtn || !picker) return;
|
||||||
|
|
||||||
|
picker._textarea = textareaEl;
|
||||||
|
browseBtn.addEventListener('click', () => toggle(picker));
|
||||||
|
|
||||||
|
const searchInput = picker.querySelector('.process-picker-search');
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener('input', () => filter(picker));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,8 +9,9 @@ import { showToast, showConfirm, setTabRefreshing } from '../core/ui.js';
|
|||||||
import { Modal } from '../core/modal.js';
|
import { Modal } from '../core/modal.js';
|
||||||
import { CardSection } from '../core/card-sections.js';
|
import { CardSection } from '../core/card-sections.js';
|
||||||
import { updateTabBadge } from './tabs.js';
|
import { updateTabBadge } from './tabs.js';
|
||||||
import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE } from '../core/icons.js';
|
import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE, ICON_CLONE } from '../core/icons.js';
|
||||||
import { wrapCard } from '../core/card-colors.js';
|
import { wrapCard } from '../core/card-colors.js';
|
||||||
|
import { attachProcessPicker } from '../core/process-picker.js';
|
||||||
import { csScenes, createSceneCard } from './scene-presets.js';
|
import { csScenes, createSceneCard } from './scene-presets.js';
|
||||||
|
|
||||||
class AutomationEditorModal extends Modal {
|
class AutomationEditorModal extends Modal {
|
||||||
@@ -558,11 +559,8 @@ function addAutomationConditionRow(condition) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
const browseBtn = container.querySelector('.btn-browse-apps');
|
const textarea = container.querySelector('.condition-apps');
|
||||||
const picker = container.querySelector('.process-picker');
|
attachProcessPicker(container, textarea);
|
||||||
browseBtn.addEventListener('click', () => toggleProcessPicker(picker, row));
|
|
||||||
const searchInput = container.querySelector('.process-picker-search');
|
|
||||||
searchInput.addEventListener('input', () => filterProcessPicker(picker));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFields(condType, condition);
|
renderFields(condType, condition);
|
||||||
@@ -573,65 +571,7 @@ function addAutomationConditionRow(condition) {
|
|||||||
list.appendChild(row);
|
list.appendChild(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleProcessPicker(picker, row) {
|
|
||||||
if (picker.style.display !== 'none') {
|
|
||||||
picker.style.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const listEl = picker.querySelector('.process-picker-list');
|
|
||||||
const searchEl = picker.querySelector('.process-picker-search');
|
|
||||||
searchEl.value = '';
|
|
||||||
listEl.innerHTML = `<div class="process-picker-loading">${t('common.loading')}</div>`;
|
|
||||||
picker.style.display = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetchWithAuth('/system/processes');
|
|
||||||
if (!resp.ok) throw new Error('Failed to fetch processes');
|
|
||||||
const data = await resp.json();
|
|
||||||
|
|
||||||
const textarea = row.querySelector('.condition-apps');
|
|
||||||
const existing = new Set(textarea.value.split('\n').map(a => a.trim().toLowerCase()).filter(Boolean));
|
|
||||||
|
|
||||||
picker._processes = data.processes;
|
|
||||||
picker._existing = existing;
|
|
||||||
renderProcessPicker(picker, data.processes, existing);
|
|
||||||
searchEl.focus();
|
|
||||||
} catch (e) {
|
|
||||||
listEl.innerHTML = `<div class="process-picker-loading" style="color:var(--danger-color)">${e.message}</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderProcessPicker(picker, processes, existing) {
|
|
||||||
const listEl = picker.querySelector('.process-picker-list');
|
|
||||||
if (processes.length === 0) {
|
|
||||||
listEl.innerHTML = `<div class="process-picker-loading">${t('automations.condition.application.no_processes')}</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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 ? ' \u2713' : ''}</div>`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
listEl.querySelectorAll('.process-picker-item:not(.added)').forEach(item => {
|
|
||||||
item.addEventListener('click', () => {
|
|
||||||
const proc = item.dataset.process;
|
|
||||||
const row = picker.closest('.automation-condition-row');
|
|
||||||
const textarea = row.querySelector('.condition-apps');
|
|
||||||
const current = textarea.value.trim();
|
|
||||||
textarea.value = current ? current + '\n' + proc : proc;
|
|
||||||
item.classList.add('added');
|
|
||||||
item.textContent = proc + ' \u2713';
|
|
||||||
picker._existing.add(proc.toLowerCase());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterProcessPicker(picker) {
|
|
||||||
const query = picker.querySelector('.process-picker-search').value.toLowerCase();
|
|
||||||
const filtered = (picker._processes || []).filter(p => p.includes(query));
|
|
||||||
renderProcessPicker(picker, filtered, picker._existing || new Set());
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAutomationEditorConditions() {
|
function getAutomationEditorConditions() {
|
||||||
const rows = document.querySelectorAll('#automation-conditions-list .automation-condition-row');
|
const rows = document.querySelectorAll('#automation-conditions-list .automation-condition-row');
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK, ICON_BELL,
|
ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK, ICON_BELL,
|
||||||
} from '../core/icons.js';
|
} from '../core/icons.js';
|
||||||
import { wrapCard } from '../core/card-colors.js';
|
import { wrapCard } from '../core/card-colors.js';
|
||||||
|
import { attachProcessPicker } from '../core/process-picker.js';
|
||||||
|
|
||||||
class CSSEditorModal extends Modal {
|
class CSSEditorModal extends Modal {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -117,7 +118,7 @@ export function onCSSTypeChange() {
|
|||||||
_syncAnimationSpeedState();
|
_syncAnimationSpeedState();
|
||||||
|
|
||||||
// LED count — only shown for picture, api_input, notification
|
// LED count — only shown for picture, api_input, notification
|
||||||
const hasLedCount = ['picture', 'api_input', 'notification'];
|
const hasLedCount = ['picture', 'api_input'];
|
||||||
document.getElementById('css-editor-led-count-group').style.display =
|
document.getElementById('css-editor-led-count-group').style.display =
|
||||||
hasLedCount.includes(type) ? '' : 'none';
|
hasLedCount.includes(type) ? '' : 'none';
|
||||||
|
|
||||||
@@ -264,7 +265,7 @@ function _colorCycleRenderList() {
|
|||||||
onclick="colorCycleRemoveColor(${i})">✕</button>`
|
onclick="colorCycleRemoveColor(${i})">✕</button>`
|
||||||
: `<div style="height:14px"></div>`}
|
: `<div style="height:14px"></div>`}
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('') + `<div class="color-cycle-item"><button type="button" class="btn btn-secondary color-cycle-add-btn" onclick="colorCycleAddColor()">+</button></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function colorCycleAddColor() {
|
export function colorCycleAddColor() {
|
||||||
@@ -597,10 +598,10 @@ function _notificationAppColorsRenderList() {
|
|||||||
const list = document.getElementById('notification-app-colors-list');
|
const list = document.getElementById('notification-app-colors-list');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
list.innerHTML = _notificationAppColors.map((entry, i) => `
|
list.innerHTML = _notificationAppColors.map((entry, i) => `
|
||||||
<div class="color-cycle-item">
|
<div class="notif-app-color-row">
|
||||||
<input type="text" class="notif-app-name" data-idx="${i}" value="${escapeHtml(entry.app)}" placeholder="App name" style="flex:1">
|
<input type="text" class="notif-app-name" data-idx="${i}" value="${escapeHtml(entry.app)}" placeholder="App name">
|
||||||
<input type="color" class="notif-app-color" data-idx="${i}" value="${entry.color}">
|
<input type="color" class="notif-app-color" data-idx="${i}" value="${entry.color}">
|
||||||
<button type="button" class="btn btn-secondary color-cycle-remove-btn"
|
<button type="button" class="btn btn-secondary notif-app-color-remove"
|
||||||
onclick="notificationRemoveAppColor(${i})">✕</button>
|
onclick="notificationRemoveAppColor(${i})">✕</button>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
@@ -647,8 +648,9 @@ function _loadNotificationState(css) {
|
|||||||
document.getElementById('css-editor-notification-duration-val').textContent = dur;
|
document.getElementById('css-editor-notification-duration-val').textContent = dur;
|
||||||
document.getElementById('css-editor-notification-default-color').value = css.default_color || '#ffffff';
|
document.getElementById('css-editor-notification-default-color').value = css.default_color || '#ffffff';
|
||||||
document.getElementById('css-editor-notification-filter-mode').value = css.app_filter_mode || 'off';
|
document.getElementById('css-editor-notification-filter-mode').value = css.app_filter_mode || 'off';
|
||||||
document.getElementById('css-editor-notification-filter-list').value = (css.app_filter_list || []).join(', ');
|
document.getElementById('css-editor-notification-filter-list').value = (css.app_filter_list || []).join('\n');
|
||||||
onNotificationFilterModeChange();
|
onNotificationFilterModeChange();
|
||||||
|
_attachNotificationProcessPicker();
|
||||||
|
|
||||||
// App colors dict → list
|
// App colors dict → list
|
||||||
const ac = css.app_colors || {};
|
const ac = css.app_colors || {};
|
||||||
@@ -666,11 +668,18 @@ function _resetNotificationState() {
|
|||||||
document.getElementById('css-editor-notification-filter-mode').value = 'off';
|
document.getElementById('css-editor-notification-filter-mode').value = 'off';
|
||||||
document.getElementById('css-editor-notification-filter-list').value = '';
|
document.getElementById('css-editor-notification-filter-list').value = '';
|
||||||
onNotificationFilterModeChange();
|
onNotificationFilterModeChange();
|
||||||
|
_attachNotificationProcessPicker();
|
||||||
_notificationAppColors = [];
|
_notificationAppColors = [];
|
||||||
_notificationAppColorsRenderList();
|
_notificationAppColorsRenderList();
|
||||||
_showNotificationEndpoint(null);
|
_showNotificationEndpoint(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _attachNotificationProcessPicker() {
|
||||||
|
const container = document.getElementById('css-editor-notification-filter-picker-container');
|
||||||
|
const textarea = document.getElementById('css-editor-notification-filter-list');
|
||||||
|
if (container && textarea) attachProcessPicker(container, textarea);
|
||||||
|
}
|
||||||
|
|
||||||
function _showNotificationEndpoint(cssId) {
|
function _showNotificationEndpoint(cssId) {
|
||||||
const el = document.getElementById('css-editor-notification-endpoint');
|
const el = document.getElementById('css-editor-notification-endpoint');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -811,7 +820,6 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
|||||||
<span style="display:inline-block;width:14px;height:14px;background:${defColor};border:1px solid #888;border-radius:2px;vertical-align:middle;margin-right:4px"></span>${defColor.toUpperCase()}
|
<span style="display:inline-block;width:14px;height:14px;background:${defColor};border:1px solid #888;border-radius:2px;vertical-align:middle;margin-right:4px"></span>${defColor.toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
${appCount > 0 ? `<span class="stream-card-prop">${ICON_PALETTE} ${appCount} ${t('color_strip.notification.app_count')}</span>` : ''}
|
${appCount > 0 ? `<span class="stream-card-prop">${ICON_PALETTE} ${appCount} ${t('color_strip.notification.app_count')}</span>` : ''}
|
||||||
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${source.led_count}</span>` : ''}
|
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
const ps = pictureSourceMap && pictureSourceMap[source.picture_source_id];
|
const ps = pictureSourceMap && pictureSourceMap[source.picture_source_id];
|
||||||
@@ -1155,7 +1163,7 @@ export async function saveCSSEditor() {
|
|||||||
if (!cssId) payload.source_type = 'api_input';
|
if (!cssId) payload.source_type = 'api_input';
|
||||||
} else if (sourceType === 'notification') {
|
} else if (sourceType === 'notification') {
|
||||||
const filterList = document.getElementById('css-editor-notification-filter-list').value
|
const filterList = document.getElementById('css-editor-notification-filter-list').value
|
||||||
.split(',').map(s => s.trim()).filter(Boolean);
|
.split('\n').map(s => s.trim()).filter(Boolean);
|
||||||
payload = {
|
payload = {
|
||||||
name,
|
name,
|
||||||
notification_effect: document.getElementById('css-editor-notification-effect').value,
|
notification_effect: document.getElementById('css-editor-notification-effect').value,
|
||||||
@@ -1164,7 +1172,6 @@ export async function saveCSSEditor() {
|
|||||||
app_filter_mode: document.getElementById('css-editor-notification-filter-mode').value,
|
app_filter_mode: document.getElementById('css-editor-notification-filter-mode').value,
|
||||||
app_filter_list: filterList,
|
app_filter_list: filterList,
|
||||||
app_colors: _notificationGetAppColorsDict(),
|
app_colors: _notificationGetAppColorsDict(),
|
||||||
led_count: parseInt(document.getElementById('css-editor-led-count').value) || 0,
|
|
||||||
};
|
};
|
||||||
if (!cssId) payload.source_type = 'notification';
|
if (!cssId) payload.source_type = 'notification';
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -826,8 +826,8 @@
|
|||||||
"color_strip.notification.filter_mode.whitelist": "Whitelist",
|
"color_strip.notification.filter_mode.whitelist": "Whitelist",
|
||||||
"color_strip.notification.filter_mode.blacklist": "Blacklist",
|
"color_strip.notification.filter_mode.blacklist": "Blacklist",
|
||||||
"color_strip.notification.filter_list": "App List:",
|
"color_strip.notification.filter_list": "App List:",
|
||||||
"color_strip.notification.filter_list.hint": "Comma-separated app names for the filter.",
|
"color_strip.notification.filter_list.hint": "One app name per line. Use Browse to pick from running processes.",
|
||||||
"color_strip.notification.filter_list.placeholder": "Discord, Slack, Telegram",
|
"color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram",
|
||||||
"color_strip.notification.app_colors": "App Colors",
|
"color_strip.notification.app_colors": "App Colors",
|
||||||
"color_strip.notification.app_colors.label": "Color Mappings:",
|
"color_strip.notification.app_colors.label": "Color Mappings:",
|
||||||
"color_strip.notification.app_colors.hint": "Per-app color overrides. Each row maps an app name to a specific notification color.",
|
"color_strip.notification.app_colors.hint": "Per-app color overrides. Each row maps an app name to a specific notification color.",
|
||||||
|
|||||||
@@ -826,8 +826,8 @@
|
|||||||
"color_strip.notification.filter_mode.whitelist": "Белый список",
|
"color_strip.notification.filter_mode.whitelist": "Белый список",
|
||||||
"color_strip.notification.filter_mode.blacklist": "Чёрный список",
|
"color_strip.notification.filter_mode.blacklist": "Чёрный список",
|
||||||
"color_strip.notification.filter_list": "Список приложений:",
|
"color_strip.notification.filter_list": "Список приложений:",
|
||||||
"color_strip.notification.filter_list.hint": "Имена приложений через запятую.",
|
"color_strip.notification.filter_list.hint": "Одно имя приложения на строку. Используйте «Обзор» для выбора из запущенных процессов.",
|
||||||
"color_strip.notification.filter_list.placeholder": "Discord, Slack, Telegram",
|
"color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram",
|
||||||
"color_strip.notification.app_colors": "Цвета приложений",
|
"color_strip.notification.app_colors": "Цвета приложений",
|
||||||
"color_strip.notification.app_colors.label": "Назначения цветов:",
|
"color_strip.notification.app_colors.label": "Назначения цветов:",
|
||||||
"color_strip.notification.app_colors.hint": "Индивидуальные цвета для приложений. Каждая строка связывает имя приложения с цветом уведомления.",
|
"color_strip.notification.app_colors.hint": "Индивидуальные цвета для приложений. Каждая строка связывает имя приложения с цветом уведомления.",
|
||||||
|
|||||||
@@ -826,8 +826,8 @@
|
|||||||
"color_strip.notification.filter_mode.whitelist": "白名单",
|
"color_strip.notification.filter_mode.whitelist": "白名单",
|
||||||
"color_strip.notification.filter_mode.blacklist": "黑名单",
|
"color_strip.notification.filter_mode.blacklist": "黑名单",
|
||||||
"color_strip.notification.filter_list": "应用列表:",
|
"color_strip.notification.filter_list": "应用列表:",
|
||||||
"color_strip.notification.filter_list.hint": "以逗号分隔的应用名称。",
|
"color_strip.notification.filter_list.hint": "每行一个应用名称。使用「浏览」从运行中的进程中选择。",
|
||||||
"color_strip.notification.filter_list.placeholder": "Discord, Slack, Telegram",
|
"color_strip.notification.filter_list.placeholder": "Discord\nSlack\nTelegram",
|
||||||
"color_strip.notification.app_colors": "应用颜色",
|
"color_strip.notification.app_colors": "应用颜色",
|
||||||
"color_strip.notification.app_colors.label": "颜色映射:",
|
"color_strip.notification.app_colors.label": "颜色映射:",
|
||||||
"color_strip.notification.app_colors.hint": "每个应用的自定义通知颜色。每行将一个应用名称映射到特定颜色。",
|
"color_strip.notification.app_colors.hint": "每个应用的自定义通知颜色。每行将一个应用名称映射到特定颜色。",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* - Navigation: network-first with offline fallback
|
* - Navigation: network-first with offline fallback
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE_NAME = 'ledgrab-v12';
|
const CACHE_NAME = 'ledgrab-v14';
|
||||||
|
|
||||||
// Only pre-cache static assets (no auth required).
|
// Only pre-cache static assets (no auth required).
|
||||||
// Do NOT pre-cache '/' — it requires API key auth and would cache an error page.
|
// Do NOT pre-cache '/' — it requires API key auth and would cache an error page.
|
||||||
|
|||||||
@@ -238,7 +238,6 @@ class ColorStripSource:
|
|||||||
app_filter_mode=data.get("app_filter_mode") or "off",
|
app_filter_mode=data.get("app_filter_mode") or "off",
|
||||||
app_filter_list=app_filter_list,
|
app_filter_list=app_filter_list,
|
||||||
os_listener=bool(data.get("os_listener", False)),
|
os_listener=bool(data.get("os_listener", False)),
|
||||||
led_count=data.get("led_count") or 0,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Default: "picture" type
|
# Default: "picture" type
|
||||||
@@ -504,7 +503,6 @@ class NotificationColorStripSource(ColorStripSource):
|
|||||||
app_filter_mode: str = "off" # off | whitelist | blacklist
|
app_filter_mode: str = "off" # off | whitelist | blacklist
|
||||||
app_filter_list: list = field(default_factory=list) # app names for filter
|
app_filter_list: list = field(default_factory=list) # app names for filter
|
||||||
os_listener: bool = False # whether to listen for OS notifications
|
os_listener: bool = False # whether to listen for OS notifications
|
||||||
led_count: int = 0 # 0 = use device LED count
|
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
d = super().to_dict()
|
d = super().to_dict()
|
||||||
@@ -515,5 +513,4 @@ class NotificationColorStripSource(ColorStripSource):
|
|||||||
d["app_filter_mode"] = self.app_filter_mode
|
d["app_filter_mode"] = self.app_filter_mode
|
||||||
d["app_filter_list"] = list(self.app_filter_list)
|
d["app_filter_list"] = list(self.app_filter_list)
|
||||||
d["os_listener"] = self.os_listener
|
d["os_listener"] = self.os_listener
|
||||||
d["led_count"] = self.led_count
|
|
||||||
return d
|
return d
|
||||||
|
|||||||
@@ -279,7 +279,6 @@ class ColorStripStore:
|
|||||||
app_filter_mode=app_filter_mode or "off",
|
app_filter_mode=app_filter_mode or "off",
|
||||||
app_filter_list=app_filter_list if isinstance(app_filter_list, list) else [],
|
app_filter_list=app_filter_list if isinstance(app_filter_list, list) else [],
|
||||||
os_listener=bool(os_listener) if os_listener is not None else False,
|
os_listener=bool(os_listener) if os_listener is not None else False,
|
||||||
led_count=led_count,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if calibration is None:
|
if calibration is None:
|
||||||
@@ -472,8 +471,6 @@ class ColorStripStore:
|
|||||||
source.app_filter_list = app_filter_list
|
source.app_filter_list = app_filter_list
|
||||||
if os_listener is not None:
|
if os_listener is not None:
|
||||||
source.os_listener = bool(os_listener)
|
source.os_listener = bool(os_listener)
|
||||||
if led_count is not None:
|
|
||||||
source.led_count = led_count
|
|
||||||
|
|
||||||
source.updated_at = datetime.utcnow()
|
source.updated_at = datetime.utcnow()
|
||||||
self._save()
|
self._save()
|
||||||
|
|||||||
@@ -145,7 +145,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<small class="input-hint" style="display:none" data-i18n="color_strip.color_cycle.colors.hint">Colors to cycle through smoothly. At least 2 required.</small>
|
<small class="input-hint" style="display:none" data-i18n="color_strip.color_cycle.colors.hint">Colors to cycle through smoothly. At least 2 required.</small>
|
||||||
<div id="color-cycle-colors-list"></div>
|
<div id="color-cycle-colors-list"></div>
|
||||||
<button type="button" class="btn btn-secondary" onclick="colorCycleAddColor()" data-i18n="color_strip.color_cycle.add_color">+ Add Color</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -497,8 +496,17 @@
|
|||||||
<label data-i18n="color_strip.notification.filter_list">App List:</label>
|
<label data-i18n="color_strip.notification.filter_list">App List:</label>
|
||||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||||
</div>
|
</div>
|
||||||
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.filter_list.hint">Comma-separated app names for the filter.</small>
|
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.filter_list.hint">One app name per line. Use Browse to pick from running processes.</small>
|
||||||
<input type="text" id="css-editor-notification-filter-list" data-i18n-placeholder="color_strip.notification.filter_list.placeholder" placeholder="Discord, Slack, Telegram">
|
<div class="condition-field" id="css-editor-notification-filter-picker-container">
|
||||||
|
<div class="condition-apps-header">
|
||||||
|
<button type="button" class="btn-browse-apps" data-i18n="automations.condition.application.browse">Browse</button>
|
||||||
|
</div>
|
||||||
|
<textarea id="css-editor-notification-filter-list" class="condition-apps" rows="3" data-i18n-placeholder="color_strip.notification.filter_list.placeholder" placeholder="Discord Slack Telegram"></textarea>
|
||||||
|
<div class="process-picker" style="display:none">
|
||||||
|
<input type="text" class="process-picker-search" data-i18n-placeholder="automations.condition.application.search" placeholder="Search..." autocomplete="off">
|
||||||
|
<div class="process-picker-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details class="form-collapse">
|
<details class="form-collapse">
|
||||||
|
|||||||
Reference in New Issue
Block a user