release: veröffentliche Twitch VOD Manager 1.0.1
Startet die öffentliche Versionslinie mit einer bereinigten Ein-Commit-Historie, stellt den Updater auf GitHub Releases um, entfernt interne Release-Ziele und beschränkt den gepackten Anwendungssatz auf notwendige Laufzeitdateien. Enthält aktualisierte produktive Abhängigkeiten ohne bekannte npm-Audit-Funde sowie die geprüfte öffentliche Quell-Positivliste.
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
// Command Palette — Pillar 5 UI Power.
|
||||
// Ctrl+K oeffnet ein Suchfeld + Liste schnell ausfuehrbarer Aktionen.
|
||||
// MVP: 6 statische Tab-Wechsel-Befehle, prefix-match auf Label.
|
||||
|
||||
interface PaletteCommand {
|
||||
id: string;
|
||||
label: string;
|
||||
hint: string;
|
||||
keywords: string; // fuer Match — Label kleingeschrieben + Synonyme
|
||||
action: () => void;
|
||||
}
|
||||
|
||||
(function initCommandPalette() {
|
||||
const STORE: { commands: PaletteCommand[]; activeIndex: number; filtered: PaletteCommand[] } = {
|
||||
commands: [],
|
||||
activeIndex: 0,
|
||||
filtered: [],
|
||||
};
|
||||
|
||||
function buildCommands(): PaletteCommand[] {
|
||||
const w = window as unknown as {
|
||||
showTab?: (tab: string) => void;
|
||||
selectStreamer?: (name: string, forceRefresh?: boolean) => Promise<void>;
|
||||
config?: { streamers?: Array<{ name: string }> };
|
||||
};
|
||||
const showTab = w.showTab;
|
||||
if (typeof showTab !== 'function') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// hint 'Open' statt 'Tab' — 'Tab' las sich wie eine Tastatur-Taste
|
||||
// ('druecke Tab') statt 'oeffnet diesen Tab'.
|
||||
const tabs: Array<{ id: string; labels: string[]; hint: string }> = [
|
||||
{ id: 'vods', labels: ['VODs', 'videos', 'streams'], hint: 'Open' },
|
||||
{ id: 'queue', labels: ['Queue', 'downloads', 'warteschlange'], hint: 'Open' },
|
||||
{ id: 'streamers', labels: ['Streamers', 'channels'], hint: 'Open' },
|
||||
{ id: 'stats', labels: ['Stats', 'statistiken', 'dashboard'], hint: 'Open' },
|
||||
{ id: 'archive', labels: ['Archive', 'archiv'], hint: 'Open' },
|
||||
{ id: 'settings', labels: ['Settings', 'einstellungen', 'config'], hint: 'Open' },
|
||||
];
|
||||
|
||||
const tabCommands: PaletteCommand[] = tabs.map(t => ({
|
||||
id: 'tab:' + t.id,
|
||||
label: t.labels[0],
|
||||
hint: t.hint,
|
||||
keywords: t.labels.join(' ').toLowerCase(),
|
||||
action: () => showTab(t.id),
|
||||
}));
|
||||
|
||||
// Streamer-Liste aus globalem config (gefuellt nach renderer-Init).
|
||||
const streamerCommands: PaletteCommand[] = [];
|
||||
const streamers = Array.isArray(w.config?.streamers) ? w.config.streamers : [];
|
||||
const selectStreamer = w.selectStreamer;
|
||||
if (typeof selectStreamer === 'function') {
|
||||
for (const entry of streamers) {
|
||||
if (!entry || typeof entry.name !== 'string') continue;
|
||||
const name = entry.name;
|
||||
streamerCommands.push({
|
||||
id: 'streamer:' + name.toLowerCase(),
|
||||
label: name,
|
||||
hint: 'Streamer',
|
||||
keywords: ('@' + name + ' ' + name).toLowerCase(),
|
||||
action: () => {
|
||||
showTab('vods');
|
||||
void selectStreamer(name);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [...tabCommands, ...streamerCommands];
|
||||
}
|
||||
|
||||
function getModal(): HTMLElement | null {
|
||||
return document.getElementById('commandPaletteModal');
|
||||
}
|
||||
|
||||
function getInput(): HTMLInputElement | null {
|
||||
return document.getElementById('commandPaletteInput') as HTMLInputElement | null;
|
||||
}
|
||||
|
||||
function getList(): HTMLUListElement | null {
|
||||
return document.getElementById('commandPaletteList') as HTMLUListElement | null;
|
||||
}
|
||||
|
||||
function isOpen(): boolean {
|
||||
return Boolean(getModal()?.classList.contains('show'));
|
||||
}
|
||||
|
||||
function clearList(list: HTMLUListElement) {
|
||||
while (list.firstChild) list.removeChild(list.firstChild);
|
||||
}
|
||||
|
||||
function render() {
|
||||
const list = getList();
|
||||
if (!list) return;
|
||||
clearList(list);
|
||||
STORE.filtered.forEach((cmd, idx) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'cp-item' + (idx === STORE.activeIndex ? ' cp-active' : '');
|
||||
li.dataset.cmdId = cmd.id;
|
||||
li.setAttribute('role', 'option');
|
||||
li.setAttribute('aria-selected', idx === STORE.activeIndex ? 'true' : 'false');
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.className = 'cp-item-label';
|
||||
label.textContent = cmd.label;
|
||||
li.appendChild(label);
|
||||
|
||||
const hint = document.createElement('span');
|
||||
hint.className = 'cp-item-hint';
|
||||
hint.textContent = cmd.hint;
|
||||
li.appendChild(hint);
|
||||
|
||||
li.addEventListener('mouseenter', () => {
|
||||
STORE.activeIndex = idx;
|
||||
render();
|
||||
});
|
||||
li.addEventListener('click', () => {
|
||||
executeAt(idx);
|
||||
});
|
||||
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilter(query: string) {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) {
|
||||
STORE.filtered = STORE.commands.slice();
|
||||
} else {
|
||||
STORE.filtered = STORE.commands.filter(c => c.keywords.includes(q));
|
||||
}
|
||||
if (STORE.activeIndex >= STORE.filtered.length) {
|
||||
STORE.activeIndex = STORE.filtered.length > 0 ? STORE.filtered.length - 1 : 0;
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
function executeAt(idx: number) {
|
||||
const cmd = STORE.filtered[idx];
|
||||
if (!cmd) return;
|
||||
close();
|
||||
try {
|
||||
cmd.action();
|
||||
} catch (e) {
|
||||
console.error('command-palette: action failed', cmd.id, e);
|
||||
}
|
||||
}
|
||||
|
||||
function open() {
|
||||
const modal = getModal();
|
||||
const input = getInput();
|
||||
if (!modal || !input) return;
|
||||
STORE.commands = buildCommands();
|
||||
STORE.filtered = STORE.commands.slice();
|
||||
STORE.activeIndex = 0;
|
||||
input.value = '';
|
||||
modal.classList.add('show');
|
||||
requestAnimationFrame(() => input.focus());
|
||||
render();
|
||||
}
|
||||
|
||||
function close() {
|
||||
const modal = getModal();
|
||||
if (!modal) return;
|
||||
modal.classList.remove('show');
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
// Toggle: Ctrl+K (Linux/Windows) or Cmd+K (Mac)
|
||||
if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && (e.key === 'k' || e.key === 'K')) {
|
||||
e.preventDefault();
|
||||
if (isOpen()) {
|
||||
close();
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOpen()) return;
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
close();
|
||||
return;
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (STORE.filtered.length === 0) return;
|
||||
STORE.activeIndex = (STORE.activeIndex + 1) % STORE.filtered.length;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (STORE.filtered.length === 0) return;
|
||||
STORE.activeIndex = (STORE.activeIndex - 1 + STORE.filtered.length) % STORE.filtered.length;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
executeAt(STORE.activeIndex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function attach() {
|
||||
const input = getInput();
|
||||
if (input) {
|
||||
input.addEventListener('input', () => applyFilter(input.value));
|
||||
}
|
||||
const modal = getModal();
|
||||
if (modal) {
|
||||
modal.addEventListener('click', e => {
|
||||
if (e.target === modal) close();
|
||||
});
|
||||
}
|
||||
document.addEventListener('keydown', onKeydown, { capture: true });
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', attach);
|
||||
} else {
|
||||
attach();
|
||||
}
|
||||
|
||||
// Expose for renderer.ts closeTopmostOpenModal integration.
|
||||
(window as unknown as { closeCommandPalette?: () => void }).closeCommandPalette = close;
|
||||
})();
|
||||
Reference in New Issue
Block a user