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 @@
321321 .video-tile { position: relative; background: #050608; border-radius: var(--radius); overflow: hidden; min-height: 0; display: flex; align-items: center; justify-content: center; }
322322 .video-tile > video { width: 100%; height: 100%; object-fit: contain; background: #050608; display: block; }
323323 .video-tile.screen > video { cursor: zoom-in; }
324+ .video-tile.screen > video:fullscreen { cursor: zoom-out; }
324325 .video-tile .tile-label {
325326 position: absolute; left: 8px; bottom: 8px;
326327 background: rgba(0,0,0,0.55); color: white;
@@ -2975,9 +2976,121 @@ async function startLoopback() {
29752976 /* -------------------------------------------------------------------------
29762977 UI: views + event wiring
29772978 ------------------------------------------------------------------------- */
2978-function showView(id) {
2979- document.querySelectorAll('.view').forEach(v => v.classList.add('hidden'));
2979+const viewToHash = {
2980+ 'view-welcome': 'welcome',
2981+ 'view-configure': 'configure',
2982+ 'view-exchange': 'exchange',
2983+ 'view-sdp-inspect': 'sdp-inspect',
2984+ 'view-call': 'call',
2985+};
2986+const hashToView = Object.fromEntries(
2987+ Object.entries(viewToHash).map(([v, h]) => [h, v])
2988+);
2989+
2990+function clearViewInputs(viewEl) {
2991+ viewEl.querySelectorAll('input, textarea').forEach(el => {
2992+ const t = (el.type || '').toLowerCase();
2993+ if (t === 'button' || t === 'submit' || t === 'reset' || t === 'file' ||
2994+ t === 'checkbox' || t === 'radio') return;
2995+ el.value = '';
2996+ });
2997+}
2998+
2999+/* Reset transient UI state held outside <input>/<textarea> elements. Called
3000+ only for views the user is actually leaving — not on initial page load
3001+ for views that were never visible — so App-global state like the log
3002+ buffer isn't wiped on every fresh start. */
3003+function resetViewState(id) {
3004+ if (id === 'view-sdp-inspect') {
3005+ document.getElementById('sdp-inspect-out')?.replaceChildren();
3006+ const status = document.getElementById('sdp-inspect-status');
3007+ if (status) { status.textContent = ''; status.className = 'pill'; }
3008+ } else if (id === 'view-exchange') {
3009+ document.getElementById('step-1-body')?.replaceChildren();
3010+ document.getElementById('step-2-body')?.replaceChildren();
3011+ document.getElementById('step-2-card')?.classList.add('hidden');
3012+ document.getElementById('exch-progress')?.classList.add('hidden');
3013+ } else if (id === 'view-configure') {
3014+ document.getElementById('cfg-progress')?.classList.add('hidden');
3015+ } else if (id === 'view-call') {
3016+ document.getElementById('chat-log')?.replaceChildren();
3017+ if (App.files && App.files.clearAll) {
3018+ App.files.clearAll('out');
3019+ App.files.clearAll('in');
3020+ }
3021+ if (App.log && App.log.clear) App.log.clear();
3022+ document.getElementById('console-drawer')?.classList.add('hidden');
3023+ }
3024+}
3025+
3026+function showView(id, opts) {
3027+ const wasVisible = new Set();
3028+ document.querySelectorAll('.view').forEach(v => {
3029+ if (!v.classList.contains('hidden')) wasVisible.add(v.id);
3030+ v.classList.add('hidden');
3031+ if (v.id !== id) clearViewInputs(v);
3032+ });
29803033 document.getElementById(id).classList.remove('hidden');
3034+ wasVisible.forEach(vid => { if (vid !== id) resetViewState(vid); });
3035+ if (opts && opts.push === false) return;
3036+ const url = '#' + viewToHash[id];
3037+ const state = { view: id };
3038+ /* The call view holds a live RTCPeerConnection — we don't want a normal
3039+ back/forward stop on it. Replace, so back lands where we came from. */
3040+ if (id === 'view-call' || (history.state && history.state.view === id)) {
3041+ history.replaceState(state, '', url);
3042+ } else {
3043+ history.pushState(state, '', url);
3044+ }
3045+}
3046+
3047+function currentView() {
3048+ for (const id of Object.keys(viewToHash)) {
3049+ const el = document.getElementById(id);
3050+ if (el && !el.classList.contains('hidden')) return id;
3051+ }
3052+ return null;
3053+}
3054+
3055+function onPopstate(event) {
3056+ const wasOnCall = currentView() === 'view-call';
3057+ const hashView = hashToView[(location.hash || '').replace(/^#/, '')];
3058+ const target = (event.state && event.state.view) || hashView || 'view-welcome';
3059+
3060+ if (wasOnCall) {
3061+ if (!confirm('Hang up and leave the call?')) {
3062+ history.pushState({ view: 'view-call' }, '', '#call');
3063+ return;
3064+ }
3065+ resetSession();
3066+ history.replaceState({ view: 'view-welcome' }, '', '#welcome');
3067+ showView('view-welcome', { push: false });
3068+ return;
3069+ }
3070+
3071+ /* Exchange and call need in-flight state (offer, live pc) that doesn't
3072+ exist when reached via back/forward — redirect to welcome. */
3073+ if (target === 'view-exchange' || target === 'view-call') {
3074+ if (App.state.pc || App.state.pcB || App.state.role) resetSession();
3075+ history.replaceState({ view: 'view-welcome' }, '', '#welcome');
3076+ showView('view-welcome', { push: false });
3077+ return;
3078+ }
3079+
3080+ if (App.state.pc || App.state.pcB) resetSession();
3081+ if (target === 'view-welcome' || target === 'view-sdp-inspect') {
3082+ if (App.state.role) { App.state.role = null; updateRoleBadge(); }
3083+ }
3084+ showView(target, { push: false });
3085+}
3086+
3087+function resolveInitialView() {
3088+ const hash = (location.hash || '').replace(/^#/, '');
3089+ const requested = hashToView[hash];
3090+ const reloadSafe = requested === 'view-welcome' || requested === 'view-sdp-inspect';
3091+ const target = reloadSafe ? requested : 'view-welcome';
3092+ history.replaceState({ view: target }, '', '#' + viewToHash[target]);
3093+ showView(target, { push: false });
29813094 }
29823095
29833096 function updateRoleBadge() {
@@ -4070,6 +4183,9 @@ App.sdpInspect = (() => {
40704183 Wire up everything on DOMContentLoaded
40714184 ------------------------------------------------------------------------- */
40724185 function wire() {
4186+ window.addEventListener('popstate', onPopstate);
4187+ resolveInitialView();
4188+
40734189 /* Welcome */
40744190 document.querySelectorAll('#view-welcome .role-picker button').forEach(b =>
40754191 b.addEventListener('click', () => pickRole(b.dataset.role)));
@@ -4296,7 +4412,7 @@ function wire() {
42964412 });
42974413 document.getElementById('tb-hangup').addEventListener('click', hangup);
42984414
4299- /* Click a tile that's showing a screen share → fullscreen. */
4415+ /* Click a tile that's showing a screen share → toggle fullscreen. */
43004416 for (const id of ['tile-local', 'tile-remote']) {
43014417 document.getElementById(id).addEventListener('click', e => {
43024418 const tile = e.currentTarget;
@@ -4305,9 +4421,11 @@ function wire() {
43054421 if (e.target.closest('.pip')) return;
43064422 const video = tile.querySelector(':scope > video');
43074423 if (!video) return;
4308- const target = video;
4309- if (!document.fullscreenElement) {
4310- (target.requestFullscreen?.() || target.webkitRequestFullscreen?.() || Promise.resolve())
4424+ if (document.fullscreenElement) {
4425+ (document.exitFullscreen?.() || document.webkitExitFullscreen?.() || Promise.resolve())
4426+ .catch?.(err => App.log.warn('ui', 'exit fullscreen failed', err.message));
4427+ } else {
4428+ (video.requestFullscreen?.() || video.webkitRequestFullscreen?.() || Promise.resolve())
43114429 .catch?.(err => App.log.warn('ui', 'fullscreen failed', err.message));
43124430 }
43134431 });
@@ -4520,13 +4638,17 @@ function setInCallControlsEnabled(enabled) {
45204638 if (filesDrop) filesDrop.classList.toggle('disabled', !enabled);
45214639 }
45224640
4523-function hangup(opts) {
4524- App.log.info('app', 'hangup');
4641+function resetSession(opts) {
45254642 teardownConnection(opts);
45264643 App.state.role = null;
45274644 updateRoleBadge();
45284645 const dlg = document.getElementById('peer-left-dialog');
45294646 if (dlg && dlg.open) dlg.close();
4647+}
4648+
4649+function hangup(opts) {
4650+ App.log.info('app', 'hangup');
4651+ resetSession(opts);
45304652 showView('view-welcome');
45314653 }
45324654