Files
AtreidesAssetsFree/client/ui/status-bar.js
T

179 lines
6.5 KiB
JavaScript
Raw Normal View History

2026-10-29 11:57:29 +01:00
// 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);
}