fix
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 1m10s

This commit is contained in:
mats committed 2026-09-23 11:06:21 +02:00
1 parent ee9cffc66d
commit 143dd26c31
3 files changed
+222 -80

No files matched your search

+175 -73
View File
@@ -1073,19 +1073,22 @@ function stopScreenShare(stream, silent) {
// ---------------------------------------------------------------------------
// position sync
// ---------------------------------------------------------------------------
// Fans new joiners out from directly in front of the room's center: the
// first joiner lands straight ahead, then each next one alternates sides,
// widening by 10deg per pair (0, +10, -10, +20, -20, ...) instead of the
// old distance-maximizing grid search, which tended to dump joiners in a
// far corner rather than somewhere natural to face the group.
function chooseOpenParticipantPosition() {
const occupied = [...S.peers.values()].map((peer) => ({ x: peer.x, y: peer.y }));
const candidates = [];
for (let row = 0; row < 5; row++) {
for (let col = 0; col < 7; col++) {
candidates.push({ x: 15 + col * 11.7, y: 15 + row * 17.5 });
}
}
if (!occupied.length) return { x: 50, y: 50 };
return candidates.sort((a, b) => {
const distance = (point) => Math.min(...occupied.map((other) => Math.hypot(point.x - other.x, point.y - other.y)));
return distance(b) - distance(a);
})[0];
const ANCHOR = { x: 50, y: 50 };
const RADIUS = 24;
const n = S.peers.size;
const step = Math.ceil(n / 2) * 10;
const angleDeg = n === 0 ? 0 : (n % 2 === 1 ? step : -step);
const rad = (angleDeg * Math.PI) / 180;
return {
x: ANCHOR.x + RADIUS * Math.sin(rad),
y: ANCHOR.y - RADIUS * Math.cos(rad),
};
}
let lastPosSend = 0;
@@ -1326,6 +1329,7 @@ function renderTiles() {
// share keys: 'me' or peer id; popped-out shares render in their own window
const poppedWindows = new Map(); // key -> {win, name}
let focusedShareKey = null; // clicked remote share, rendered large with the rest as a filmstrip
function activeShares() {
const shares = [];
@@ -1342,80 +1346,164 @@ function activeShares() {
return shares;
}
// Picks the column/row count (out of `count` tiles) that best fills a
// width x height area assuming ~16:9 tiles, so multiple streams can be
// watched at once with as little wasted space as possible.
function bestGrid(count, width, height) {
const aspect = 16 / 9;
let best = { cols: count, rows: 1, area: 0 };
for (let cols = 1; cols <= count; cols++) {
const rows = Math.ceil(count / cols);
let cellW = width / cols;
let cellH = height / rows;
if (cellW / cellH > aspect) cellW = cellH * aspect; else cellH = cellW / aspect;
const area = cellW * cellH;
if (area > best.area) best = { cols, rows, area };
}
return best;
}
// Re-measures the live grid and updates its column/row count only — avoids
// renderScreens()'s full teardown, which would restart <video> playback.
function applyGridLayout() {
const body = els.screens?.querySelector('.screens-grid');
const count = body?.children.length;
if (!body || !count) return;
const rect = body.getBoundingClientRect();
const { cols, rows } = bestGrid(count, rect.width || 800, rect.height || 450);
body.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
body.style.gridTemplateRows = `repeat(${rows}, 1fr)`;
}
let screensResizeObserver = null;
function ensureScreensResizeObserver() {
if (screensResizeObserver || !els.screens || typeof ResizeObserver === 'undefined') return;
let raf = null;
screensResizeObserver = new ResizeObserver(() => {
if (raf) return;
raf = requestAnimationFrame(() => { raf = null; applyGridLayout(); });
});
screensResizeObserver.observe(els.screens);
}
function buildPoppedPlaceholder(share) {
const ph = document.createElement('div');
ph.className = 'screen-card popped';
ph.dataset.shareKey = share.key;
const btn = document.createElement('button');
btn.className = 'pop-btn';
btn.appendChild(mIcon('open_in_new'));
btn.appendChild(document.createTextNode(' 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);
return ph;
}
function buildShareCard(share, focused) {
if (poppedWindows.has(share.key)) return buildPoppedPlaceholder(share);
const card = document.createElement('div');
card.className = 'screen-card' + (focused ? ' focused' : '');
card.dataset.shareKey = share.key;
card.title = focused ? 'Click to exit spotlight' : 'Click to watch this stream large';
const video = document.createElement('video');
video.autoplay = true;
video.playsInline = true;
video.muted = share.local;
video.srcObject = share.stream;
video.play().catch(() => {});
const label = document.createElement('div');
label.className = 'label';
label.innerHTML = '<span class="live">&#9679;</span>';
label.appendChild(document.createTextNode(share.name));
const micBadge = document.createElement('span');
micBadge.className = 'tile-mic-badge';
micBadge.appendChild(mIcon('mic_off'));
label.appendChild(micBadge);
const pop = document.createElement('button');
pop.className = 'pop-btn';
pop.title = 'Pop out in new window';
pop.appendChild(mIcon('open_in_new'));
pop.onclick = (e) => { e.stopPropagation(); popOutShare(share); };
card.append(video, label, pop);
card.addEventListener('click', (e) => {
if (e.target.closest('button')) return;
focusedShareKey = focusedShareKey === share.key ? null : share.key;
renderScreens();
});
return card;
}
function buildOwnPill(share) {
const pill = document.createElement('div');
pill.className = 'own-pill';
pill.dataset.shareKey = share.key;
const dot = document.createElement('span');
dot.className = 'live';
dot.innerHTML = '&#9679;';
const label = document.createElement('span');
label.className = 'own-pill-label';
label.textContent = 'You are sharing';
const stop = document.createElement('button');
stop.className = 'own-pill-close';
stop.title = 'Stop screen share';
stop.appendChild(mIcon('close'));
stop.onclick = () => stopScreenShare(share.stream);
pill.append(dot, label, stop);
return pill;
}
function renderScreens() {
const shares = activeShares();
// prune popouts whose share ended
const live = new Set(activeShares().map((s) => s.key));
const live = new Set(shares.map((s) => s.key));
for (const [key, p] of [...poppedWindows]) {
if (!live.has(key)) {
try { p.win.close(); } catch {}
poppedWindows.delete(key);
}
}
if (focusedShareKey && !live.has(focusedShareKey)) focusedShareKey = null;
if (!els.screens) return;
const ownShares = shares.filter((s) => s.local);
const remoteShares = shares.filter((s) => !s.local);
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.appendChild(mIcon('open_in_new'));
btn.appendChild(document.createTextNode(' 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);
if (share.local) {
const stop = document.createElement('button');
stop.className = 'pop-btn stop-share-btn';
stop.title = 'Stop this screen share';
stop.appendChild(mIcon('stop_circle'));
stop.appendChild(document.createTextNode(' stop'));
stop.onclick = () => stopScreenShare(share.stream);
ph.appendChild(stop);
}
els.screens.appendChild(ph);
continue;
}
const card = document.createElement('div');
card.className = 'screen-card';
card.dataset.shareKey = share.key;
const video = document.createElement('video');
video.autoplay = true;
video.playsInline = true;
video.muted = share.local;
video.srcObject = share.stream;
video.play().catch(() => {});
const label = document.createElement('div');
label.className = 'label';
label.innerHTML = '<span class="live">&#9679;</span>';
label.appendChild(document.createTextNode(share.name));
const micBadge = document.createElement('span');
micBadge.className = 'tile-mic-badge';
micBadge.appendChild(mIcon('mic_off'));
label.appendChild(micBadge);
const pop = document.createElement('button');
pop.className = 'pop-btn';
pop.title = 'Pop out in new window';
pop.appendChild(mIcon('open_in_new'));
pop.onclick = () => popOutShare(share);
card.append(video, label, pop);
if (share.local) {
const stop = document.createElement('button');
stop.className = 'pop-btn stop-share-btn';
stop.title = 'Stop this screen share';
stop.appendChild(mIcon('stop_circle'));
stop.appendChild(document.createTextNode(' stop'));
stop.onclick = () => stopScreenShare(share.stream);
card.appendChild(stop);
}
els.screens.appendChild(card);
if (ownShares.length) {
const pillRow = document.createElement('div');
pillRow.className = 'own-pills';
for (const share of ownShares) pillRow.appendChild(buildOwnPill(share));
els.screens.appendChild(pillRow);
}
if (remoteShares.length) {
const body = document.createElement('div');
if (focusedShareKey) {
body.className = 'screens-body spotlight';
const focused = remoteShares.find((s) => s.key === focusedShareKey);
const rest = remoteShares.filter((s) => s.key !== focusedShareKey);
body.appendChild(buildShareCard(focused, true));
if (rest.length) {
const strip = document.createElement('div');
strip.className = 'thumbs-row';
for (const s of rest) strip.appendChild(buildShareCard(s, false));
body.appendChild(strip);
}
els.screens.appendChild(body);
} else {
body.className = 'screens-body screens-grid';
for (const s of remoteShares) body.appendChild(buildShareCard(s, false));
els.screens.appendChild(body);
applyGridLayout();
ensureScreensResizeObserver();
}
}
updateScreenTileIndicators();
}
@@ -2146,6 +2234,20 @@ dragResize({
});
spatialResizer.addEventListener('mousedown', () => { spatialStartH = 0; });
// spatial pane collapse/expand
const LS_SPATIAL_COLLAPSED = 'voicepoc.spatialCollapsed';
const btnSpatialCollapse = $('#btn-spatial-collapse');
const btnSpatialExpand = $('#btn-spatial-expand');
function setSpatialCollapsed(collapsed) {
document.body.classList.toggle('spatial-collapsed', collapsed);
localStorage.setItem(LS_SPATIAL_COLLAPSED, collapsed ? '1' : '0');
if (!collapsed && typeof cy !== 'undefined') requestAnimationFrame(() => cy.resize());
applyGridLayout();
}
btnSpatialCollapse.onclick = () => setSpatialCollapsed(true);
btnSpatialExpand.onclick = () => setSpatialCollapsed(false);
if (localStorage.getItem(LS_SPATIAL_COLLAPSED) === '1') setSpatialCollapsed(true);
// dropping below the desktop breakpoint: let mobile's own CSS take over sizing
window.addEventListener('resize', () => {
if (isDesktop()) return;