| 2975 | 2976 | | /* ------------------------------------------------------------------------- |
| 2976 | 2977 | | UI: views + event wiring |
| 2977 | 2978 | | ------------------------------------------------------------------------- */ |
| 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 | + | }); |
| 2980 | 3033 | | 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 }); |
| 2981 | 3094 | | } |
| 2982 | 3095 | | |
| 2983 | 3096 | | function updateRoleBadge() { |