Live portal: auto-arrange panels as an even grid on join/leave, Arrange button
Moves existing panels (video keeps playing) into cols=ceil(sqrt(n)) row-major and equalizes group sizes. Verified headless for 3-6 streams. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,25 @@ import GoLive from './GoLive.jsx'
|
||||
const components = { player: Player }
|
||||
const POLL_MS = 3000
|
||||
|
||||
// Lay every panel out as a grid (cols = ceil(sqrt(n)), row-major in stream-start order) and equalize sizes.
|
||||
// Moves the existing panels (dockview keeps the React component and its <video> alive), so viewers do not
|
||||
// reconnect. Called when a stream joins or leaves, and from the "Arrange" button.
|
||||
export function arrange(api) {
|
||||
const panels = [...api.panels].sort((a, b) => ((a.params?.since || '').localeCompare(b.params?.since || '')) || a.id.localeCompare(b.id))
|
||||
const n = panels.length; if (n < 2) return
|
||||
const cols = Math.ceil(Math.sqrt(n))
|
||||
for (let i = 1; i < n; i++) {
|
||||
const r = Math.floor(i / cols), c = i % cols
|
||||
const ref = r === 0 ? panels[i - 1] : panels[(r - 1) * cols + c] // right of the previous in row 0, below the one above otherwise
|
||||
panels[i].api.moveTo({ group: ref.group, position: r === 0 ? 'right' : 'bottom', skipSetActive: true })
|
||||
}
|
||||
const W = api.width || 0, H = api.height || 0; if (!W || !H) return
|
||||
panels.forEach((p, i) => {
|
||||
const c = i % cols, rowsInCol = Math.ceil((n - c) / cols)
|
||||
p.group.api.setSize({ width: Math.floor(W / cols), height: Math.floor(H / rowsInCol) })
|
||||
})
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const apiRef = useRef(null)
|
||||
const [streams, setStreams] = useState([])
|
||||
@@ -25,19 +44,18 @@ export default function App() {
|
||||
const api = apiRef.current; if (!api) return // (in golive mode the dock is unmounted: no viewer connections)
|
||||
const have = new Set(api.panels.map(p => p.id))
|
||||
const want = new Set(list.map(s => s.id))
|
||||
let changed = false
|
||||
for (const s of list) {
|
||||
if (!have.has(s.id)) {
|
||||
const last = api.panels[api.panels.length - 1]
|
||||
api.addPanel({
|
||||
id: s.id, component: 'player', title: `${s.user} · ${s.title}`,
|
||||
params: s,
|
||||
position: last ? { referencePanel: last.id, direction: api.panels.length % 2 ? 'below' : 'right' } : undefined,
|
||||
})
|
||||
api.addPanel({ id: s.id, component: 'player', title: `${s.user} · ${s.title}`, params: s,
|
||||
position: api.panels.length ? { direction: 'right' } : undefined }) // relative to the whole grid; arrange() places it
|
||||
changed = true
|
||||
} else {
|
||||
api.getPanel(s.id)?.api.updateParameters(s)
|
||||
}
|
||||
}
|
||||
for (const p of [...api.panels]) if (!want.has(p.id)) { api.removePanel(p); audio.forget(p.id) }
|
||||
for (const p of [...api.panels]) if (!want.has(p.id)) { api.removePanel(p); audio.forget(p.id); changed = true }
|
||||
if (changed) arrange(api)
|
||||
} catch (e) { if (alive) setErr(e.message) }
|
||||
}
|
||||
tick(); const t = setInterval(tick, POLL_MS)
|
||||
@@ -56,6 +74,7 @@ export default function App() {
|
||||
<span className="count">{streams.length ? `${streams.length} live` : 'nobody is live'}{err ? ` · ${err}` : ''}</span>
|
||||
<span className="spacer" />
|
||||
<span className="count">audio follows the focused window</span>
|
||||
<button onClick={() => apiRef.current && arrange(apiRef.current)} title="lay the windows out as an even grid">Arrange</button>
|
||||
<button onClick={() => audio.muteAll()} title="mute every window until you click one">Mute all</button>
|
||||
<button onClick={() => audio.followFocus()} title="clear pinned audio; only the focused window plays sound">Follow focus</button>
|
||||
<button className="golive-btn" onClick={() => { audio.muteAll(); apiRef.current = null; setMode('golive') }} title="stop watching and broadcast a window from this browser">Go Live</button>
|
||||
|
||||
Reference in New Issue
Block a user