mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-17 09:31:04 +00:00
- Implemented a new SPA page for LLM Bridge and MCP Server settings in `llm-config.js`. - Added functionality for managing LLM and MCP configurations, including toggling, saving settings, and testing connections. - Created HTTP endpoints in `llm_utils.py` for handling LLM chat, status checks, and MCP server configuration. - Integrated model fetching from LaRuche and Ollama backends. - Enhanced error handling and logging for better debugging and user feedback.
1187 lines
37 KiB
JavaScript
1187 lines
37 KiB
JavaScript
/**
|
|
* QuickPanel — WiFi & Bluetooth management panel.
|
|
*
|
|
* Slide-down panel with two tabs (WiFi / Bluetooth), scan controls,
|
|
* auto-scan toggles, known-network management with drag-and-drop
|
|
* priority reordering and multi-select batch delete, potfile upload,
|
|
* and Bluetooth pairing/trust/connect with state indicators.
|
|
*/
|
|
|
|
import { $, $$, el, toast, empty } from './dom.js';
|
|
import { api } from './api.js';
|
|
import { t } from './i18n.js';
|
|
|
|
/* ---------- API endpoints ---------- */
|
|
|
|
const API = {
|
|
scanWifi: '/scan_wifi',
|
|
getKnownWifi: '/get_known_wifi',
|
|
connectKnown: '/connect_known_wifi',
|
|
connectWifi: '/connect_wifi',
|
|
updatePriority: '/update_wifi_priority',
|
|
deleteKnown: '/delete_known_wifi',
|
|
importPotfiles: '/import_potfiles',
|
|
uploadPotfile: '/upload_potfile',
|
|
scanBluetooth: '/scan_bluetooth',
|
|
pairBluetooth: '/pair_bluetooth',
|
|
trustBluetooth: '/trust_bluetooth',
|
|
connectBluetooth: '/connect_bluetooth',
|
|
disconnectBluetooth: '/disconnect_bluetooth',
|
|
forgetBluetooth: '/forget_bluetooth',
|
|
};
|
|
|
|
/* ---------- Constants ---------- */
|
|
|
|
const AUTOSCAN_INTERVAL = 15_000; // 15 s
|
|
const LS_WIFI_AUTO = 'qp_wifi_auto';
|
|
const LS_BT_AUTO = 'qp_bt_auto';
|
|
|
|
/* ---------- Module state ---------- */
|
|
|
|
let panel; // #quickpanel element
|
|
let wifiList; // container for wifi scan results
|
|
let knownList; // container for known networks
|
|
let knownWrapper; // wrapper that holds knownList + batch bar
|
|
let btList; // container for bluetooth results
|
|
let wifiTab; // wifi tab content wrapper
|
|
let btTab; // bluetooth tab content wrapper
|
|
let tabBtns; // [wifiTabBtn, btTabBtn]
|
|
let wifiAutoTimer = null;
|
|
let btAutoTimer = null;
|
|
let activeTab = 'wifi';
|
|
let scanning = { wifi: false, bt: false };
|
|
|
|
// Known networks state
|
|
let knownNetworks = []; // current known networks data
|
|
let editMode = false; // multi-select edit mode
|
|
let selectedSsids = new Set();
|
|
let batchBar = null;
|
|
|
|
// Drag state
|
|
let dragSrcIdx = null;
|
|
let dragPlaceholder = null;
|
|
let touchDragEl = null;
|
|
let touchStartY = 0;
|
|
|
|
/* =================================================================
|
|
Helpers
|
|
================================================================= */
|
|
|
|
function getAutoScan(key) {
|
|
try { return localStorage.getItem(key) === '1'; } catch { return false; }
|
|
}
|
|
function setAutoScan(key, on) {
|
|
try { localStorage.setItem(key, on ? '1' : '0'); } catch { /* noop */ }
|
|
}
|
|
|
|
/** Signal strength (0-100 percent) to bar count (1-4). */
|
|
function signalBars(pct) {
|
|
if (pct >= 75) return 4;
|
|
if (pct >= 50) return 3;
|
|
if (pct >= 25) return 2;
|
|
return 1;
|
|
}
|
|
|
|
/** Signal strength from BT RSSI (dBm) to bar count (1-4). */
|
|
function rssiToBars(rssi) {
|
|
if (rssi === undefined || rssi === null || rssi <= -999) return 0;
|
|
if (rssi >= -50) return 4;
|
|
if (rssi >= -65) return 3;
|
|
if (rssi >= -75) return 2;
|
|
return 1;
|
|
}
|
|
|
|
/** Build a `<span class="sig">` with four bar elements + tooltip. */
|
|
function sigEl(pct, tooltip) {
|
|
const count = signalBars(pct);
|
|
const bars = [];
|
|
for (let i = 1; i <= 4; i++) {
|
|
const bar = el('i');
|
|
bar.style.height = `${4 + i * 3}px`;
|
|
if (i <= count) bar.className = 'on';
|
|
bars.push(bar);
|
|
}
|
|
return el('span', { class: 'sig', title: tooltip || `${pct}%` }, bars);
|
|
}
|
|
|
|
/** Build signal bars from RSSI dBm. */
|
|
function rssiSigEl(rssi) {
|
|
if (rssi === undefined || rssi === null || rssi <= -999) return null;
|
|
const count = rssiToBars(rssi);
|
|
const bars = [];
|
|
for (let i = 1; i <= 4; i++) {
|
|
const bar = el('i');
|
|
bar.style.height = `${4 + i * 3}px`;
|
|
if (i <= count) bar.className = 'on';
|
|
bars.push(bar);
|
|
}
|
|
return el('span', { class: 'sig', title: `${rssi} dBm` }, bars);
|
|
}
|
|
|
|
/** Security type to badge CSS class. */
|
|
function secClass(sec) {
|
|
if (!sec) return 'badge-ok';
|
|
const s = sec.toUpperCase();
|
|
if (s === 'OPEN' || s === '' || s === 'NONE' || s === '--') return 'badge-ok';
|
|
if (s.includes('WPA3')) return 'badge-info';
|
|
if (s.includes('WPA')) return 'badge-warn';
|
|
if (s.includes('WEP')) return 'badge-error';
|
|
return 'badge-warn';
|
|
}
|
|
|
|
function secLabel(sec) {
|
|
if (!sec || sec === '--' || sec.toUpperCase() === 'NONE') return 'Open';
|
|
return sec;
|
|
}
|
|
|
|
function secBadge(sec) {
|
|
return el('span', { class: `badge ${secClass(sec)}`, style: 'font-size:11px;padding:1px 6px' }, [secLabel(sec)]);
|
|
}
|
|
|
|
/** State dot element. */
|
|
function stateDot(on) {
|
|
return el('span', { class: `state-dot ${on ? 'state-on' : 'state-off'}` });
|
|
}
|
|
|
|
/** BT device type to icon. */
|
|
function btIcon(icon) {
|
|
const map = {
|
|
'audio-card': '\u{1F3B5}', // music
|
|
'audio-headphones': '\u{1F3A7}', // headphones
|
|
'audio-headset': '\u{1F3A7}',
|
|
'phone': '\u{1F4F1}', // phone
|
|
'computer': '\u{1F4BB}', // laptop
|
|
'input-keyboard': '\u2328', // keyboard
|
|
'input-mouse': '\u{1F5B1}',
|
|
'input-gaming': '\u{1F3AE}', // gamepad
|
|
'input-tablet': '\u{1F4DD}',
|
|
'printer': '\u{1F5A8}',
|
|
'camera-photo': '\u{1F4F7}',
|
|
'camera-video': '\u{1F4F9}',
|
|
'modem': '\u{1F4E1}',
|
|
'network-wireless': '\u{1F4F6}',
|
|
};
|
|
return map[icon] || '\u{1F4E1}'; // default: satellite antenna
|
|
}
|
|
|
|
/** Auto-scan toggle switch. */
|
|
function autoScanToggle(key, onChange) {
|
|
const isOn = getAutoScan(key);
|
|
const sw = el('span', { class: `switch${isOn ? ' on' : ''}`, role: 'switch', 'aria-checked': String(isOn), tabindex: '0' });
|
|
const label = el('span', { style: 'font-size:12px;color:var(--muted);user-select:none' }, [t('quick.autoScan')]);
|
|
const wrap = el('label', { style: 'display:inline-flex;align-items:center;gap:8px;cursor:pointer' }, [label, sw]);
|
|
|
|
function toggle() {
|
|
const next = !sw.classList.contains('on');
|
|
sw.classList.toggle('on', next);
|
|
sw.setAttribute('aria-checked', String(next));
|
|
setAutoScan(key, next);
|
|
onChange(next);
|
|
}
|
|
|
|
sw.addEventListener('click', toggle);
|
|
sw.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); }
|
|
});
|
|
|
|
return { wrap, isOn };
|
|
}
|
|
|
|
/* =================================================================
|
|
System Dialog (WiFi password prompt)
|
|
================================================================= */
|
|
|
|
function openSysDialog(title, fields, onSubmit) {
|
|
const backdrop = $('#sysDialogBackdrop');
|
|
if (!backdrop) return;
|
|
empty(backdrop);
|
|
|
|
const modal = el('div', { class: 'modal', role: 'dialog', 'aria-modal': 'true', style: 'padding:20px;max-width:400px;width:90vw;border-radius:16px;background:var(--grad-quickpanel,#0a1116);border:1px solid var(--c-border-strong)' });
|
|
|
|
const heading = el('h3', { style: 'margin:0 0 16px;color:var(--ink)' }, [title]);
|
|
modal.appendChild(heading);
|
|
|
|
const form = el('form', { style: 'display:flex;flex-direction:column;gap:12px' });
|
|
|
|
const inputs = {};
|
|
for (const f of fields) {
|
|
const input = el('input', {
|
|
class: 'input',
|
|
type: f.type || 'text',
|
|
placeholder: f.placeholder || '',
|
|
autocomplete: f.autocomplete || 'off',
|
|
style: 'width:100%;padding:10px 12px;border-radius:8px;border:1px solid var(--c-border-strong);background:var(--c-panel);color:var(--ink);font-size:14px',
|
|
});
|
|
if (f.value) input.value = f.value;
|
|
if (f.readonly) input.readOnly = true;
|
|
inputs[f.name] = input;
|
|
|
|
const label = el('label', { style: 'display:flex;flex-direction:column;gap:4px' }, [
|
|
el('span', { style: 'font-size:12px;color:var(--muted)' }, [f.label]),
|
|
input,
|
|
]);
|
|
form.appendChild(label);
|
|
}
|
|
|
|
const btnRow = el('div', { style: 'display:flex;gap:8px;justify-content:flex-end;margin-top:8px' });
|
|
const cancelBtn = el('button', { class: 'btn', type: 'button' }, [t('common.cancel')]);
|
|
const submitBtn = el('button', { class: 'btn', type: 'submit', style: 'background:var(--acid);color:var(--ink-invert,#001014)' }, [t('common.connect')]);
|
|
btnRow.appendChild(cancelBtn);
|
|
btnRow.appendChild(submitBtn);
|
|
form.appendChild(btnRow);
|
|
modal.appendChild(form);
|
|
backdrop.appendChild(modal);
|
|
|
|
backdrop.style.display = 'flex';
|
|
backdrop.classList.add('show');
|
|
|
|
function closeDlg() {
|
|
backdrop.style.display = 'none';
|
|
backdrop.classList.remove('show');
|
|
empty(backdrop);
|
|
}
|
|
|
|
cancelBtn.addEventListener('click', closeDlg);
|
|
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) closeDlg(); });
|
|
|
|
form.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
const values = {};
|
|
for (const [name, inp] of Object.entries(inputs)) values[name] = inp.value;
|
|
closeDlg();
|
|
onSubmit(values);
|
|
});
|
|
|
|
const firstInput = Object.values(inputs).find(i => !i.readOnly);
|
|
if (firstInput) requestAnimationFrame(() => firstInput.focus());
|
|
}
|
|
|
|
function closeSysDialog() {
|
|
const backdrop = $('#sysDialogBackdrop');
|
|
if (!backdrop) return;
|
|
backdrop.style.display = 'none';
|
|
backdrop.classList.remove('show');
|
|
empty(backdrop);
|
|
}
|
|
|
|
/* =================================================================
|
|
WiFi — scan, connect
|
|
================================================================= */
|
|
|
|
async function scanWifi() {
|
|
if (scanning.wifi) return;
|
|
scanning.wifi = true;
|
|
try {
|
|
const data = await api.get(API.scanWifi);
|
|
renderWifiResults(data);
|
|
} catch (err) {
|
|
toast(t('quick.btScanFailed') + ': ' + (err.message || t('common.unknown')), 3000, 'error');
|
|
} finally {
|
|
scanning.wifi = false;
|
|
}
|
|
}
|
|
|
|
function renderWifiResults(data) {
|
|
if (!wifiList) return;
|
|
empty(wifiList);
|
|
|
|
const networks = Array.isArray(data) ? data : (data?.networks || data?.results || []);
|
|
if (!networks.length) {
|
|
wifiList.appendChild(el('div', { style: 'padding:12px;color:var(--muted);text-align:center' }, [t('common.noData')]));
|
|
return;
|
|
}
|
|
|
|
const currentSsid = data?.current_ssid || null;
|
|
networks.sort((a, b) => (b.signal ?? 0) - (a.signal ?? 0));
|
|
|
|
networks.forEach((net, idx) => {
|
|
const ssid = net.ssid || '(Hidden)';
|
|
const signal = net.signal ?? 0;
|
|
const sec = net.security || 'Open';
|
|
const isConnected = net.in_use || ssid === currentSsid;
|
|
|
|
const infoEl = el('div', { class: 'wifi-card-info' }, [
|
|
el('span', { class: 'wifi-card-ssid' + (isConnected ? '' : '') , style: isConnected ? 'color:var(--acid)' : '' }, [ssid]),
|
|
el('div', { class: 'wifi-card-meta' }, [
|
|
sigEl(signal),
|
|
secBadge(sec),
|
|
...(isConnected ? [el('span', { class: 'wifi-connected-chip' }, ['\u2713 Connected'])] : []),
|
|
]),
|
|
]);
|
|
|
|
const actionsEl = el('div', { class: 'wifi-card-actions' }, [
|
|
el('button', {
|
|
class: 'btn',
|
|
style: 'font-size:12px;padding:4px 12px;border-radius:99px',
|
|
onclick: () => promptWifiConnect(ssid, sec),
|
|
}, [isConnected ? 'Reconnect' : t('common.connect')]),
|
|
]);
|
|
|
|
const row = el('div', {
|
|
class: `qprow wifi-card${isConnected ? ' connected' : ''}`,
|
|
style: `--i:${idx}`,
|
|
}, [infoEl, actionsEl]);
|
|
|
|
wifiList.appendChild(row);
|
|
});
|
|
}
|
|
|
|
function promptWifiConnect(ssid, sec) {
|
|
const s = (sec || '').toUpperCase();
|
|
const isOpen = !sec || s === 'OPEN' || s === 'NONE' || s === '--' || s === '';
|
|
if (isOpen) {
|
|
connectWifi(ssid, '');
|
|
return;
|
|
}
|
|
|
|
openSysDialog(t('quick.connectWifi'), [
|
|
{ name: 'ssid', label: t('network.title'), value: ssid, readonly: true },
|
|
{ name: 'password', label: t('creds.password'), type: 'password', placeholder: t('creds.password'), autocomplete: 'current-password' },
|
|
], (vals) => {
|
|
connectWifi(vals.ssid, vals.password);
|
|
});
|
|
}
|
|
|
|
async function connectWifi(ssid, password) {
|
|
try {
|
|
toast(t('quick.connectingTo', { ssid }), 2000, 'info');
|
|
await api.post(API.connectWifi, { ssid, password });
|
|
toast(t('quick.connectedTo', { ssid }), 3000, 'success');
|
|
scanWifi();
|
|
} catch (err) {
|
|
toast(t('quick.connectionFailed') + ': ' + (err.message || t('common.unknown')), 3500, 'error');
|
|
}
|
|
}
|
|
|
|
/* =================================================================
|
|
Known networks — load, render, drag-drop, multi-select, upload
|
|
================================================================= */
|
|
|
|
async function loadKnownWifi() {
|
|
if (!knownList) return;
|
|
empty(knownList);
|
|
knownList.appendChild(el('div', { style: 'padding:8px;color:var(--muted);text-align:center' }, [t('common.loading')]));
|
|
|
|
try {
|
|
const data = await api.get(API.getKnownWifi);
|
|
parseAndRenderKnown(data);
|
|
} catch (err) {
|
|
empty(knownList);
|
|
toast(t('quick.loadKnownFailed') + ': ' + (err.message || t('common.unknown')), 3000, 'error');
|
|
}
|
|
}
|
|
|
|
function parseKnownData(data) {
|
|
let networks = [];
|
|
if (Array.isArray(data)) {
|
|
networks = data;
|
|
} else if (data && typeof data === 'object') {
|
|
networks = data.networks || data.known || data.data || data.results || [];
|
|
if (!networks.length) {
|
|
const keys = Object.keys(data);
|
|
if (keys.length === 1 && Array.isArray(data[keys[0]])) {
|
|
networks = data[keys[0]];
|
|
}
|
|
}
|
|
}
|
|
return networks.map((n, i) => ({
|
|
ssid: n.ssid || n.SSID || '(Unknown)',
|
|
priority: n.priority ?? i,
|
|
}));
|
|
}
|
|
|
|
function parseAndRenderKnown(data) {
|
|
knownNetworks = parseKnownData(data);
|
|
selectedSsids.clear();
|
|
renderKnownNetworks();
|
|
}
|
|
|
|
function renderKnownNetworks() {
|
|
if (!knownList) return;
|
|
empty(knownList);
|
|
removeBatchBar();
|
|
|
|
if (!knownNetworks.length) {
|
|
knownList.appendChild(el('div', { style: 'padding:12px;color:var(--muted);text-align:center' }, [t('common.noData')]));
|
|
return;
|
|
}
|
|
|
|
knownNetworks.forEach((net, idx) => {
|
|
const ssid = net.ssid;
|
|
const priority = net.priority;
|
|
|
|
// Checkbox (visible in edit mode)
|
|
const cb = el('input', {
|
|
type: 'checkbox',
|
|
class: 'known-select-cb',
|
|
...(selectedSsids.has(ssid) ? { checked: '' } : {}),
|
|
});
|
|
cb.addEventListener('change', () => {
|
|
if (cb.checked) selectedSsids.add(ssid);
|
|
else selectedSsids.delete(ssid);
|
|
updateBatchBar();
|
|
});
|
|
|
|
// Drag grip
|
|
const grip = el('span', { class: 'known-card-grip', title: 'Drag to reorder' }, ['\u2807']);
|
|
|
|
const info = el('div', { class: 'known-card-info' }, [
|
|
el('span', { class: 'known-card-ssid' }, [ssid]),
|
|
el('span', { class: 'known-card-priority' }, [`Priority: ${priority}`]),
|
|
]);
|
|
|
|
const connectBtn = el('button', {
|
|
class: 'qp-icon-btn',
|
|
title: t('common.connect'),
|
|
onclick: () => connectKnownWifi(ssid),
|
|
}, ['\u21C8']);
|
|
|
|
const deleteBtn = el('button', {
|
|
class: 'qp-icon-btn danger',
|
|
title: t('common.delete'),
|
|
onclick: () => deleteKnown(ssid),
|
|
}, ['\u2715']);
|
|
|
|
const actions = el('div', { class: 'known-card-actions' }, [connectBtn, deleteBtn]);
|
|
|
|
const row = el('div', {
|
|
class: `qprow known-card${editMode ? ' edit-mode' : ''}`,
|
|
style: `--i:${idx}`,
|
|
draggable: editMode ? 'false' : 'true',
|
|
'data-idx': String(idx),
|
|
}, [
|
|
editMode ? cb : grip,
|
|
info,
|
|
actions,
|
|
]);
|
|
|
|
// HTML5 Drag events (desktop)
|
|
if (!editMode) {
|
|
row.addEventListener('dragstart', (e) => onDragStart(e, idx));
|
|
row.addEventListener('dragover', (e) => onDragOver(e, idx));
|
|
row.addEventListener('dragend', onDragEnd);
|
|
row.addEventListener('drop', (e) => onDrop(e, idx));
|
|
|
|
// Touch drag on grip
|
|
const gripEl = row.querySelector('.known-card-grip');
|
|
if (gripEl) {
|
|
gripEl.addEventListener('touchstart', (e) => onTouchDragStart(e, idx, row), { passive: false });
|
|
}
|
|
}
|
|
|
|
knownList.appendChild(row);
|
|
});
|
|
|
|
if (editMode && selectedSsids.size > 0) {
|
|
updateBatchBar();
|
|
}
|
|
}
|
|
|
|
/* ---- Drag & Drop (Desktop) ---- */
|
|
|
|
function onDragStart(e, idx) {
|
|
dragSrcIdx = idx;
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
e.dataTransfer.setData('text/plain', String(idx));
|
|
requestAnimationFrame(() => e.target.classList.add('dragging'));
|
|
}
|
|
|
|
function onDragOver(e, idx) {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'move';
|
|
|
|
// Remove old placeholder
|
|
if (dragPlaceholder && dragPlaceholder.parentNode) {
|
|
dragPlaceholder.parentNode.removeChild(dragPlaceholder);
|
|
}
|
|
|
|
const row = e.currentTarget;
|
|
const rect = row.getBoundingClientRect();
|
|
const midY = rect.top + rect.height / 2;
|
|
|
|
dragPlaceholder = el('div', { class: 'drag-placeholder' });
|
|
|
|
if (e.clientY < midY) {
|
|
row.parentNode.insertBefore(dragPlaceholder, row);
|
|
} else {
|
|
row.parentNode.insertBefore(dragPlaceholder, row.nextSibling);
|
|
}
|
|
}
|
|
|
|
function onDrop(e, targetIdx) {
|
|
e.preventDefault();
|
|
if (dragSrcIdx === null || dragSrcIdx === targetIdx) return;
|
|
|
|
// Calculate insertion index based on placeholder position
|
|
const row = e.currentTarget;
|
|
const rect = row.getBoundingClientRect();
|
|
const insertBefore = e.clientY < rect.top + rect.height / 2;
|
|
let newIdx = insertBefore ? targetIdx : targetIdx + 1;
|
|
if (dragSrcIdx < newIdx) newIdx--;
|
|
|
|
// Reorder array
|
|
const [moved] = knownNetworks.splice(dragSrcIdx, 1);
|
|
knownNetworks.splice(newIdx, 0, moved);
|
|
|
|
onDragEnd();
|
|
commitPriorityOrder();
|
|
}
|
|
|
|
function onDragEnd() {
|
|
dragSrcIdx = null;
|
|
if (dragPlaceholder && dragPlaceholder.parentNode) {
|
|
dragPlaceholder.parentNode.removeChild(dragPlaceholder);
|
|
}
|
|
dragPlaceholder = null;
|
|
// Remove dragging class
|
|
if (knownList) {
|
|
knownList.querySelectorAll('.dragging').forEach(r => r.classList.remove('dragging'));
|
|
}
|
|
renderKnownNetworks();
|
|
}
|
|
|
|
/* ---- Touch Drag (Mobile) ---- */
|
|
|
|
function onTouchDragStart(e, idx, row) {
|
|
if (e.touches.length !== 1) return;
|
|
e.preventDefault();
|
|
|
|
dragSrcIdx = idx;
|
|
touchStartY = e.touches[0].clientY;
|
|
|
|
// Create a floating clone
|
|
const rect = row.getBoundingClientRect();
|
|
touchDragEl = row.cloneNode(true);
|
|
touchDragEl.style.cssText = `
|
|
position:fixed;left:${rect.left}px;top:${rect.top}px;
|
|
width:${rect.width}px;opacity:.8;z-index:999;
|
|
pointer-events:none;transform:scale(1.02);
|
|
box-shadow:0 8px 24px rgba(0,0,0,.4);
|
|
border-radius:12px;
|
|
`;
|
|
document.body.appendChild(touchDragEl);
|
|
row.classList.add('dragging');
|
|
|
|
const onMove = (ev) => {
|
|
if (!touchDragEl) return;
|
|
const dy = ev.touches[0].clientY - touchStartY;
|
|
touchDragEl.style.transform = `scale(1.02) translateY(${dy}px)`;
|
|
|
|
// Determine target
|
|
const touchY = ev.touches[0].clientY;
|
|
const rows = [...knownList.querySelectorAll('.known-card:not(.dragging)')];
|
|
if (dragPlaceholder && dragPlaceholder.parentNode) {
|
|
dragPlaceholder.parentNode.removeChild(dragPlaceholder);
|
|
}
|
|
dragPlaceholder = el('div', { class: 'drag-placeholder' });
|
|
|
|
for (const r of rows) {
|
|
const rr = r.getBoundingClientRect();
|
|
if (touchY < rr.top + rr.height / 2) {
|
|
r.parentNode.insertBefore(dragPlaceholder, r);
|
|
return;
|
|
}
|
|
}
|
|
// After all rows
|
|
if (rows.length) {
|
|
knownList.appendChild(dragPlaceholder);
|
|
}
|
|
};
|
|
|
|
const onEnd = () => {
|
|
document.removeEventListener('touchmove', onMove);
|
|
document.removeEventListener('touchend', onEnd);
|
|
|
|
if (touchDragEl && touchDragEl.parentNode) {
|
|
touchDragEl.parentNode.removeChild(touchDragEl);
|
|
}
|
|
touchDragEl = null;
|
|
|
|
// Calculate new position from placeholder
|
|
if (dragPlaceholder && dragPlaceholder.parentNode && dragSrcIdx !== null) {
|
|
const allChildren = [...knownList.children];
|
|
let newIdx = allChildren.indexOf(dragPlaceholder);
|
|
// Adjust: items before placeholder that aren't the placeholder itself
|
|
if (newIdx > dragSrcIdx) newIdx--;
|
|
if (newIdx < 0) newIdx = 0;
|
|
if (newIdx !== dragSrcIdx) {
|
|
const [moved] = knownNetworks.splice(dragSrcIdx, 1);
|
|
knownNetworks.splice(newIdx, 0, moved);
|
|
commitPriorityOrder();
|
|
}
|
|
}
|
|
|
|
dragSrcIdx = null;
|
|
if (dragPlaceholder && dragPlaceholder.parentNode) {
|
|
dragPlaceholder.parentNode.removeChild(dragPlaceholder);
|
|
}
|
|
dragPlaceholder = null;
|
|
renderKnownNetworks();
|
|
};
|
|
|
|
document.addEventListener('touchmove', onMove, { passive: false });
|
|
document.addEventListener('touchend', onEnd);
|
|
}
|
|
|
|
/* ---- Priority commit ---- */
|
|
|
|
async function commitPriorityOrder() {
|
|
// Assign descending priorities: first item gets N, last gets 1
|
|
const total = knownNetworks.length;
|
|
for (let i = 0; i < total; i++) {
|
|
knownNetworks[i].priority = total - i;
|
|
}
|
|
renderKnownNetworks();
|
|
|
|
// Send updates to backend
|
|
for (const net of knownNetworks) {
|
|
try {
|
|
await api.post(API.updatePriority, { ssid: net.ssid, priority: net.priority });
|
|
} catch (err) {
|
|
toast(`Failed to update priority for ${net.ssid}`, 2000, 'error');
|
|
}
|
|
}
|
|
toast(t('quick.priorityUpdated') || 'Priorities updated', 2000, 'success');
|
|
}
|
|
|
|
/* ---- Multi-select ---- */
|
|
|
|
function toggleEditMode() {
|
|
editMode = !editMode;
|
|
if (!editMode) {
|
|
selectedSsids.clear();
|
|
removeBatchBar();
|
|
}
|
|
renderKnownNetworks();
|
|
}
|
|
|
|
function selectAll() {
|
|
if (selectedSsids.size === knownNetworks.length) {
|
|
selectedSsids.clear();
|
|
} else {
|
|
for (const net of knownNetworks) selectedSsids.add(net.ssid);
|
|
}
|
|
renderKnownNetworks();
|
|
updateBatchBar();
|
|
}
|
|
|
|
function updateBatchBar() {
|
|
removeBatchBar();
|
|
if (!editMode || selectedSsids.size === 0) return;
|
|
|
|
batchBar = el('div', { class: 'qp-batch-bar' }, [
|
|
el('span', { style: 'font-size:13px;color:var(--ink)' }, [`${selectedSsids.size} selected`]),
|
|
el('button', {
|
|
class: 'btn',
|
|
style: 'font-size:12px;padding:4px 14px;color:var(--danger,#ff3b3b);border-color:var(--danger,#ff3b3b)',
|
|
onclick: batchDelete,
|
|
}, [`Delete ${selectedSsids.size}`]),
|
|
]);
|
|
|
|
if (knownWrapper) knownWrapper.appendChild(batchBar);
|
|
}
|
|
|
|
function removeBatchBar() {
|
|
if (batchBar && batchBar.parentNode) {
|
|
batchBar.parentNode.removeChild(batchBar);
|
|
}
|
|
batchBar = null;
|
|
}
|
|
|
|
async function batchDelete() {
|
|
const ssids = [...selectedSsids];
|
|
if (!ssids.length) return;
|
|
|
|
toast(`Deleting ${ssids.length} network(s)...`, 2000, 'info');
|
|
|
|
let ok = 0, fail = 0;
|
|
for (const ssid of ssids) {
|
|
try {
|
|
await api.post(API.deleteKnown, { ssid });
|
|
ok++;
|
|
} catch {
|
|
fail++;
|
|
}
|
|
}
|
|
|
|
selectedSsids.clear();
|
|
editMode = false;
|
|
toast(`Deleted ${ok}${fail ? `, ${fail} failed` : ''}`, 2500, ok ? 'success' : 'error');
|
|
loadKnownWifi();
|
|
}
|
|
|
|
/* ---- Known network actions ---- */
|
|
|
|
async function connectKnownWifi(ssid) {
|
|
try {
|
|
toast(t('quick.connectingTo', { ssid }), 2000, 'info');
|
|
await api.post(API.connectKnown, { ssid });
|
|
toast(t('quick.connectedTo', { ssid }), 3000, 'success');
|
|
} catch (err) {
|
|
toast(t('quick.connectionFailed') + ': ' + (err.message || t('common.unknown')), 3500, 'error');
|
|
}
|
|
}
|
|
|
|
async function deleteKnown(ssid) {
|
|
openSysDialog(t('common.delete'), [
|
|
{ name: 'ssid', label: t('quick.forgetNetworkPrompt') || 'Forget this network?', value: ssid, readonly: true },
|
|
], async (vals) => {
|
|
try {
|
|
await api.post(API.deleteKnown, { ssid: vals.ssid });
|
|
toast('Network removed', 2000, 'success');
|
|
loadKnownWifi();
|
|
} catch (err) {
|
|
toast('Delete failed: ' + (err.message || 'Unknown error'), 3000, 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
/* ---- Potfile import & upload ---- */
|
|
|
|
async function importPotfiles() {
|
|
try {
|
|
toast(t('quick.importingPotfiles') || 'Importing potfiles...', 2000, 'info');
|
|
const res = await api.post(API.importPotfiles);
|
|
const count = res?.imported ?? res?.networks_added?.length ?? '?';
|
|
const skipped = res?.skipped ?? 0;
|
|
const failed = res?.failed ?? 0;
|
|
let msg = t('quick.importedCount', { count }) || `Imported ${count} network(s)`;
|
|
if (skipped) msg += `, ${skipped} skipped`;
|
|
if (failed) msg += `, ${failed} failed`;
|
|
toast(msg, 3000, 'success');
|
|
loadKnownWifi();
|
|
} catch (err) {
|
|
toast(t('studio.importFailed') + ': ' + (err.message || t('common.unknown')), 3000, 'error');
|
|
}
|
|
}
|
|
|
|
function uploadPotfile() {
|
|
const input = el('input', { type: 'file', accept: '.pot,.potfile', style: 'display:none' });
|
|
input.addEventListener('change', async () => {
|
|
const file = input.files?.[0];
|
|
if (!file) return;
|
|
|
|
const fd = new FormData();
|
|
fd.append('potfile', file);
|
|
|
|
try {
|
|
toast(`Uploading ${file.name}...`, 2000, 'info');
|
|
const resp = await fetch(API.uploadPotfile, { method: 'POST', body: fd });
|
|
const data = await resp.json();
|
|
if (data.status === 'success') {
|
|
toast(`Uploaded ${file.name}`, 2000, 'success');
|
|
// Auto-import after upload
|
|
await importPotfiles();
|
|
} else {
|
|
toast(`Upload failed: ${data.message || 'Unknown'}`, 3000, 'error');
|
|
}
|
|
} catch (err) {
|
|
toast('Upload failed: ' + (err.message || 'Unknown error'), 3000, 'error');
|
|
}
|
|
input.remove();
|
|
});
|
|
|
|
document.body.appendChild(input);
|
|
input.click();
|
|
}
|
|
|
|
/* =================================================================
|
|
Bluetooth — scan, pair, trust, connect, disconnect, forget
|
|
================================================================= */
|
|
|
|
async function scanBluetooth() {
|
|
if (scanning.bt) return;
|
|
scanning.bt = true;
|
|
try {
|
|
const data = await api.get(API.scanBluetooth);
|
|
renderBtResults(data);
|
|
} catch (err) {
|
|
toast(t('quick.btScanFailed') + ': ' + (err.message || t('common.unknown')), 3000, 'error');
|
|
} finally {
|
|
scanning.bt = false;
|
|
}
|
|
}
|
|
|
|
function renderBtResults(data) {
|
|
if (!btList) return;
|
|
empty(btList);
|
|
|
|
const devices = Array.isArray(data) ? data : (data?.devices || data?.results || []);
|
|
if (!devices.length) {
|
|
btList.appendChild(el('div', { style: 'padding:12px;color:var(--muted);text-align:center' }, [t('common.noData')]));
|
|
return;
|
|
}
|
|
|
|
// Sort: connected first, then paired, then by name
|
|
devices.sort((a, b) => {
|
|
if (a.connected !== b.connected) return a.connected ? -1 : 1;
|
|
if (a.paired !== b.paired) return a.paired ? -1 : 1;
|
|
return (a.name || '').localeCompare(b.name || '');
|
|
});
|
|
|
|
devices.forEach((dev, idx) => {
|
|
const name = dev.name || dev.Name || '(Unknown)';
|
|
const mac = dev.mac || dev.address || dev.MAC || '';
|
|
const iconHint = dev.icon || dev.Icon || '';
|
|
const paired = !!(dev.paired || dev.Paired);
|
|
const trusted = !!(dev.trusted || dev.Trusted);
|
|
const connected = !!(dev.connected || dev.Connected);
|
|
const rssi = dev.rssi ?? dev.RSSI ?? null;
|
|
|
|
// Icon
|
|
const iconEl = el('span', { class: 'bt-icon' }, [btIcon(iconHint)]);
|
|
|
|
// Device info
|
|
const stateChips = [];
|
|
if (paired) stateChips.push(el('span', { class: 'bt-chip bt-chip-paired' }, ['Paired']));
|
|
if (trusted) stateChips.push(el('span', { class: 'bt-chip bt-chip-trusted' }, ['Trusted']));
|
|
if (connected) stateChips.push(el('span', { class: 'bt-chip bt-chip-connected' }, ['\u2713 Connected']));
|
|
|
|
const infoChildren = [
|
|
el('span', { class: 'bt-device-name' }, [name]),
|
|
el('span', { class: 'bt-device-mac' }, [mac]),
|
|
];
|
|
if (stateChips.length) {
|
|
infoChildren.push(el('div', { class: 'bt-state-chips' }, stateChips));
|
|
}
|
|
|
|
const deviceEl = el('div', { class: 'bt-device' }, [
|
|
iconEl,
|
|
el('div', { class: 'bt-device-info' }, infoChildren),
|
|
...(rssi !== null && rssi > -999 ? [rssiSigEl(rssi)] : []),
|
|
]);
|
|
|
|
// Actions (contextual)
|
|
const actions = [];
|
|
|
|
if (!paired) {
|
|
actions.push(el('button', {
|
|
class: 'btn',
|
|
style: 'font-size:12px;padding:4px 12px;border-radius:99px;background:var(--acid);color:var(--ink-invert,#001014)',
|
|
onclick: () => btAction('pair', mac, name),
|
|
}, [t('quick.pair') || 'Pair']));
|
|
} else {
|
|
if (!trusted) {
|
|
actions.push(el('button', {
|
|
class: 'btn',
|
|
style: 'font-size:12px;padding:4px 10px;border-radius:99px',
|
|
onclick: () => btAction('trust', mac, name),
|
|
}, [t('quick.trust') || 'Trust']));
|
|
}
|
|
if (connected) {
|
|
actions.push(el('button', {
|
|
class: 'btn',
|
|
style: 'font-size:12px;padding:4px 10px;border-radius:99px',
|
|
onclick: () => btAction('disconnect', mac, name),
|
|
}, [t('common.disconnect') || 'Disconnect']));
|
|
} else {
|
|
actions.push(el('button', {
|
|
class: 'btn',
|
|
style: 'font-size:12px;padding:4px 10px;border-radius:99px',
|
|
onclick: () => btAction('connect', mac, name),
|
|
}, [t('common.connect') || 'Connect']));
|
|
}
|
|
actions.push(el('button', {
|
|
class: 'qp-icon-btn danger',
|
|
title: t('common.remove') || 'Forget',
|
|
onclick: () => btForget(mac, name),
|
|
}, ['\u2715']));
|
|
}
|
|
|
|
const row = el('div', {
|
|
class: `qprow${connected ? ' connected' : ''}`,
|
|
style: `grid-template-columns:1fr auto;align-items:center;--i:${idx}`,
|
|
}, [
|
|
deviceEl,
|
|
el('div', { class: 'bt-actions' }, actions),
|
|
]);
|
|
|
|
btList.appendChild(row);
|
|
});
|
|
}
|
|
|
|
async function btAction(action, mac, name) {
|
|
const endpoints = {
|
|
pair: API.pairBluetooth,
|
|
trust: API.trustBluetooth,
|
|
connect: API.connectBluetooth,
|
|
disconnect: API.disconnectBluetooth,
|
|
};
|
|
|
|
const url = endpoints[action];
|
|
if (!url) return;
|
|
|
|
try {
|
|
toast(t('quick.btActioning', { action, name }) || `${action}ing ${name}...`, 2000, 'info');
|
|
await api.post(url, { address: mac, mac });
|
|
toast(t('quick.btActionDone', { action, name }) || `${action} successful for ${name}`, 3000, 'success');
|
|
scanBluetooth();
|
|
} catch (err) {
|
|
toast(t('quick.btActionFailed', { action }) + ': ' + (err.message || t('common.unknown')), 3500, 'error');
|
|
}
|
|
}
|
|
|
|
function btForget(mac, name) {
|
|
openSysDialog(t('quick.forgetDevice') || 'Forget Device', [
|
|
{ name: 'mac', label: t('quick.forgetDevicePrompt', { name }) || `Remove ${name}?`, value: mac, readonly: true },
|
|
], async (vals) => {
|
|
try {
|
|
await api.post(API.forgetBluetooth, { address: vals.mac, mac: vals.mac });
|
|
toast(t('quick.btForgotten', { name }) || `${name} forgotten`, 2000, 'success');
|
|
scanBluetooth();
|
|
} catch (err) {
|
|
toast(t('common.deleteFailed') + ': ' + (err.message || t('common.unknown')), 3000, 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
/* =================================================================
|
|
Auto-scan timers
|
|
================================================================= */
|
|
|
|
function startWifiAutoScan() {
|
|
stopWifiAutoScan();
|
|
wifiAutoTimer = setInterval(() => {
|
|
if (panel && panel.classList.contains('open') && activeTab === 'wifi') scanWifi();
|
|
}, AUTOSCAN_INTERVAL);
|
|
scanWifi();
|
|
}
|
|
|
|
function stopWifiAutoScan() {
|
|
if (wifiAutoTimer) { clearInterval(wifiAutoTimer); wifiAutoTimer = null; }
|
|
}
|
|
|
|
function startBtAutoScan() {
|
|
stopBtAutoScan();
|
|
btAutoTimer = setInterval(() => {
|
|
if (panel && panel.classList.contains('open') && activeTab === 'bt') scanBluetooth();
|
|
}, AUTOSCAN_INTERVAL);
|
|
scanBluetooth();
|
|
}
|
|
|
|
function stopBtAutoScan() {
|
|
if (btAutoTimer) { clearInterval(btAutoTimer); btAutoTimer = null; }
|
|
}
|
|
|
|
/* =================================================================
|
|
Tab switching
|
|
================================================================= */
|
|
|
|
function switchTab(tab) {
|
|
const prevTab = activeTab;
|
|
activeTab = tab;
|
|
|
|
if (tabBtns) {
|
|
tabBtns.forEach(btn => btn.classList.toggle('active', btn.dataset.tab === tab));
|
|
}
|
|
|
|
if (wifiTab) wifiTab.style.display = (tab === 'wifi') ? '' : 'none';
|
|
if (btTab) btTab.style.display = (tab === 'bt') ? '' : 'none';
|
|
|
|
// Stop the inactive tab's timer to save resources
|
|
if (prevTab === 'wifi' && tab !== 'wifi') stopWifiAutoScan();
|
|
if (prevTab === 'bt' && tab !== 'bt') stopBtAutoScan();
|
|
|
|
// Start the active tab's timer if enabled
|
|
if (tab === 'wifi' && getAutoScan(LS_WIFI_AUTO) && panel?.classList.contains('open')) {
|
|
startWifiAutoScan();
|
|
}
|
|
if (tab === 'bt' && getAutoScan(LS_BT_AUTO) && panel?.classList.contains('open')) {
|
|
startBtAutoScan();
|
|
}
|
|
}
|
|
|
|
/* =================================================================
|
|
Panel open / close / toggle
|
|
================================================================= */
|
|
|
|
export function open() {
|
|
if (!panel) return;
|
|
panel.classList.add('open');
|
|
panel.setAttribute('aria-hidden', 'false');
|
|
|
|
loadKnownWifi();
|
|
|
|
// Start auto-scan only for the active tab
|
|
if (activeTab === 'wifi' && getAutoScan(LS_WIFI_AUTO)) startWifiAutoScan();
|
|
if (activeTab === 'bt' && getAutoScan(LS_BT_AUTO)) startBtAutoScan();
|
|
}
|
|
|
|
export function close() {
|
|
if (!panel) return;
|
|
panel.classList.remove('open');
|
|
panel.setAttribute('aria-hidden', 'true');
|
|
|
|
stopWifiAutoScan();
|
|
stopBtAutoScan();
|
|
closeSysDialog();
|
|
|
|
// Exit edit mode on close
|
|
if (editMode) {
|
|
editMode = false;
|
|
selectedSsids.clear();
|
|
removeBatchBar();
|
|
}
|
|
}
|
|
|
|
export function toggle() {
|
|
if (!panel) return;
|
|
if (panel.classList.contains('open')) close();
|
|
else open();
|
|
}
|
|
|
|
/* =================================================================
|
|
Build panel content (init)
|
|
================================================================= */
|
|
|
|
export function init() {
|
|
panel = $('#quickpanel');
|
|
if (!panel) {
|
|
console.warn('[QuickPanel] #quickpanel not found in DOM');
|
|
return;
|
|
}
|
|
|
|
/* ---- Header ---- */
|
|
const closeBtn = el('button', { class: 'qp-close', 'aria-label': t('quick.close'), onclick: close }, ['\u2715']);
|
|
const header = el('div', { class: 'qp-header', style: 'padding:20px 16px 8px' }, [
|
|
el('div', { class: 'qp-head-left' }, [
|
|
el('strong', { style: 'font-size:16px' }, [t('nav.shortcuts')]),
|
|
el('span', { style: 'font-size:11px;color:var(--muted)' }, [t('quick.subtitle')]),
|
|
]),
|
|
closeBtn,
|
|
]);
|
|
|
|
/* ---- Tab bar ---- */
|
|
const wifiTabBtn = el('div', { class: 'qp-tab active', 'data-tab': 'wifi', onclick: () => switchTab('wifi') }, [
|
|
el('span', { class: 'qp-tab-icon' }, ['\u{1F4F6}']),
|
|
t('dash.wifi'),
|
|
]);
|
|
const btTabBtn = el('div', { class: 'qp-tab', 'data-tab': 'bt', onclick: () => switchTab('bt') }, [
|
|
el('span', { class: 'qp-tab-icon' }, ['\u{1F4E1}']),
|
|
t('dash.bluetooth'),
|
|
]);
|
|
tabBtns = [wifiTabBtn, btTabBtn];
|
|
|
|
const tabBar = el('div', { class: 'qp-tabs' }, [wifiTabBtn, btTabBtn]);
|
|
|
|
/* ---- WiFi tab content ---- */
|
|
wifiList = el('div', { class: 'wifilist', style: 'max-height:40vh;overflow-y:auto;padding:0 16px' });
|
|
knownList = el('div', { class: 'knownlist', style: 'max-height:30vh;overflow-y:auto;padding:0 16px' });
|
|
|
|
const wifiScanBtn = el('button', { class: 'btn', style: 'font-size:13px', onclick: scanWifi }, [t('common.refresh')]);
|
|
const potfileBtn = el('button', { class: 'btn', style: 'font-size:13px', onclick: importPotfiles }, [t('quick.importPotfiles') || 'Import Potfiles']);
|
|
const uploadBtn = el('button', { class: 'btn', style: 'font-size:13px', onclick: uploadPotfile }, ['\u2191 Upload']);
|
|
|
|
const wifiAutoCtrl = autoScanToggle(LS_WIFI_AUTO, (on) => {
|
|
if (on && panel.classList.contains('open') && activeTab === 'wifi') startWifiAutoScan();
|
|
else stopWifiAutoScan();
|
|
});
|
|
|
|
const wifiToolbar = el('div', { class: 'qp-toolbar' }, [
|
|
wifiScanBtn, potfileBtn, uploadBtn,
|
|
el('span', { class: 'qp-toolbar-spacer' }),
|
|
wifiAutoCtrl.wrap,
|
|
]);
|
|
|
|
// Known networks section header with edit toggle
|
|
const editBtn = el('button', {
|
|
class: 'qp-icon-btn',
|
|
title: 'Select multiple',
|
|
onclick: toggleEditMode,
|
|
}, ['\u2611']);
|
|
|
|
const selectAllBtn = el('button', {
|
|
class: 'qp-icon-btn',
|
|
title: 'Select all',
|
|
onclick: selectAll,
|
|
style: 'display:none',
|
|
}, ['\u2610']);
|
|
|
|
const knownHeader = el('div', { class: 'qp-section-header' }, [
|
|
el('span', {}, [t('quick.knownNetworks') || 'Saved Networks']),
|
|
el('div', { class: 'qp-section-actions' }, [selectAllBtn, editBtn]),
|
|
]);
|
|
|
|
// Show select all button only in edit mode
|
|
const origToggle = toggleEditMode;
|
|
|
|
knownWrapper = el('div', { style: 'position:relative' }, [knownList]);
|
|
|
|
wifiTab = el('div', { 'data-panel': 'wifi' }, [wifiToolbar, wifiList, knownHeader, knownWrapper]);
|
|
|
|
/* ---- Bluetooth tab content ---- */
|
|
btList = el('div', { class: 'btlist', style: 'max-height:50vh;overflow-y:auto;padding:0 16px' });
|
|
|
|
const btScanBtn = el('button', { class: 'btn', style: 'font-size:13px', onclick: scanBluetooth }, [t('common.refresh')]);
|
|
|
|
const btAutoCtrl = autoScanToggle(LS_BT_AUTO, (on) => {
|
|
if (on && panel.classList.contains('open') && activeTab === 'bt') startBtAutoScan();
|
|
else stopBtAutoScan();
|
|
});
|
|
|
|
const btToolbar = el('div', { class: 'qp-toolbar' }, [
|
|
btScanBtn,
|
|
el('span', { class: 'qp-toolbar-spacer' }),
|
|
btAutoCtrl.wrap,
|
|
]);
|
|
|
|
btTab = el('div', { 'data-panel': 'bt', style: 'display:none' }, [btToolbar, btList]);
|
|
|
|
/* ---- Assemble into panel (after the grip) ---- */
|
|
panel.appendChild(header);
|
|
panel.appendChild(tabBar);
|
|
panel.appendChild(wifiTab);
|
|
panel.appendChild(btTab);
|
|
|
|
/* ---- Global keyboard shortcuts ---- */
|
|
document.addEventListener('keydown', onKeyDown);
|
|
|
|
/* ---- Click outside to close ---- */
|
|
document.addEventListener('pointerdown', onOutsideClick);
|
|
|
|
/* ---- Wire topbar trigger button ---- */
|
|
const openBtn = $('#openQuick');
|
|
if (openBtn) openBtn.addEventListener('click', toggle);
|
|
|
|
/* ---- Page visibility: pause scans when tab hidden ---- */
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.hidden) {
|
|
stopWifiAutoScan();
|
|
stopBtAutoScan();
|
|
} else if (panel && panel.classList.contains('open')) {
|
|
if (activeTab === 'wifi' && getAutoScan(LS_WIFI_AUTO)) startWifiAutoScan();
|
|
if (activeTab === 'bt' && getAutoScan(LS_BT_AUTO)) startBtAutoScan();
|
|
}
|
|
});
|
|
}
|
|
|
|
/* =================================================================
|
|
Event handlers
|
|
================================================================= */
|
|
|
|
function onKeyDown(e) {
|
|
if (e.ctrlKey && e.key === '\\') {
|
|
e.preventDefault();
|
|
toggle();
|
|
return;
|
|
}
|
|
if (e.key === 'Escape' && panel && panel.classList.contains('open')) {
|
|
const dlg = $('#sysDialogBackdrop');
|
|
if (dlg && (dlg.style.display === 'flex' || dlg.classList.contains('show'))) {
|
|
closeSysDialog();
|
|
return;
|
|
}
|
|
close();
|
|
}
|
|
}
|
|
|
|
function onOutsideClick(e) {
|
|
if (!panel || !panel.classList.contains('open')) return;
|
|
if (panel.contains(e.target)) return;
|
|
const openBtn = $('#openQuick');
|
|
if (openBtn && openBtn.contains(e.target)) return;
|
|
const dlg = $('#sysDialogBackdrop');
|
|
if (dlg && dlg.contains(e.target)) return;
|
|
close();
|
|
}
|