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:
2026-03-21 01:23:08 +03:00
parent 5fa851618b
commit 07bb89e9b7
10 changed files with 5787 additions and 5397 deletions

View File

@@ -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&#10;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>
`;

View File

@@ -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})">&#x2715;</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) {