266 lines
8.7 KiB
JavaScript
266 lines
8.7 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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
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 initCommsStack() {
|
|
await BackOneWebHubClient.connect();
|
|
await BackOneWebRtcSession.init();
|
|
|
|
BackOneWebHubClient.subscribe('incoming_call', (msg) => {
|
|
if (BackOneWebRtcSession.isInCall()) return;
|
|
BackOneRingingPopup.showIncomingCall({
|
|
callerName: msg.callerName || msg.callerId,
|
|
callType: msg.callType || 'voice',
|
|
callId: msg.callId,
|
|
onAccept: async () => {
|
|
const type = msg.callType || 'voice';
|
|
const addonId = type === 'video' ? 'video' : 'voice';
|
|
if (addonButtons[addonId]) activateAddon(addonId);
|
|
try {
|
|
await BackOneWebRtcSession.acceptIncomingCall(msg);
|
|
} catch (err) {
|
|
alert(err.message || 'Could not accept call');
|
|
}
|
|
},
|
|
onReject: (callId) => {
|
|
BackOneWebRtcSession.rejectIncomingCall(callId || msg.callId);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function initAppShell() {
|
|
await BackOneAuth.init();
|
|
await initNetworkBar();
|
|
await initCommsStack();
|
|
|
|
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-secure-chat, 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 = ['secure-chat', '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);
|