import { getLibraries, getRecentItems } from '../embyApi.js'; import { connectLobby, joinParty, createParty } from '../party.js'; import { navigate } from '../router.js'; import { tileHtml, wireTileButtons, escapeHtml } from '../tiles.js'; export function renderHome(root) { root.innerHTML = `
`; // Live, not a one-shot fetch: the list (and each card's host/member count) // updates the moment anything changes anywhere - see // server/src/socketHandler.js's "lobby" room. The elapsed-time label is // the one piece that ticks purely client-side (from createdUtc) - no // point re-pushing from the server once a second just for a clock. const lobby = connectLobby((parties) => renderParties(root, parties)); const elapsedTimer = setInterval(() => updateElapsedLabels(root), 1000); loadRecentSections(root); // Called by router.js before rendering whatever comes next - stops the // lobby socket and the ticking clock from running after you've navigated // away (see docs/architecture.md's memory-leak writeup). return function cleanup() { lobby.disconnect(); clearInterval(elapsedTimer); }; } function renderParties(root, parties) { const section = root.querySelector('#partiesSection'); const list = root.querySelector('#partiesCarousel'); if (!section || !list) return; // navigated away mid-flight if (!parties.length) { section.hidden = true; list.innerHTML = ''; return; } section.hidden = false; list.innerHTML = parties.map((p) => `
${escapeHtml(p.itemName || '')} ${escapeHtml(p.hostUserName || '')} · ${p.memberCount} participant${p.memberCount > 1 ? 's' : ''} · --:--
`).join(''); list.querySelectorAll('.joinPartyBtn').forEach((btn) => { btn.addEventListener('click', async () => { btn.disabled = true; try { // Actually register as a member before navigating - the player's // realtime "join" is rejected server-side otherwise (you'd load the // video but never get added to the members list or sync). await joinParty(btn.dataset.partyId); navigate(`/party/${btn.dataset.partyId}`); } catch (err) { alert(err.message); btn.disabled = false; } }); }); updateElapsedLabels(root); } function updateElapsedLabels(root) { const now = Date.now(); root.querySelectorAll('.elapsed').forEach((el) => { const created = Number(el.dataset.created); if (!created) return; el.textContent = formatElapsed(now - created); }); } function formatElapsed(ms) { const totalSeconds = Math.max(0, Math.floor(ms / 1000)); const h = Math.floor(totalSeconds / 3600); const m = Math.floor((totalSeconds % 3600) / 60); const s = totalSeconds % 60; const pad = (n) => String(n).padStart(2, '0'); return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`; } async function loadRecentSections(root) { const container = root.querySelector('#recentSections'); let libraries; try { libraries = await getLibraries(); } catch (err) { container.innerHTML = '

Impossible de charger les bibliothèques Emby.

'; return; } for (const lib of libraries) { if (lib.collectionType === 'music') continue; // music gets its own experience later, not a poster row const section = document.createElement('section'); section.className = 'section'; section.innerHTML = `

Ajouts récents - ${escapeHtml(lib.name)}

`; container.appendChild(section); getRecentItems(lib.id) .then((items) => { if (!items.length) { section.remove(); return; } const carousel = section.querySelector('.carousel'); carousel.innerHTML = items.map(tileHtml).join(''); wireTileButtons(carousel, async (itemId) => { const party = await createParty(itemId); navigate(`/party/${party.partyId}`); }); }) .catch(() => section.remove()); } }