Files
BackOne/app-shell/dist/main.js
T
2026-06-30 13:10:35 +07:00

238 lines
7.8 KiB
JavaScript

window.BackOneAddons = window.BackOneAddons || {};
const addonButtons = {};
let addonModules = {};
let coreRefreshTimer = null;
function hashAddonId() {
const h = (location.hash || '').replace(/^#\/?/, '').toLowerCase();
if (h === 'voice' || h === 'video' || h === 'chat') return h;
return '';
}
function activateAddon(id) {
const btn = addonButtons[id];
if (!btn) return;
btn.click();
}
function escapeHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function networkLabel(n) {
if (!n) return '';
return n.name ? `${n.name} (${n.id})` : n.id;
}
function updateCoreBadge(statusEl) {
const badge = document.getElementById('core-status-badge');
const core = BackOneNetworkSession.getCoreStatus();
if (badge) {
badge.textContent = core.online ? 'Online' : 'Offline';
badge.classList.toggle('online', core.online);
badge.classList.toggle('offline', !core.online);
}
if (!statusEl) return;
if (!core.online) {
statusEl.textContent = 'Start the backone service to use networks and peers';
return;
}
const net = BackOneNetworkSession.getActiveNetwork();
const netPart = net ? networkLabel(net) : 'no network joined';
statusEl.textContent = `${netPart} · ${core.leafPeerCount} peer(s)`;
}
async function initAuthTokenBar(statusEl, onSaved) {
const bar = document.getElementById('auth-token-bar');
const input = document.getElementById('auth-token-input');
const saveBtn = document.getElementById('auth-token-save');
if (!bar || !input || !saveBtn) return;
async function refreshAuthUi() {
const ok = await BackOneAuth.validateCurrentToken();
bar.classList.toggle('auth-token-hidden', ok);
if (!ok && !input.value) {
input.placeholder = 'Paste from ~/.backOneOneAuthToken';
}
return ok;
}
saveBtn.addEventListener('click', async () => {
const token = input.value.trim();
if (!token) {
if (statusEl) statusEl.textContent = 'Paste the token from ~/.backOneOneAuthToken';
return;
}
saveBtn.disabled = true;
await BackOneAuth.setUserToken(token);
BackOneAuth.invalidate();
const ok = await BackOneAuth.validateCurrentToken();
if (!ok) {
if (statusEl) statusEl.textContent = 'Token rejected — check ~/.backOneOneAuthToken';
saveBtn.disabled = false;
return;
}
input.value = '';
bar.classList.add('auth-token-hidden');
if (typeof onSaved === 'function') await onSaved();
saveBtn.disabled = false;
});
await refreshAuthUi();
return refreshAuthUi;
}
async function initNetworkBar() {
const bar = document.getElementById('network-bar');
if (!bar) return;
const netSel = bar.querySelector('#global-network');
const joinInput = bar.querySelector('#global-join-id');
const joinBtn = bar.querySelector('#global-join-btn');
const leaveBtn = bar.querySelector('#global-leave-btn');
const statusEl = bar.querySelector('#global-network-status');
async function reloadNetworkList(selectId) {
await BackOneNetworkSession.syncFromCore();
const networks = await BackOneNetworkSession.loadNetworks();
netSel.innerHTML = networks.map(n =>
`<option value="${n.id}">${escapeHtml(networkLabel(n))} · ${escapeHtml(n.status || 'OK')}</option>`
).join('') || '<option value="">— no networks —</option>';
const want = (selectId || BackOneNetworkSession.getNetworkId() || '').toLowerCase();
if (want && networks.some(n => (n.id || '').toLowerCase() === want)) {
netSel.value = networks.find(n => (n.id || '').toLowerCase() === want).id;
await BackOneNetworkSession.selectNetwork(netSel.value);
} else if (networks.length === 1) {
netSel.value = networks[0].id;
await BackOneNetworkSession.selectNetwork(networks[0].id);
} else if (networks.length > 0 && BackOneNetworkSession.getNetworkId()) {
netSel.value = BackOneNetworkSession.getNetworkId();
}
updateCoreBadge(statusEl);
}
netSel.addEventListener('change', async () => {
if (!netSel.value) return;
statusEl.textContent = 'Switching network…';
await BackOneNetworkSession.selectNetwork(netSel.value);
await BackOneNetworkSession.syncFromCore();
updateCoreBadge(statusEl);
});
joinBtn.addEventListener('click', async () => {
const id = BackOnePeerDirectory.normalizeNetworkId(joinInput.value);
if (!id) {
statusEl.textContent = 'Enter a 16-digit hexadecimal network ID';
return;
}
joinBtn.disabled = true;
leaveBtn.disabled = true;
statusEl.textContent = `Joining ${id}…`;
try {
await BackOneNetworkSession.joinNetwork(id);
joinInput.value = '';
await reloadNetworkList(id);
} catch (err) {
statusEl.textContent = err.message || 'Join failed';
} finally {
joinBtn.disabled = false;
leaveBtn.disabled = false;
}
});
leaveBtn.addEventListener('click', async () => {
const id = netSel.value || BackOneNetworkSession.getNetworkId();
if (!id) {
statusEl.textContent = 'Select a network to leave';
return;
}
if (!confirm(`Leave network ${networkLabel(BackOneNetworkSession.getActiveNetwork() || { id })}?`)) return;
joinBtn.disabled = true;
leaveBtn.disabled = true;
statusEl.textContent = `Leaving ${id}…`;
try {
await BackOneNetworkSession.leaveNetwork(id);
await reloadNetworkList();
} catch (err) {
statusEl.textContent = err.message || 'Leave failed';
} finally {
joinBtn.disabled = false;
leaveBtn.disabled = false;
}
});
BackOneNetworkSession.subscribe(() => updateCoreBadge(statusEl));
const refreshAuthUi = await initAuthTokenBar(statusEl, () => reloadNetworkList());
await reloadNetworkList();
if (refreshAuthUi) await refreshAuthUi();
coreRefreshTimer = setInterval(() => {
BackOneNetworkSession.syncFromCore().then(() => updateCoreBadge(statusEl));
}, 45000);
}
async function initAppShell() {
await BackOneAuth.init();
await initNetworkBar();
const nav = document.getElementById('addon-nav');
const panel = document.getElementById('addon-panel');
const addons = await BackOneAddonLoader.loadAddonManifest();
if (!addons.length) {
panel.innerHTML = '<p>No UI addons found. Install backone-addon-voice, backone-addon-video, or backone-addon-chat, then run: <code>sudo backone-updater rebuild-manifest</code> and restart backone.</p>';
return;
}
const navOrder = ['chat', 'video', 'voice'];
const sorted = [...addons].sort((a, b) => {
const ia = navOrder.indexOf(a.id);
const ib = navOrder.indexOf(b.id);
return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib);
});
for (const entry of sorted) {
const mod = await BackOneAddonLoader.loadAddonModule(entry);
if (mod) addonModules[entry.id] = mod;
const btn = document.createElement('button');
btn.dataset.addonId = entry.id;
btn.textContent = (entry.frontend && entry.frontend.navLabel) || entry.name || entry.id;
btn.onclick = () => {
nav.querySelectorAll('button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
panel.innerHTML = '';
location.hash = entry.id;
if (addonModules[entry.id] && addonModules[entry.id].mount) {
addonModules[entry.id].mount(panel);
} else {
panel.innerHTML = `<p>Addon "${entry.id}" loaded.</p>`;
}
};
addonButtons[entry.id] = btn;
nav.appendChild(btn);
}
const fromHash = hashAddonId();
if (fromHash && addonButtons[fromHash]) {
activateAddon(fromHash);
} else {
nav.querySelector('button')?.click();
}
}
window.addEventListener('hashchange', () => {
const id = hashAddonId();
if (id) activateAddon(id);
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
BackOneNetworkSession.syncFromCore();
}
});
document.addEventListener('DOMContentLoaded', initAppShell);