diff --git a/voice-poc/public/app.js b/voice-poc/public/app.js index 381639c..3bb755f 100644 --- a/voice-poc/public/app.js +++ b/voice-poc/public/app.js @@ -536,14 +536,46 @@ function renderTiles() { renderStageEmpty(); } -function renderScreens() { - els.screens.innerHTML = ''; +// share keys: 'me' or peer id; popped-out shares render in their own window +const poppedWindows = new Map(); // key -> {win, name} + +function activeShares() { const shares = []; - if (S.sharing && S.screenStream) shares.push({ name: `${S.name} (you)`, stream: S.screenStream }); + if (S.sharing && S.screenStream) shares.push({ key: 'me', name: `${S.name} (you)`, stream: S.screenStream }); for (const peer of S.peers.values()) { - if (peer.screenStream) shares.push({ name: peer.name, stream: peer.screenStream }); + if (peer.screenStream) shares.push({ key: peer.id, name: peer.name, stream: peer.screenStream }); } - for (const share of shares) { + return shares; +} + +function renderScreens() { + // prune popouts whose share ended + const live = new Set(activeShares().map((s) => s.key)); + for (const [key, p] of [...poppedWindows]) { + if (!live.has(key)) { + try { p.win.close(); } catch {} + poppedWindows.delete(key); + } + } + + els.screens.innerHTML = ''; + els.screens.classList.toggle('multi', activeShares().length > 1); + for (const share of activeShares()) { + if (poppedWindows.has(share.key)) { + // rendered in its own window — show a small placeholder card to re-embed + const ph = document.createElement('div'); + ph.className = 'screen-card popped'; + const btn = document.createElement('button'); + btn.className = 'pop-btn'; + btn.textContent = '⧉ re-embed'; + btn.onclick = () => closePopout(share.key); + const label = document.createElement('div'); + label.className = 'label'; + label.textContent = `${share.name} — popped out`; + ph.append(btn, label); + els.screens.appendChild(ph); + continue; + } const card = document.createElement('div'); card.className = 'screen-card'; const video = document.createElement('video'); @@ -556,11 +588,47 @@ function renderScreens() { label.className = 'label'; label.innerHTML = '●'; label.appendChild(document.createTextNode(share.name)); - card.append(video, label); + const pop = document.createElement('button'); + pop.className = 'pop-btn'; + pop.title = 'Pop out in new window'; + pop.textContent = '⧉'; + pop.onclick = () => popOutShare(share); + card.append(video, label, pop); els.screens.appendChild(card); } } +function popOutShare(share) { + if (poppedWindows.has(share.key)) return; + const win = window.open('', `share-${share.key}`, 'width=960,height=540'); + if (!win) { + toast('Pop-out blocked by the browser — allow pop-ups for this page', 'error'); + return; + } + win.document.write(`${share.name} — screen share + + `); + win.document.close(); + const v = win.document.querySelector('video'); + v.srcObject = share.stream; + v.play().catch(() => {}); + poppedWindows.set(share.key, { win, name: share.name }); + win.addEventListener('beforeunload', () => { + poppedWindows.delete(share.key); + renderScreens(); + }); + renderScreens(); +} + +function closePopout(key) { + const p = poppedWindows.get(key); + if (p) { + poppedWindows.delete(key); + try { p.win.close(); } catch {} + renderScreens(); + } +} + // --------------------------------------------------------------------------- // spatial canvas // --------------------------------------------------------------------------- diff --git a/voice-poc/public/index.html b/voice-poc/public/index.html index 321f46b..b784126 100644 --- a/voice-poc/public/index.html +++ b/voice-poc/public/index.html @@ -88,6 +88,6 @@
- + diff --git a/voice-poc/public/style.css b/voice-poc/public/style.css index 0d7c60e..f591612 100644 --- a/voice-poc/public/style.css +++ b/voice-poc/public/style.css @@ -106,6 +106,18 @@ body { #stage:not(.empty) #stage-empty { display: none; } #screens { display: flex; gap: 12px; flex-wrap: wrap; } +#screens.multi { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); align-content: start; } +#screens.multi .screen-card { flex: none; } +.screen-card.popped { + min-height: 120px; display: grid; place-items: center; + background: var(--bg-0); box-shadow: none; border: 1px dashed var(--bg-4); +} +.pop-btn { + position: absolute; right: 8px; top: 8px; z-index: 2; + background: rgba(0,0,0,.65); color: var(--text-0); border: none; cursor: pointer; + border-radius: 5px; padding: 3px 8px; font-size: 13px; font-weight: 600; +} +.pop-btn:hover { background: var(--brand); } .screen-card { position: relative; background: #000; border-radius: var(--radius); overflow: hidden; flex: 1 1 480px; max-width: 100%; box-shadow: 0 8px 24px rgba(0,0,0,.4);