Refactor process picker into palette pattern, add notification app picker
- Refactor process-picker.ts into generic NamePalette with two concrete instances: ProcessPalette (running processes) and NotificationAppPalette (OS notification history apps) - Notification color strip app colors and filter list now use NotificationAppPalette (shows display names like "Telegram" instead of process names like "telegram.exe") - Fix case-insensitive matching for app_colors and app_filter_list in notification_stream.py - Compact browse/remove buttons in notification app color rows with proper search icon - Remove old inline process-picker HTML/CSS (replaced by palette overlay) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -507,6 +507,60 @@ function _buildConditionTypeItems() {
|
||||
}));
|
||||
}
|
||||
|
||||
/** Wire up the custom time-range picker inputs → sync to hidden fields. */
|
||||
function _wireTimeRangePicker(container: HTMLElement) {
|
||||
const startH = container.querySelector('.tr-start-h') as HTMLInputElement;
|
||||
const startM = container.querySelector('.tr-start-m') as HTMLInputElement;
|
||||
const endH = container.querySelector('.tr-end-h') as HTMLInputElement;
|
||||
const endM = container.querySelector('.tr-end-m') as HTMLInputElement;
|
||||
const hiddenStart = container.querySelector('.condition-start-time') as HTMLInputElement;
|
||||
const hiddenEnd = container.querySelector('.condition-end-time') as HTMLInputElement;
|
||||
if (!startH || !startM || !endH || !endM) return;
|
||||
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
|
||||
function clamp(input: HTMLInputElement, min: number, max: number) {
|
||||
let v = parseInt(input.value, 10);
|
||||
if (isNaN(v)) v = min;
|
||||
if (v < min) v = min;
|
||||
if (v > max) v = max;
|
||||
input.value = pad(v);
|
||||
return v;
|
||||
}
|
||||
|
||||
function sync() {
|
||||
const sh = clamp(startH, 0, 23);
|
||||
const sm = clamp(startM, 0, 59);
|
||||
const eh = clamp(endH, 0, 23);
|
||||
const em = clamp(endM, 0, 59);
|
||||
hiddenStart.value = `${pad(sh)}:${pad(sm)}`;
|
||||
hiddenEnd.value = `${pad(eh)}:${pad(em)}`;
|
||||
}
|
||||
|
||||
[startH, startM, endH, endM].forEach(inp => {
|
||||
inp.addEventListener('focus', () => inp.select());
|
||||
inp.addEventListener('input', sync);
|
||||
inp.addEventListener('blur', sync);
|
||||
inp.addEventListener('keydown', (e) => {
|
||||
const isHour = inp.dataset.role === 'hour';
|
||||
const max = isHour ? 23 : 59;
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
let v = parseInt(inp.value, 10) || 0;
|
||||
inp.value = pad(v >= max ? 0 : v + 1);
|
||||
sync();
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
let v = parseInt(inp.value, 10) || 0;
|
||||
inp.value = pad(v <= 0 ? max : v - 1);
|
||||
sync();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
sync();
|
||||
}
|
||||
|
||||
function addAutomationConditionRow(condition: any) {
|
||||
const list = document.getElementById('automation-conditions-list');
|
||||
const row = document.createElement('div');
|
||||
@@ -545,18 +599,35 @@ function addAutomationConditionRow(condition: any) {
|
||||
if (type === 'time_of_day') {
|
||||
const startTime = data.start_time || '00:00';
|
||||
const endTime = data.end_time || '23:59';
|
||||
const [sh, sm] = startTime.split(':').map(Number);
|
||||
const [eh, em] = endTime.split(':').map(Number);
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
container.innerHTML = `
|
||||
<div class="condition-fields">
|
||||
<div class="condition-field">
|
||||
<label>${t('automations.condition.time_of_day.start_time')}</label>
|
||||
<input type="time" class="condition-start-time" value="${startTime}">
|
||||
</div>
|
||||
<div class="condition-field">
|
||||
<label>${t('automations.condition.time_of_day.end_time')}</label>
|
||||
<input type="time" class="condition-end-time" value="${endTime}">
|
||||
<input type="hidden" class="condition-start-time" value="${startTime}">
|
||||
<input type="hidden" class="condition-end-time" value="${endTime}">
|
||||
<div class="time-range-picker">
|
||||
<div class="time-range-slot">
|
||||
<span class="time-range-label">${t('automations.condition.time_of_day.start_time')}</span>
|
||||
<div class="time-range-input-wrap">
|
||||
<input type="number" class="tr-start-h" min="0" max="23" value="${sh}" data-role="hour">
|
||||
<span class="time-range-colon">:</span>
|
||||
<input type="number" class="tr-start-m" min="0" max="59" value="${pad(sm)}" data-role="minute">
|
||||
</div>
|
||||
</div>
|
||||
<div class="time-range-arrow">→</div>
|
||||
<div class="time-range-slot">
|
||||
<span class="time-range-label">${t('automations.condition.time_of_day.end_time')}</span>
|
||||
<div class="time-range-input-wrap">
|
||||
<input type="number" class="tr-end-h" min="0" max="23" value="${eh}" data-role="hour">
|
||||
<span class="time-range-colon">:</span>
|
||||
<input type="number" class="tr-end-m" min="0" max="59" value="${pad(em)}" data-role="minute">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="condition-always-desc">${t('automations.condition.time_of_day.overnight_hint')}</small>
|
||||
</div>`;
|
||||
_wireTimeRangePicker(container);
|
||||
return;
|
||||
}
|
||||
if (type === 'system_idle') {
|
||||
@@ -660,10 +731,6 @@ function addAutomationConditionRow(condition: any) {
|
||||
<button type="button" class="btn-browse-apps" title="${t('automations.condition.application.browse')}">${t('automations.condition.application.browse')}</button>
|
||||
</div>
|
||||
<textarea class="condition-apps" rows="3" placeholder="firefox.exe chrome.exe">${escapeHtml(appsValue)}</textarea>
|
||||
<div class="process-picker" style="display:none">
|
||||
<input type="text" class="process-picker-search" placeholder="${t('automations.condition.application.search')}" autocomplete="off">
|
||||
<div class="process-picker-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -14,13 +14,13 @@ import {
|
||||
ICON_LED, ICON_PALETTE, ICON_FPS, ICON_MAP_PIN, ICON_MUSIC,
|
||||
ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM,
|
||||
ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK, ICON_BELL, ICON_TEST,
|
||||
ICON_SUN_DIM, ICON_WARNING, ICON_AUTOMATION,
|
||||
ICON_SUN_DIM, ICON_WARNING, ICON_AUTOMATION, ICON_SEARCH,
|
||||
} from '../core/icons.ts';
|
||||
import * as P from '../core/icon-paths.ts';
|
||||
import { wrapCard } from '../core/card-colors.ts';
|
||||
import type { ColorStripSource } from '../types.ts';
|
||||
import { TagInput, renderTagChips } from '../core/tag-input.ts';
|
||||
import { attachProcessPicker } from '../core/process-picker.ts';
|
||||
import { attachNotificationAppPicker, NotificationAppPalette } from '../core/process-picker.ts';
|
||||
import { IconSelect, showTypePicker } from '../core/icon-select.ts';
|
||||
import { EntitySelect } from '../core/entity-palette.ts';
|
||||
import { getBaseOrigin } from './settings.ts';
|
||||
@@ -1270,11 +1270,30 @@ function _notificationAppColorsRenderList() {
|
||||
list.innerHTML = _notificationAppColors.map((entry, i) => `
|
||||
<div class="notif-app-color-row">
|
||||
<input type="text" class="notif-app-name" data-idx="${i}" value="${escapeHtml(entry.app)}" placeholder="App name">
|
||||
<button type="button" class="notif-app-browse" data-idx="${i}"
|
||||
title="${t('automations.condition.application.browse')}">${ICON_SEARCH}</button>
|
||||
<input type="color" class="notif-app-color" data-idx="${i}" value="${entry.color}">
|
||||
<button type="button" class="btn btn-secondary notif-app-color-remove"
|
||||
<button type="button" class="notif-app-color-remove"
|
||||
onclick="notificationRemoveAppColor(${i})">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Wire up browse buttons to open process palette
|
||||
list.querySelectorAll<HTMLButtonElement>('.notif-app-browse').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const idx = parseInt(btn.dataset.idx!);
|
||||
const nameInput = list.querySelector<HTMLInputElement>(`.notif-app-name[data-idx="${idx}"]`);
|
||||
if (!nameInput) return;
|
||||
const picked = await NotificationAppPalette.pick({
|
||||
current: nameInput.value,
|
||||
placeholder: t('color_strip.notification.search_apps') || 'Search notification apps…',
|
||||
});
|
||||
if (picked !== undefined) {
|
||||
nameInput.value = picked;
|
||||
_notificationAppColorsSyncFromDom();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function notificationAddAppColor() {
|
||||
@@ -1443,7 +1462,7 @@ function _resetNotificationState() {
|
||||
function _attachNotificationProcessPicker() {
|
||||
const container = document.getElementById('css-editor-notification-filter-picker-container') as HTMLElement | null;
|
||||
const textarea = document.getElementById('css-editor-notification-filter-list') as HTMLTextAreaElement | null;
|
||||
if (container && textarea) attachProcessPicker(container, textarea);
|
||||
if (container && textarea) attachNotificationAppPicker(container, textarea);
|
||||
}
|
||||
|
||||
function _showNotificationEndpoint(cssId: any) {
|
||||
|
||||
Reference in New Issue
Block a user