179 lines
6.5 KiB
JavaScript
179 lines
6.5 KiB
JavaScript
// Generated, do not edit. sha256:ab0e4307745aa40cfdb48df36e50ccead564b09ed673bdaf465f9ded7d5651dd
|
|||
|
|
import { listProviders, getProvider } from '../data/providers.js';
|
||
|
|
import { t } from '../i18n/index.js';
|
||
|
|
import { CONFIG, TARGETS, targetHash } from '../../shared/config.js';
|
||
|
|
import { appSetting } from '../app-settings.js';
|
||
|
|
import { initPrivacyBlur } from './blur.js';
|
||
|
|
import { generalAttention, attentionHasProblem, attentionDimmed } from './attention.js';
|
||
|
|
import { openPopover, closePopover } from './popover.js';
|
||
|
|
import { esc } from './escape.js';
|
||
|
|
|
||
|
|
const FADE_OK_MS = 2600;
|
||
|
|
const FADE_FAIL_MS = 6000;
|
||
|
|
const LABEL_MS = 2600;
|
||
|
|
const MAX_COLS = 6;
|
||
|
|
const MAX_ROWS = 3;
|
||
|
|
const MOBILE_MQ = '(max-width: 768px)';
|
||
|
|
|
||
|
|
let rootEl = null;
|
||
|
|
let dotsEl = null;
|
||
|
|
let labelEls = [];
|
||
|
|
let dotTimers = new Map();
|
||
|
|
let labelSlots = [];
|
||
|
|
let dotKeys = '';
|
||
|
|
let attnEl = null;
|
||
|
|
let labelActive = 0;
|
||
|
|
let attnMenuOpen = false;
|
||
|
|
|
||
|
|
async function callableProviders() {
|
||
|
|
const rijen = await listProviders();
|
||
|
|
const configs = await Promise.all(rijen.map(r => getProvider(r.key)));
|
||
|
|
return configs.filter(p => p && p.isActive && ((!p.isSystem && p.endpoints.length > 0) || isAskedService(p)));
|
||
|
|
}
|
||
|
|
|
||
|
|
function isAskedService(p) {
|
||
|
|
if (p.key !== CONFIG.MARKET_SERVICE.PROVIDER_KEY) return false;
|
||
|
|
try { return appSetting('market_spot') !== 'off'; } catch { return false; }
|
||
|
|
}
|
||
|
|
|
||
|
|
function rowsFor(n) {
|
||
|
|
if (n <= 1) return 1;
|
||
|
|
if (n <= MAX_COLS * 2) return 2;
|
||
|
|
return MAX_ROWS;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function syncDots() {
|
||
|
|
const provs = await callableProviders();
|
||
|
|
const keys = provs.map(p => p.key).join('|');
|
||
|
|
if (keys === dotKeys) return;
|
||
|
|
dotKeys = keys;
|
||
|
|
const rows = rowsFor(provs.length);
|
||
|
|
const cols = Math.max(1, Math.ceil(provs.length / rows));
|
||
|
|
dotsEl.style.setProperty('--sb-rows', String(rows));
|
||
|
|
rootEl.style.setProperty('--sb-vak', `max(2.1rem, ${cols * 7 + (cols - 1) * 5}px)`);
|
||
|
|
dotsEl.innerHTML = provs.map(p =>
|
||
|
|
`<button type="button" class="statusbar-dot" tabindex="-1" data-provider-key="${esc(p.key)}" title="${esc(p.name)}" aria-label="${esc(p.name)}"></button>`
|
||
|
|
).join('');
|
||
|
|
dotTimers.clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
function lightDot(key, ok) {
|
||
|
|
const dot = dotsEl.querySelector(`[data-provider-key="${CSS.escape(key)}"]`);
|
||
|
|
if (!dot) return;
|
||
|
|
dot.classList.remove('is-active', 'is-ok', 'is-fail');
|
||
|
|
void dot.offsetWidth;
|
||
|
|
dot.classList.add('is-active', ok ? 'is-ok' : 'is-fail');
|
||
|
|
const prev = dotTimers.get(key);
|
||
|
|
if (prev) clearTimeout(prev);
|
||
|
|
dotTimers.set(key, setTimeout(() => dot.classList.remove('is-active', 'is-ok', 'is-fail'),
|
||
|
|
ok ? FADE_OK_MS : FADE_FAIL_MS));
|
||
|
|
}
|
||
|
|
|
||
|
|
function showSymbol(sym) {
|
||
|
|
if (!sym) return;
|
||
|
|
let slot = labelSlots.find(s => s.ts === 0);
|
||
|
|
if (!slot) slot = labelSlots[0].ts <= labelSlots[1].ts ? labelSlots[0] : labelSlots[1];
|
||
|
|
slot.ts = Date.now();
|
||
|
|
slot.el.textContent = sym;
|
||
|
|
slot.el.classList.remove('is-flash');
|
||
|
|
void slot.el.offsetWidth;
|
||
|
|
slot.el.classList.add('is-flash');
|
||
|
|
labelActive++;
|
||
|
|
syncAttention();
|
||
|
|
if (slot.timer) clearTimeout(slot.timer);
|
||
|
|
slot.timer = setTimeout(() => {
|
||
|
|
slot.el.classList.remove('is-flash'); slot.el.textContent = ''; slot.ts = 0;
|
||
|
|
labelActive = Math.max(0, labelActive - 1);
|
||
|
|
syncAttention();
|
||
|
|
}, LABEL_MS);
|
||
|
|
}
|
||
|
|
|
||
|
|
function syncAttention() {
|
||
|
|
if (!attnEl) return;
|
||
|
|
const items = generalAttention();
|
||
|
|
const show = items.length > 0 && labelActive === 0;
|
||
|
|
const dim = attentionDimmed(items);
|
||
|
|
attnEl.textContent = items.length >= 2 ? String(items.length) : '!';
|
||
|
|
attnEl.classList.toggle('is-shown', show);
|
||
|
|
attnEl.classList.toggle('is-count', items.length >= 2);
|
||
|
|
attnEl.classList.toggle('is-dim', dim);
|
||
|
|
attnEl.classList.toggle('is-problem', attentionHasProblem(items));
|
||
|
|
attnEl.setAttribute('aria-hidden', show ? 'false' : 'true');
|
||
|
|
attnEl.tabIndex = show ? 0 : -1;
|
||
|
|
if (!show && attnMenuOpen) closePopover();
|
||
|
|
}
|
||
|
|
|
||
|
|
function openAttentionMenu() {
|
||
|
|
if (attnMenuOpen) return;
|
||
|
|
const items = generalAttention();
|
||
|
|
if (!items.length) return;
|
||
|
|
const { el, position } = openPopover(attnEl, {
|
||
|
|
className: 'popover attn-menu',
|
||
|
|
onClose: () => { attnMenuOpen = false; },
|
||
|
|
});
|
||
|
|
attnMenuOpen = true;
|
||
|
|
el.innerHTML =
|
||
|
|
`<div class="t-label t-label--ter attn-menu-title">${esc(t('attention.title'))}</div>` +
|
||
|
|
items.map(a => `<button type="button" class="attn-menu-item" data-hash="${esc(a.hash || '')}">${esc(a.label)}</button>`).join('');
|
||
|
|
position();
|
||
|
|
el.addEventListener('click', (e) => {
|
||
|
|
const b = e.target.closest('[data-hash]');
|
||
|
|
if (!b) return;
|
||
|
|
closePopover();
|
||
|
|
if (b.dataset.hash) location.hash = b.dataset.hash;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function place(mq) {
|
||
|
|
const slot = document.getElementById(mq.matches ? 'sb-slot-mobile' : 'sb-slot-desktop');
|
||
|
|
if (slot && rootEl.parentElement !== slot) slot.appendChild(rootEl);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function initStatusBar() {
|
||
|
|
if (rootEl) return;
|
||
|
|
rootEl = document.createElement('div');
|
||
|
|
rootEl.className = 'statusbar';
|
||
|
|
rootEl.setAttribute('role', 'group');
|
||
|
|
rootEl.setAttribute('aria-label', t('integrations.indicatorHint'));
|
||
|
|
rootEl.innerHTML =
|
||
|
|
'<div class="statusbar-dots"></div>' +
|
||
|
|
'<span class="statusbar-div" aria-hidden="true"></span>' +
|
||
|
|
'<div class="statusbar-labels"><span class="statusbar-label"></span><span class="statusbar-label"></span>' +
|
||
|
|
'<button type="button" class="statusbar-attn" aria-hidden="true" tabindex="-1" aria-label="' + esc(t('attention.title')) + '">!</button>' +
|
||
|
|
'</div>' +
|
||
|
|
'<span class="statusbar-div" aria-hidden="true"></span>' +
|
||
|
|
'<div class="statusbar-blur"></div>';
|
||
|
|
|
||
|
|
dotsEl = rootEl.querySelector('.statusbar-dots');
|
||
|
|
labelEls = [...rootEl.querySelectorAll('.statusbar-label')];
|
||
|
|
labelSlots = labelEls.map(el => ({ el, ts: 0, timer: null }));
|
||
|
|
attnEl = rootEl.querySelector('.statusbar-attn');
|
||
|
|
|
||
|
|
attnEl.addEventListener('click', openAttentionMenu);
|
||
|
|
|
||
|
|
initPrivacyBlur(rootEl.querySelector('.statusbar-blur'));
|
||
|
|
syncDots();
|
||
|
|
|
||
|
|
dotsEl.addEventListener('click', (e) => {
|
||
|
|
const dot = e.target.closest('[data-provider-key]');
|
||
|
|
if (!dot) return;
|
||
|
|
try { sessionStorage.setItem('integrations.focus', dot.dataset.providerKey); } catch { }
|
||
|
|
location.hash = targetHash(TARGETS.INTEGRATIONS);
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener('api-call', (e) => {
|
||
|
|
const d = e.detail || {};
|
||
|
|
if (d.provider_key) lightDot(d.provider_key, !!d.ok);
|
||
|
|
if (d.symbol) showSymbol(d.symbol);
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener('providers-changed', syncDots);
|
||
|
|
window.addEventListener('hashchange', syncDots);
|
||
|
|
|
||
|
|
document.addEventListener('attention-changed', syncAttention);
|
||
|
|
syncAttention();
|
||
|
|
|
||
|
|
const mq = window.matchMedia(MOBILE_MQ);
|
||
|
|
mq.addEventListener('change', () => place(mq));
|
||
|
|
place(mq);
|
||
|
|
}
|