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) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-10-01 21:09:58 +10:00
1 parent fab04f0558
commit 0bceb5c928
2 files changed
+30 -5

No files matched your search

+15 -3
View File
@@ -67,8 +67,10 @@ const log = ()=>{}, toast = ()=>{};
const assert = require('node:assert/strict'); const assert = require('node:assert/strict');
const elements = new Map(); const elements = new Map();
const $ = id => { const $ = id => {
if (!elements.has(id)) elements.set(id, {textContent:'', hidden:true, disabled:false, value:'30', checked:true, // The form's own defaults, as in the page's HTML.
type: /Alert$/.test(id) ? 'checkbox' : 'number'}); 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); return elements.get(id);
}; };
const window = {frameApp:{notify:async()=>{}}}; 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}, 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'}]}); events:[{id:'old:1',kind:'finished',time:90,message:'Session ended'}]});
assert.equal($('sessionMinutes').value, '30'); assert.equal($('sessionMinutes').value, '30');
assert.equal($('breakMinutes').value, '30'); assert.equal($('breakMinutes').value, '20');
assert.equal($('batteryAlert').checked, true); assert.equal($('batteryAlert').checked, true);
assert.match($('comfortEvents').textContent, /Session ended/); assert.match($('comfortEvents').textContent, /Session ended/);
assert.notEqual($('comfortEvents').textContent, 'Session ended'); // it carries a time 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:[]}); 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($('sessionMinutes').value, 45); // a running session shows its own settings
assert.equal($('heatAlert').checked, false); 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) result = subprocess.run(['node', '-e', setup + code + checks], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.returncode, 0, result.stderr)
+15 -2
View File
@@ -1649,7 +1649,7 @@ $("vrUtilities").onclick = async e => {
}; };
// The Frame owns the clock. Poll independently of status and the selected tab. // 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) { async function localNotification(message, request = false) {
try { try {
if (window.frameApp?.notify) await window.frameApp.notify(message, request); 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; 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 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"}` ? `${Math.ceil(s.remaining / 60)} min until Steam Home · ${s.activity == null ? "activity unknown" : s.activity === 3 ? "headset in standby" : "monitoring"}`
: "No session running."); : "No session running.");
@@ -2697,7 +2703,11 @@ $("clipMac").onclick = () => act($("clipMac").textContent, sendComputerClipboard
const drop = $("drop"); const drop = $("drop");
drop.onclick = () => $("fileInput").click(); drop.onclick = () => $("fileInput").click();
drop.onkeydown = e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); $("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. // 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. // Dialogs keep their own drops (none take files today), so the overlay stays out of them.
let dragDepth = 0; let dragDepth = 0;
@@ -3821,6 +3831,7 @@ function showPage() {
const id = location.hash.slice(1); const id = location.hash.slice(1);
const next = PAGES.includes(id) ? id : SECTION_PAGE[id] || "home"; 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 if (next !== page && page === "home" && live) toggleLive(false); // don't stream video nobody is watching
const prev = page;
page = next; page = next;
document.querySelectorAll(".page").forEach(p => p.classList.toggle("on", p.dataset.page === page)); 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)); 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); const section = !PAGES.includes(id) && id && $(id);
if (section) section.scrollIntoView(); else scrollToY(0); if (section) section.scrollIntoView(); else scrollToY(0);
if (page === "tools") { loadMacView(); loadPanels(); } 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); window.addEventListener("hashchange", showPage);
// While a text field has focus, phones hide the bottom tab bar (see body.typing in the CSS). // While a text field has focus, phones hide the bottom tab bar (see body.typing in the CSS).