Files
2026-06-30 13:10:35 +07:00

194 lines
7.9 KiB
JavaScript

window.BackOnePeerPicker = function PeerPicker(container, options) {
options = options || {};
const onSelect = options.onSelect || (() => {});
const onNetworkChange = options.onNetworkChange || (() => {});
const onAction = options.onAction || null;
const actionLabel = options.actionLabel || 'Select';
const showActionButton = options.showActionButton !== false;
const useSharedNetwork = options.useSharedNetwork !== false;
const layout = options.layout || 'default';
let selectedPeer = null;
let peers = [];
let currentNetwork = null;
let unsub = null;
if (layout === 'sidebar') {
container.innerHTML = `
<div class="peer-picker">
<div id="pp-network-banner" class="peer-info-box"></div>
<label class="peer-picker-row">
<input id="pp-search" type="search" placeholder="Name or peer ID" />
</label>
<ul id="pp-list" class="peer-list"></ul>
</div>`;
} else {
container.innerHTML = `
<div class="peer-picker">
<div class="peer-picker-core">BackOne core · same network as <code>backone-cli</code></div>
<div id="pp-network-banner" class="peer-network-banner"></div>
<label class="peer-picker-row">Search
<input id="pp-search" type="search" placeholder="Name or peer ID" />
</label>
<ul id="pp-list" class="peer-list"></ul>
<div id="pp-selected" class="peer-selected"></div>
${showActionButton ? `<button id="pp-action" type="button" disabled>${actionLabel}</button>` : ''}
</div>`;
}
const netBanner = container.querySelector('#pp-network-banner');
const searchInput = container.querySelector('#pp-search');
const listEl = container.querySelector('#pp-list');
const selectedEl = container.querySelector('#pp-selected');
const actionBtn = container.querySelector('#pp-action');
function networkLabel(n) {
if (!n) return '';
return n.name ? `${n.name} (${n.id})` : n.id;
}
function renderNetworkBanner() {
if (!useSharedNetwork) return;
const id = BackOneNetworkSession.getNetworkId();
const core = BackOneNetworkSession.getCoreStatus();
if (!core.online) {
netBanner.innerHTML = '<span class="peer-network-warn">BackOne offline — peers unavailable</span>';
return;
}
if (!currentNetwork || !id) {
netBanner.innerHTML = '<span class="peer-network-warn">No network — join one in the header bar</span>';
return;
}
const labelSuffix = `${peers.length} contact(s) · ${core.leafPeerCount} VL1 peer(s) from core`;
if (layout === 'sidebar') {
netBanner.textContent = `${networkLabel(currentNetwork)} · ${labelSuffix}`;
} else {
netBanner.textContent = `Network: ${networkLabel(currentNetwork)} · ${textSuffix}`;
}
}
function renderList() {
const q = searchInput.value;
const filtered = BackOnePeerDirectory.searchPeers(q, peers);
const netTag = currentNetwork ? (currentNetwork.name || currentNetwork.id) : '';
listEl.innerHTML = filtered.map(p => {
const lat = p.latency != null ? `${p.latency}ms` : '—';
const link = p.link || 'DIRECT';
const meta = [p.role || 'LEAF', lat, link, netTag].filter(Boolean).join(' · ');
return `
<li class="peer-item ${p.online ? 'online' : 'offline'} ${selectedPeer && selectedPeer.peerId === p.peerId ? 'active' : ''}" data-id="${p.peerId}">
<span class="peer-dot" title="${p.online ? 'Online' : 'Offline'}"></span>
<span class="peer-main">
<strong class="peer-name">${escapeHtml(p.displayName || p.peerId)}</strong>
<small class="peer-sub">${escapeHtml(meta)}</small>
</span>
<button type="button" class="peer-edit" title="Edit nickname">✎</button>
</li>`;
}).join('') || '<li class="peer-empty">No peers on this network</li>';
listEl.querySelectorAll('.peer-item').forEach(li => {
li.addEventListener('click', (ev) => {
if (ev.target.classList.contains('peer-edit')) return;
const id = li.dataset.id;
selectedPeer = peers.find(x => x.peerId === id) || null;
if (selectedPeer) BackOneNetworkSession.setSelectedPeer(selectedPeer);
renderList();
renderSelected();
if (selectedPeer) onSelect(selectedPeer, currentNetwork);
});
const editBtn = li.querySelector('.peer-edit');
if (editBtn) {
editBtn.addEventListener('click', async (ev) => {
ev.stopPropagation();
const id = li.dataset.id;
const peer = peers.find(x => x.peerId === id);
if (!peer) return;
const nick = prompt('Nickname for ' + peer.peerId, peer.nickname || peer.controllerName || '');
if (nick === null) return;
await BackOneContacts.setNickname(peer.peerId, nick);
BackOneContacts.invalidate();
await refreshPeers();
});
}
});
}
function renderSelected() {
if (layout === 'sidebar') return;
if (!selectedPeer) {
if (selectedEl) selectedEl.textContent = '';
if (actionBtn) actionBtn.disabled = true;
return;
}
const net = currentNetwork ? networkLabel(currentNetwork) : BackOneNetworkSession.getNetworkId();
if (selectedEl) {
selectedEl.innerHTML = `Selected: <strong>${escapeHtml(selectedPeer.displayName)}</strong> (${escapeHtml(selectedPeer.peerId)})${net ? ` on <em>${escapeHtml(net)}</em>` : ''}`;
}
if (actionBtn) actionBtn.disabled = !BackOneNetworkSession.getNetworkId();
}
function escapeHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
async function refreshPeers() {
currentNetwork = BackOneNetworkSession.getActiveNetwork();
if (useSharedNetwork && BackOneNetworkSession.getNetworkId() && !currentNetwork) {
currentNetwork = await BackOneNetworkSession.refresh();
}
renderNetworkBanner();
const nwid = BackOneNetworkSession.getNetworkId();
await BackOneContacts.load();
peers = nwid ? await BackOnePeerDirectory.loadPeersForNetwork(nwid) : [];
const lastPeerId = BackOneNetworkSession.getSelectedPeerId();
if (lastPeerId) {
selectedPeer = peers.find(p => p.peerId === lastPeerId) || BackOneNetworkSession.getSelectedPeer();
}
renderList();
renderSelected();
onNetworkChange(currentNetwork, peers);
}
searchInput.addEventListener('input', renderList);
if (actionBtn) {
actionBtn.addEventListener('click', () => {
if (selectedPeer && onAction) onAction(selectedPeer, currentNetwork);
});
}
if (useSharedNetwork) {
unsub = BackOneNetworkSession.subscribe(() => refreshPeers());
}
refreshPeers();
return {
getSelectedPeer: () => selectedPeer,
getSelectedNetwork: () => currentNetwork,
getPeers: () => peers,
refresh: refreshPeers,
destroy: () => { if (unsub) unsub(); }
};
};
window.BackOneCommLayout = {
connectionBarHtml(peer, network) {
const netLabel = network
? (network.name ? `${network.name} (${network.id})` : network.id)
: BackOneNetworkSession.getNetworkId() || '—';
const lat = peer && peer.latency != null ? `${peer.latency}ms` : '—';
const latClass = peer && peer.latency != null && peer.latency < 100 ? 'latency-ok' : '';
return `
<div class="comm-stat"><label>Network ID</label><span>${escapeHtml(netLabel)}</span></div>
<div class="comm-stat"><label>Peer ID</label><span>${peer ? escapeHtml(peer.peerId) : '—'}</span></div>
<div class="comm-stat"><label>Role</label><span>${peer ? escapeHtml(peer.role || 'LEAF') : '—'}</span></div>
<div class="comm-stat"><label>Transport</label><span>${peer ? escapeHtml(peer.link || 'DIRECT') : '—'}</span></div>
<div class="comm-stat"><label>Latency</label><span class="${latClass}">${lat}</span></div>
<div class="comm-stat"><label>Quality</label><span class="comm-quality"><span></span><span></span><span></span><span></span></span></div>`;
}
};
function escapeHtml(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}