navigatable views, fullscreen exit fix

AuthorKonata <konata@posteo.jp>
Date
Commit66f72890439a4c5dc9a99f2ffed3e94aa9877d50
Parent9bdf484
1 file changed, 130 insertions(+), 8 deletions(-)
▾Mindex.html
@@ -321,6 +321,7 @@
.video-tile { position: relative; background: #050608; border-radius: var(--radius); overflow: hidden; min-height: 0; display: flex; align-items: center; justify-content: center; }
.video-tile > video { width: 100%; height: 100%; object-fit: contain; background: #050608; display: block; }
.video-tile.screen > video { cursor: zoom-in; }
.video-tile.screen > video:fullscreen { cursor: zoom-out; }
.video-tile .tile-label {
position: absolute; left: 8px; bottom: 8px;
background: rgba(0,0,0,0.55); color: white;
@@ -2975,9 +2976,121 @@ async function startLoopback() {
/* -------------------------------------------------------------------------
UI: views + event wiring
------------------------------------------------------------------------- */
function showView(id) {
document.querySelectorAll('.view').forEach(v => v.classList.add('hidden'));
const viewToHash = {
'view-welcome': 'welcome',
'view-configure': 'configure',
'view-exchange': 'exchange',
'view-sdp-inspect': 'sdp-inspect',
'view-call': 'call',
};
const hashToView = Object.fromEntries(
Object.entries(viewToHash).map(([v, h]) => [h, v])
);
function clearViewInputs(viewEl) {
viewEl.querySelectorAll('input, textarea').forEach(el => {
const t = (el.type || '').toLowerCase();
if (t === 'button' || t === 'submit' || t === 'reset' || t === 'file' ||
t === 'checkbox' || t === 'radio') return;
el.value = '';
});
}
/* Reset transient UI state held outside <input>/<textarea> elements. Called
only for views the user is actually leaving — not on initial page load
for views that were never visible — so App-global state like the log
buffer isn't wiped on every fresh start. */
function resetViewState(id) {
if (id === 'view-sdp-inspect') {
document.getElementById('sdp-inspect-out')?.replaceChildren();
const status = document.getElementById('sdp-inspect-status');
if (status) { status.textContent = ''; status.className = 'pill'; }
} else if (id === 'view-exchange') {
document.getElementById('step-1-body')?.replaceChildren();
document.getElementById('step-2-body')?.replaceChildren();
document.getElementById('step-2-card')?.classList.add('hidden');
document.getElementById('exch-progress')?.classList.add('hidden');
} else if (id === 'view-configure') {
document.getElementById('cfg-progress')?.classList.add('hidden');
} else if (id === 'view-call') {
document.getElementById('chat-log')?.replaceChildren();
if (App.files && App.files.clearAll) {
App.files.clearAll('out');
App.files.clearAll('in');
}
if (App.log && App.log.clear) App.log.clear();
document.getElementById('console-drawer')?.classList.add('hidden');
}
}
function showView(id, opts) {
const wasVisible = new Set();
document.querySelectorAll('.view').forEach(v => {
if (!v.classList.contains('hidden')) wasVisible.add(v.id);
v.classList.add('hidden');
if (v.id !== id) clearViewInputs(v);
});
document.getElementById(id).classList.remove('hidden');
wasVisible.forEach(vid => { if (vid !== id) resetViewState(vid); });
if (opts && opts.push === false) return;
const url = '#' + viewToHash[id];
const state = { view: id };
/* The call view holds a live RTCPeerConnection — we don't want a normal
back/forward stop on it. Replace, so back lands where we came from. */
if (id === 'view-call' || (history.state && history.state.view === id)) {
history.replaceState(state, '', url);
} else {
history.pushState(state, '', url);
}
}
function currentView() {
for (const id of Object.keys(viewToHash)) {
const el = document.getElementById(id);
if (el && !el.classList.contains('hidden')) return id;
}
return null;
}
function onPopstate(event) {
const wasOnCall = currentView() === 'view-call';
const hashView = hashToView[(location.hash || '').replace(/^#/, '')];
const target = (event.state && event.state.view) || hashView || 'view-welcome';
if (wasOnCall) {
if (!confirm('Hang up and leave the call?')) {
history.pushState({ view: 'view-call' }, '', '#call');
return;
}
resetSession();
history.replaceState({ view: 'view-welcome' }, '', '#welcome');
showView('view-welcome', { push: false });
return;
}
/* Exchange and call need in-flight state (offer, live pc) that doesn't
exist when reached via back/forward — redirect to welcome. */
if (target === 'view-exchange' || target === 'view-call') {
if (App.state.pc || App.state.pcB || App.state.role) resetSession();
history.replaceState({ view: 'view-welcome' }, '', '#welcome');
showView('view-welcome', { push: false });
return;
}
if (App.state.pc || App.state.pcB) resetSession();
if (target === 'view-welcome' || target === 'view-sdp-inspect') {
if (App.state.role) { App.state.role = null; updateRoleBadge(); }
}
showView(target, { push: false });
}
function resolveInitialView() {
const hash = (location.hash || '').replace(/^#/, '');
const requested = hashToView[hash];
const reloadSafe = requested === 'view-welcome' || requested === 'view-sdp-inspect';
const target = reloadSafe ? requested : 'view-welcome';
history.replaceState({ view: target }, '', '#' + viewToHash[target]);
showView(target, { push: false });
}
function updateRoleBadge() {
@@ -4070,6 +4183,9 @@ App.sdpInspect = (() => {
Wire up everything on DOMContentLoaded
------------------------------------------------------------------------- */
function wire() {
window.addEventListener('popstate', onPopstate);
resolveInitialView();
/* Welcome */
document.querySelectorAll('#view-welcome .role-picker button').forEach(b =>
b.addEventListener('click', () => pickRole(b.dataset.role)));
@@ -4296,7 +4412,7 @@ function wire() {
});
document.getElementById('tb-hangup').addEventListener('click', hangup);
/* Click a tile that's showing a screen share → fullscreen. */
/* Click a tile that's showing a screen share → toggle fullscreen. */
for (const id of ['tile-local', 'tile-remote']) {
document.getElementById(id).addEventListener('click', e => {
const tile = e.currentTarget;
@@ -4305,9 +4421,11 @@ function wire() {
if (e.target.closest('.pip')) return;
const video = tile.querySelector(':scope > video');
if (!video) return;
const target = video;
if (!document.fullscreenElement) {
(target.requestFullscreen?.() || target.webkitRequestFullscreen?.() || Promise.resolve())
if (document.fullscreenElement) {
(document.exitFullscreen?.() || document.webkitExitFullscreen?.() || Promise.resolve())
.catch?.(err => App.log.warn('ui', 'exit fullscreen failed', err.message));
} else {
(video.requestFullscreen?.() || video.webkitRequestFullscreen?.() || Promise.resolve())
.catch?.(err => App.log.warn('ui', 'fullscreen failed', err.message));
}
});
@@ -4520,13 +4638,17 @@ function setInCallControlsEnabled(enabled) {
if (filesDrop) filesDrop.classList.toggle('disabled', !enabled);
}
function hangup(opts) {
App.log.info('app', 'hangup');
function resetSession(opts) {
teardownConnection(opts);
App.state.role = null;
updateRoleBadge();
const dlg = document.getElementById('peer-left-dialog');
if (dlg && dlg.open) dlg.close();
}
function hangup(opts) {
App.log.info('app', 'hangup');
resetSession(opts);
showView('view-welcome');
}