Give the install dialog its own snapshot of installed titles

A Refresh in flight could leave the shared list stale when the dialog
opened. The drop now fetches the list itself and hands it to the dialog.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-26 22:55:42 +10:00
1 parent fd0a284942
commit eb78eba0dc
1 file changed
+9 -9
+9 -9
View File
@@ -1200,8 +1200,10 @@ async function sideloadOne(file, path, isDir = false) {
await act(`Copy ${file.name} to ~/Downloads`, () => upload(file, "push")); await act(`Copy ${file.name} to ~/Downloads`, () => upload(file, "push"));
return; return;
} }
await loadTitles(); // fresh, so the dialog can say whether this replaces an installed title // Its own fresh list, so the dialog can say whether this replaces an installed title.
const choice = await confirmTitle(r.plan, canPush); let installed = null;
try { installed = new Set((await api("/api/titles")).titles.map(t => String(t.id).toLowerCase())); } catch {}
const choice = await confirmTitle(r.plan, canPush, installed);
if (choice === "push") { if (choice === "push") {
api("/api/titles", { action: "discard", token: r.token }).catch(() => {}); api("/api/titles", { action: "discard", token: r.token }).catch(() => {});
await act(`Copy ${file.name} to ~/Downloads`, () => upload(file, "push")); await act(`Copy ${file.name} to ~/Downloads`, () => upload(file, "push"));
@@ -1210,7 +1212,7 @@ async function sideloadOne(file, path, isDir = false) {
if (!choice) { api("/api/titles", { action: "discard", token: r.token }).catch(() => {}); return; } if (!choice) { api("/api/titles", { action: "discard", token: r.token }).catch(() => {}); return; }
await installTitle(r.token, choice); await installTitle(r.token, choice);
} }
function confirmTitle(plan, canPush) { function confirmTitle(plan, canPush, installed) { // installed: a Set of lower-case ids, or null if unknown
return new Promise(resolve => { return new Promise(resolve => {
const dlg = $("titleDlg"); const dlg = $("titleDlg");
$("titlePush").hidden = !canPush; $("titlePush").hidden = !canPush;
@@ -1218,8 +1220,8 @@ function confirmTitle(plan, canPush) {
$("titleName").value = plan.name; $("titleName").value = plan.name;
const idNote = () => { const id = titleId($("titleName").value); const idNote = () => { const id = titleId($("titleName").value);
$("titleIdNote").textContent = !id ? "Needs some letters or digits" $("titleIdNote").textContent = !id ? "Needs some letters or digits"
: !installedTitles ? `Shows in Steam as ${id}. Couldn't check whether it's installed already; if it is, this replaces it` : !installed ? `Shows in Steam as ${id}. Couldn't check whether it's installed already; if it is, this replaces it`
: installedTitles.has(id.toLowerCase()) ? `Replaces the installed ${id}, and everything in its folder on the Frame` : installed.has(id.toLowerCase()) ? `Replaces the installed ${id}, and everything in its folder on the Frame`
: `Shows in Steam as ${id}`; }; : `Shows in Steam as ${id}`; };
$("titleName").oninput = idNote; idNote(); $("titleName").oninput = idNote; idNote();
$("titleExe").innerHTML = plan.candidates.map(c => $("titleExe").innerHTML = plan.candidates.map(c =>
@@ -1276,15 +1278,13 @@ async function installTitle(token, choice) {
} }
} finally { $("prog").style.display = "none"; loadTitles(); } } finally { $("prog").style.display = "none"; loadTitles(); }
} }
let installedTitles = null; // lower-case ids, so the install dialog can warn before replacing one; null: unknown let titlesSeq = 0; // a slower, older request mustn't overwrite a newer list
let titlesSeq = 0; // a slower, older request mustn't overwrite a newer answer
async function loadTitles() { async function loadTitles() {
const seq = ++titlesSeq; const seq = ++titlesSeq;
let list, err; let list, err;
try { list = (await api("/api/titles")).titles; } catch (e) { err = e; } try { list = (await api("/api/titles")).titles; } catch (e) { err = e; }
if (seq !== titlesSeq) return; if (seq !== titlesSeq) return;
if (err) { installedTitles = null; $("titleList").innerHTML = `<div class="sub">${esc(err.message)}</div>`; return; } if (err) { $("titleList").innerHTML = `<div class="sub">${esc(err.message)}</div>`; return; }
installedTitles = new Set(list.map(t => String(t.id).toLowerCase()));
$("titleCount").textContent = list.length ? `${list.length}` : ""; $("titleCount").textContent = list.length ? `${list.length}` : "";
$("titleList").innerHTML = list.length ? list.map(t => ` $("titleList").innerHTML = list.length ? list.map(t => `
<div class="item"> <div class="item">