From 0bceb5c92826e3258ee7d8dedfaf332f4bb97b0c Mon Sep 17 00:00:00 2001 From: saphid <4596216+saphid@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:09:58 +1000 Subject: [PATCH] Reload titles on Games, show Home uploads' progress, reset the comfort form From the independent review of this PR: - Phones lost the Refresh button, and nothing else reloaded sideloaded titles, so one added or removed from another computer never showed. Opening Games now reloads them. - Send files... on Home uploaded with its progress bar in the hidden Tools page, and phones have no Activity bar. Choosing files from Home now opens the file panel on Tools. - A session that finished while the page was open left its settings (1 / 0 / 0 after a test) in the form. The form goes back to its defaults when the session ends. The test now covers that, and its mock uses the page's real break default (20 minutes, not 30). Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_comfort_ui.py | 18 +++++++++++++++--- ui/index.html | 17 +++++++++++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/tests/test_comfort_ui.py b/tests/test_comfort_ui.py index c990bc2..4ed21e9 100644 --- a/tests/test_comfort_ui.py +++ b/tests/test_comfort_ui.py @@ -67,8 +67,10 @@ const log = ()=>{}, toast = ()=>{}; const assert = require('node:assert/strict'); const elements = new Map(); const $ = id => { - if (!elements.has(id)) elements.set(id, {textContent:'', hidden:true, disabled:false, value:'30', checked:true, - type: /Alert$/.test(id) ? 'checkbox' : 'number'}); + // The form's own defaults, as in the page's HTML. + const value = {sessionMinutes:'30', breakMinutes:'20', stillMinutes:'30'}[id] || ''; + if (!elements.has(id)) elements.set(id, {textContent:'', hidden:true, disabled:false, value, defaultValue:value, + checked:true, defaultChecked:true, type: /Alert$/.test(id) ? 'checkbox' : 'number'}); return elements.get(id); }; const window = {frameApp:{notify:async()=>{}}}; @@ -78,13 +80,23 @@ const log = ()=>{}, toast = ()=>{}; renderComfort({id:'old', active:false, time:100, options:{minutes:1,breakMinutes:0,stillMinutes:0,batteryAlert:false,heatAlert:false}, events:[{id:'old:1',kind:'finished',time:90,message:'Session ended'}]}); assert.equal($('sessionMinutes').value, '30'); -assert.equal($('breakMinutes').value, '30'); +assert.equal($('breakMinutes').value, '20'); assert.equal($('batteryAlert').checked, true); assert.match($('comfortEvents').textContent, /Session ended/); assert.notEqual($('comfortEvents').textContent, 'Session ended'); // it carries a time renderComfort({id:'new', active:true, time:200, remaining:600, options:{minutes:45,breakMinutes:15,stillMinutes:20,batteryAlert:true,heatAlert:false}, events:[]}); assert.equal($('sessionMinutes').value, 45); // a running session shows its own settings assert.equal($('heatAlert').checked, false); +// The page stays open while that session ends: the next one starts from the defaults again. +renderComfort({id:'new', active:false, time:900, options:{minutes:45,breakMinutes:15,stillMinutes:20,batteryAlert:true,heatAlert:false}, + events:[{id:'new:1',kind:'finished',time:899,message:'Session ended'}]}); +assert.equal($('sessionMinutes').value, '30'); +assert.equal($('breakMinutes').value, '20'); +assert.equal($('heatAlert').checked, true); +renderComfort({id:'new', active:false, time:905, options:{minutes:45}, events:[]}); +$('sessionMinutes').value = '50'; // what's typed for the next session +renderComfort({id:'new', active:false, time:910, options:{minutes:45}, events:[]}); +assert.equal($('sessionMinutes').value, '50'); // later polls leave it alone ''' result = subprocess.run(['node', '-e', setup + code + checks], capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stderr) diff --git a/ui/index.html b/ui/index.html index 9ac7a9b..6112d3e 100644 --- a/ui/index.html +++ b/ui/index.html @@ -1649,7 +1649,7 @@ $("vrUtilities").onclick = async e => { }; // The Frame owns the clock. Poll independently of status and the selected tab. -let comfortBusy = false, comfortSeen = new Set(), comfortSession = null, comfortHydrated = false; +let comfortBusy = false, comfortSeen = new Set(), comfortSession = null, comfortHydrated = false, comfortActive = false; async function localNotification(message, request = false) { try { if (window.frameApp?.notify) await window.frameApp.notify(message, request); @@ -1684,6 +1684,12 @@ function renderComfort(s) { if (el.type === "checkbox") el.checked = value; else el.value = value; } } + // When the running session ends, the form goes back to the page's defaults for the next one. + if (comfortActive && !s.active) for (const id of ["sessionMinutes", "breakMinutes", "stillMinutes", "batteryAlert", "heatAlert"]) { + const el = $(id); + if (el.type === "checkbox") el.checked = el.defaultChecked; else el.value = el.defaultValue; + } + comfortActive = !!s.active; let statusText = s.error || (s.active ? `${Math.ceil(s.remaining / 60)} min until Steam Home ยท ${s.activity == null ? "activity unknown" : s.activity === 3 ? "headset in standby" : "monitoring"}` : "No session running."); @@ -2697,7 +2703,11 @@ $("clipMac").onclick = () => act($("clipMac").textContent, sendComputerClipboard const drop = $("drop"); drop.onclick = () => $("fileInput").click(); drop.onkeydown = e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); $("fileInput").click(); } }; -$("fileInput").onchange = () => sendFiles([...$("fileInput").files]); +$("fileInput").onchange = () => { + // Chosen from Home: show Tools' file panel, where the progress bar is (phones have no Activity bar). + if ($("fileInput").files.length && page !== "tools") location.hash = "#transfer"; + sendFiles([...$("fileInput").files]); +}; // Files can be dropped anywhere in the window; an overlay says what happens. // Dialogs keep their own drops (none take files today), so the overlay stays out of them. let dragDepth = 0; @@ -3821,6 +3831,7 @@ function showPage() { const id = location.hash.slice(1); const next = PAGES.includes(id) ? id : SECTION_PAGE[id] || "home"; if (next !== page && page === "home" && live) toggleLive(false); // don't stream video nobody is watching + const prev = page; page = next; document.querySelectorAll(".page").forEach(p => p.classList.toggle("on", p.dataset.page === page)); document.querySelectorAll("nav a").forEach(a => a.classList.toggle("on", a.getAttribute("href") === "#" + page)); @@ -3829,6 +3840,8 @@ function showPage() { const section = !PAGES.includes(id) && id && $(id); if (section) section.scrollIntoView(); else scrollToY(0); if (page === "tools") { loadMacView(); loadPanels(); } + // Another computer may have added or removed a title: phones have no Refresh button. + if (page === "games" && prev !== "games" && link.s && link.s.phase === "connected") loadTitles(); } window.addEventListener("hashchange", showPage); // While a text field has focus, phones hide the bottom tab bar (see body.typing in the CSS).