mirror of
https://github.com/smittix/intercept.git
synced 2026-04-25 07:10:00 -07:00
feat: ship platform UX and reliability upgrades
This commit is contained in:
322
static/js/core/command-palette.js
Normal file
322
static/js/core/command-palette.js
Normal file
@@ -0,0 +1,322 @@
|
||||
const CommandPalette = (function() {
|
||||
'use strict';
|
||||
|
||||
let overlayEl = null;
|
||||
let inputEl = null;
|
||||
let listEl = null;
|
||||
let isOpen = false;
|
||||
let activeIndex = 0;
|
||||
let filteredItems = [];
|
||||
|
||||
const modeCommands = [
|
||||
{ mode: 'pager', label: 'Pager' },
|
||||
{ mode: 'sensor', label: '433MHz Sensors' },
|
||||
{ mode: 'rtlamr', label: 'Meters' },
|
||||
{ mode: 'listening', label: 'Listening Post' },
|
||||
{ mode: 'subghz', label: 'SubGHz' },
|
||||
{ mode: 'aprs', label: 'APRS' },
|
||||
{ mode: 'wifi', label: 'WiFi Scanner' },
|
||||
{ mode: 'bluetooth', label: 'Bluetooth Scanner' },
|
||||
{ mode: 'bt_locate', label: 'BT Locate' },
|
||||
{ mode: 'satellite', label: 'Satellite' },
|
||||
{ mode: 'sstv', label: 'ISS SSTV' },
|
||||
{ mode: 'weathersat', label: 'Weather Sat' },
|
||||
{ mode: 'sstv_general', label: 'HF SSTV' },
|
||||
{ mode: 'gps', label: 'GPS' },
|
||||
{ mode: 'meshtastic', label: 'Meshtastic' },
|
||||
{ mode: 'dmr', label: 'Digital Voice' },
|
||||
{ mode: 'websdr', label: 'WebSDR' },
|
||||
{ mode: 'analytics', label: 'Analytics' },
|
||||
{ mode: 'spaceweather', label: 'Space Weather' },
|
||||
];
|
||||
|
||||
function init() {
|
||||
buildDOM();
|
||||
registerHotkeys();
|
||||
renderItems('');
|
||||
}
|
||||
|
||||
function buildDOM() {
|
||||
overlayEl = document.createElement('div');
|
||||
overlayEl.className = 'command-palette-overlay';
|
||||
overlayEl.id = 'commandPaletteOverlay';
|
||||
overlayEl.addEventListener('click', (event) => {
|
||||
if (event.target === overlayEl) close();
|
||||
});
|
||||
|
||||
const palette = document.createElement('div');
|
||||
palette.className = 'command-palette';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'command-palette-header';
|
||||
|
||||
inputEl = document.createElement('input');
|
||||
inputEl.className = 'command-palette-input';
|
||||
inputEl.type = 'text';
|
||||
inputEl.autocomplete = 'off';
|
||||
inputEl.placeholder = 'Search commands and modes...';
|
||||
inputEl.setAttribute('aria-label', 'Command Palette Search');
|
||||
inputEl.addEventListener('input', () => {
|
||||
renderItems(inputEl.value || '');
|
||||
});
|
||||
inputEl.addEventListener('keydown', onInputKeyDown);
|
||||
|
||||
const hint = document.createElement('span');
|
||||
hint.className = 'command-palette-hint';
|
||||
hint.textContent = 'Esc close';
|
||||
|
||||
header.appendChild(inputEl);
|
||||
header.appendChild(hint);
|
||||
|
||||
listEl = document.createElement('div');
|
||||
listEl.className = 'command-palette-list';
|
||||
listEl.id = 'commandPaletteList';
|
||||
|
||||
palette.appendChild(header);
|
||||
palette.appendChild(listEl);
|
||||
overlayEl.appendChild(palette);
|
||||
document.body.appendChild(overlayEl);
|
||||
}
|
||||
|
||||
function registerHotkeys() {
|
||||
document.addEventListener('keydown', (event) => {
|
||||
const cmdK = (event.key.toLowerCase() === 'k') && (event.ctrlKey || event.metaKey);
|
||||
if (cmdK) {
|
||||
event.preventDefault();
|
||||
if (isOpen) {
|
||||
close();
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOpen) return;
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onInputKeyDown(event) {
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
activeIndex = Math.min(activeIndex + 1, Math.max(filteredItems.length - 1, 0));
|
||||
renderSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
activeIndex = Math.max(activeIndex - 1, 0);
|
||||
renderSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
const item = filteredItems[activeIndex];
|
||||
if (item && typeof item.run === 'function') {
|
||||
item.run();
|
||||
}
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
function getCommands() {
|
||||
const commands = [
|
||||
{
|
||||
title: 'Open Settings',
|
||||
description: 'Open global settings panel',
|
||||
keyword: 'settings configure tools',
|
||||
shortcut: 'S',
|
||||
run: () => {
|
||||
if (typeof showSettings === 'function') showSettings();
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Settings: Alerts',
|
||||
description: 'Open alert rules and feed',
|
||||
keyword: 'settings alerts rule',
|
||||
run: () => openSettingsTab('alerts')
|
||||
},
|
||||
{
|
||||
title: 'Settings: Recording',
|
||||
description: 'Open recording manager',
|
||||
keyword: 'settings recording replay',
|
||||
run: () => openSettingsTab('recording')
|
||||
},
|
||||
{
|
||||
title: 'Settings: Location',
|
||||
description: 'Configure observer location',
|
||||
keyword: 'settings location gps lat lon',
|
||||
run: () => openSettingsTab('location')
|
||||
},
|
||||
{
|
||||
title: 'View Aircraft Dashboard',
|
||||
description: 'Open dedicated ADS-B dashboard page',
|
||||
keyword: 'aircraft adsb dashboard',
|
||||
run: () => { window.location.href = '/adsb/dashboard'; }
|
||||
},
|
||||
{
|
||||
title: 'View Vessel Dashboard',
|
||||
description: 'Open dedicated AIS dashboard page',
|
||||
keyword: 'vessel ais dashboard',
|
||||
run: () => { window.location.href = '/ais/dashboard'; }
|
||||
},
|
||||
{
|
||||
title: 'Kill All Running Processes',
|
||||
description: 'Stop all decoders and scans',
|
||||
keyword: 'kill stop processes emergency',
|
||||
run: () => {
|
||||
if (typeof killAll === 'function') {
|
||||
killAll();
|
||||
} else if (typeof fetch === 'function') {
|
||||
fetch('/killall', { method: 'POST' });
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Toggle Sidebar Width',
|
||||
description: 'Collapse or expand the left sidebar',
|
||||
keyword: 'sidebar collapse layout',
|
||||
run: () => {
|
||||
if (typeof toggleMainSidebarCollapse === 'function') {
|
||||
toggleMainSidebarCollapse();
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
for (const modeEntry of modeCommands) {
|
||||
commands.push({
|
||||
title: `Switch Mode: ${modeEntry.label}`,
|
||||
description: 'Navigate directly to mode',
|
||||
keyword: `mode ${modeEntry.mode} ${modeEntry.label.toLowerCase()}`,
|
||||
run: () => goToMode(modeEntry.mode),
|
||||
});
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
function renderItems(query) {
|
||||
const q = String(query || '').trim().toLowerCase();
|
||||
const allItems = getCommands();
|
||||
|
||||
filteredItems = allItems.filter((item) => {
|
||||
if (!q) return true;
|
||||
const haystack = `${item.title} ${item.description || ''} ${item.keyword || ''}`.toLowerCase();
|
||||
return haystack.includes(q);
|
||||
}).slice(0, 80);
|
||||
|
||||
activeIndex = 0;
|
||||
|
||||
listEl.innerHTML = '';
|
||||
if (filteredItems.length === 0) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'command-palette-empty';
|
||||
empty.textContent = 'No matching commands';
|
||||
listEl.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
filteredItems.forEach((item, idx) => {
|
||||
const row = document.createElement('button');
|
||||
row.type = 'button';
|
||||
row.className = 'command-palette-item';
|
||||
row.dataset.index = String(idx);
|
||||
row.addEventListener('click', () => {
|
||||
item.run();
|
||||
close();
|
||||
});
|
||||
|
||||
const meta = document.createElement('span');
|
||||
meta.className = 'meta';
|
||||
|
||||
const title = document.createElement('span');
|
||||
title.className = 'title';
|
||||
title.textContent = item.title;
|
||||
meta.appendChild(title);
|
||||
|
||||
const desc = document.createElement('span');
|
||||
desc.className = 'desc';
|
||||
desc.textContent = item.description || '';
|
||||
meta.appendChild(desc);
|
||||
|
||||
row.appendChild(meta);
|
||||
|
||||
if (item.shortcut) {
|
||||
const kbd = document.createElement('span');
|
||||
kbd.className = 'kbd';
|
||||
kbd.textContent = item.shortcut;
|
||||
row.appendChild(kbd);
|
||||
}
|
||||
|
||||
listEl.appendChild(row);
|
||||
});
|
||||
|
||||
renderSelection();
|
||||
}
|
||||
|
||||
function renderSelection() {
|
||||
const rows = listEl.querySelectorAll('.command-palette-item');
|
||||
rows.forEach((row) => {
|
||||
const idx = Number(row.dataset.index || 0);
|
||||
row.classList.toggle('active', idx === activeIndex);
|
||||
});
|
||||
|
||||
const activeRow = listEl.querySelector(`.command-palette-item[data-index="${activeIndex}"]`);
|
||||
if (activeRow) {
|
||||
activeRow.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
function goToMode(mode) {
|
||||
const welcome = document.getElementById('welcomePage');
|
||||
if (welcome && getComputedStyle(welcome).display !== 'none') {
|
||||
welcome.style.display = 'none';
|
||||
}
|
||||
|
||||
if (typeof switchMode === 'function') {
|
||||
switchMode(mode, { updateUrl: true });
|
||||
}
|
||||
}
|
||||
|
||||
function openSettingsTab(tab) {
|
||||
if (typeof showSettings === 'function') {
|
||||
showSettings();
|
||||
}
|
||||
if (typeof switchSettingsTab === 'function') {
|
||||
switchSettingsTab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (!overlayEl) return;
|
||||
isOpen = true;
|
||||
overlayEl.classList.add('open');
|
||||
renderItems('');
|
||||
inputEl.value = '';
|
||||
requestAnimationFrame(() => {
|
||||
inputEl.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (!overlayEl) return;
|
||||
isOpen = false;
|
||||
overlayEl.classList.remove('open');
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
open,
|
||||
close,
|
||||
};
|
||||
})();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
CommandPalette.init();
|
||||
});
|
||||
Reference in New Issue
Block a user