Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,179 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in new issue
Block a user