mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 07:00:37 +02:00
Merge sideloading, install links and devkit pairing
Combines the three feature branches on bundle-deps. Conflicts in server.py,
index.html, preload.js, README and test_server.py keep both sides. The
web-install downloader now uses urllib's default HTTPS context, so the
bundled CA list from 770f26c applies to it on Windows too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
636a4a47b7
13 files changed
+1541
-10
No files matched your search
+122
-3
@@ -221,10 +221,10 @@
|
||||
.and-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 22px; align-items: start; }
|
||||
.and-col { display: grid; gap: 22px; align-content: start; }
|
||||
.rep-item .s { white-space: normal; }
|
||||
#repDlg, #titleDlg { background: #1e2329; color: var(--text); border: 1px solid rgba(255,255,255,.1); border-radius: 4px;
|
||||
#repDlg, #titleDlg, #wiDlg { background: #1e2329; color: var(--text); border: 1px solid rgba(255,255,255,.1); border-radius: 4px;
|
||||
padding: 22px; width: min(560px, 92vw); box-shadow: 0 20px 60px rgba(0,0,0,.6); }
|
||||
#repDlg::backdrop, #titleDlg::backdrop { background: rgba(0,0,0,.55); }
|
||||
#repDlg h2, #titleDlg h2 { margin: 0 0 14px; font-size: 15px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--bright); }
|
||||
#repDlg::backdrop, #titleDlg::backdrop, #wiDlg::backdrop { background: rgba(0,0,0,.55); }
|
||||
#repDlg h2, #titleDlg h2, #wiDlg h2 { margin: 0 0 14px; font-size: 15px; letter-spacing: 1.5px; text-transform: uppercase; color: var(--bright); }
|
||||
#repForm label, #titleForm label { display: block; font-size: 12.5px; color: var(--muted); margin-top: 10px; }
|
||||
#repForm label input[type=text], #repForm textarea, #titleForm label input, #titleForm label select { margin-top: 5px; }
|
||||
#titleForm select { width: 100%; background: rgba(0,0,0,.28); color: var(--text); border: 1px solid transparent;
|
||||
@@ -233,6 +233,11 @@
|
||||
#titleForm .note { font-size: 12.5px; color: var(--muted); margin-top: 10px; }
|
||||
#titleForm .note.warn { color: #d9a23a; }
|
||||
#titleList { margin-top: 8px; }
|
||||
#wiFacts { display: grid; grid-template-columns: max-content 1fr; gap: 6px 14px; margin: 0; font-size: 13.5px; }
|
||||
#wiFacts dt { color: var(--muted); }
|
||||
#wiFacts dd { margin: 0; color: var(--bright); overflow-wrap: anywhere; }
|
||||
#wiWarn { color: var(--muted); font-size: 12.5px; line-height: 1.45; margin: 14px 0 0; }
|
||||
#wiProg:not([hidden]) { display: block; }
|
||||
#repForm fieldset { border: 0; padding: 0; margin: 12px 0 0; }
|
||||
#repForm legend { font-size: 12.5px; color: var(--muted); padding: 0; margin-bottom: 4px; }
|
||||
#repForm label.opt { display: inline-flex; align-items: center; gap: 6px; margin: 4px 14px 0 0; color: var(--text); font-size: 13.5px; }
|
||||
@@ -592,6 +597,16 @@
|
||||
<button type="submit" class="action small" id="titleInstall">Install</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="wiDlg" aria-labelledby="wiTitle">
|
||||
<h2 id="wiTitle">Install from a website</h2>
|
||||
<dl id="wiFacts"></dl>
|
||||
<p id="wiWarn">A website asked Frame Control to install this. Nothing is downloaded until you click Install.
|
||||
Only install software from sites you trust.</p>
|
||||
<div class="progress" id="wiProg" hidden><i></i></div>
|
||||
<div class="row rep-actions"><span class="sub" id="wiMsg"></span><span class="spacer"></span>
|
||||
<button type="button" class="small" id="wiCancel">Cancel</button>
|
||||
<button type="button" class="action small" id="wiGo" disabled>Install</button></div>
|
||||
</dialog>
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
@@ -1798,6 +1813,110 @@ const spy = new IntersectionObserver(entries => {
|
||||
}, { rootMargin: "-80px 0px -55% 0px" });
|
||||
["view", "shots", "library", "getgames", "android", "transfer", "apps", "display", "power"].forEach(id => spy.observe($(id)));
|
||||
|
||||
// ---- install links from websites (frame-control://install, docs/web-install.md) ----
|
||||
// The app passes each link here. The server checks it and reads the manifest;
|
||||
// nothing downloads until the user clicks Install in this dialog.
|
||||
const wi = { queue: [], open: false, gen: 0, plan: null, job: null, starting: false };
|
||||
function wiSize(n) {
|
||||
if (n == null) return "Not given";
|
||||
return n >= 1e9 ? gb(n) : n >= 1e6 ? (n / 1e6).toFixed(1) + " MB" : Math.max(1, Math.round(n / 1e3)) + " KB";
|
||||
}
|
||||
function wiFacts(rows) { $("wiFacts").innerHTML = rows.map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`).join(""); }
|
||||
function wiButtons(cancel, go) {
|
||||
$("wiCancel").textContent = cancel[0]; $("wiCancel").disabled = !cancel[1];
|
||||
$("wiGo").hidden = !go; $("wiGo").disabled = go !== "on";
|
||||
}
|
||||
async function wiNext() {
|
||||
if (wi.open || !wi.queue.length) return;
|
||||
const req = wi.queue.shift(), gen = ++wi.gen;
|
||||
wi.open = true; wi.plan = null; wi.job = null;
|
||||
let host = "";
|
||||
try { host = new URL(req.target).hostname; } catch {}
|
||||
wiFacts([["From", host], [req.kind === "manifest" ? "Manifest" : "File", req.target]]);
|
||||
$("wiProg").hidden = true; $("wiMsg").textContent = "Checking the link…";
|
||||
wiButtons(["Cancel", true], "off");
|
||||
$("wiDlg").showModal();
|
||||
log(`Install link from ${host}`);
|
||||
try {
|
||||
const p = await api("/api/webinstall/check", { [req.kind]: req.target });
|
||||
if (gen !== wi.gen) return;
|
||||
wi.plan = p;
|
||||
const from = p.host === p.linkHost ? p.host : `${p.linkHost} (file on ${p.host})`;
|
||||
wiFacts([["Title", p.name], ["From", from], ["File", p.file], ["Type", p.kindLabel], ["Size", wiSize(p.size)],
|
||||
["SHA-256", p.sha256 ? "Given; checked after downloading" : "Not given; the download can't be checked"]]);
|
||||
$("wiMsg").textContent = "";
|
||||
wiButtons(["Cancel", true], "on");
|
||||
$("wiGo").focus();
|
||||
} catch (e) {
|
||||
if (gen !== wi.gen) return;
|
||||
$("wiMsg").textContent = e.message;
|
||||
wiButtons(["Close", true], null);
|
||||
log(`Install link refused: ${e.message}`, "e");
|
||||
}
|
||||
}
|
||||
async function wiPoll(gen) {
|
||||
if (gen !== wi.gen || !wi.job) return;
|
||||
let j;
|
||||
try {
|
||||
j = await api(`/api/webinstall/job?id=${encodeURIComponent(wi.job)}`);
|
||||
} catch (e) {
|
||||
$("wiMsg").textContent = e.message;
|
||||
return setTimeout(() => wiPoll(gen), 1500);
|
||||
}
|
||||
if (gen !== wi.gen) return;
|
||||
const bar = $("wiProg").firstElementChild;
|
||||
$("wiProg").hidden = false;
|
||||
if (j.phase === "download") {
|
||||
bar.style.width = j.total ? (100 * j.done / j.total) + "%" : "0";
|
||||
$("wiMsg").textContent = `Downloading ${wiSize(j.done)}` + (j.total ? ` of ${wiSize(j.total)}` : "");
|
||||
wiButtons(["Stop download", true], "off");
|
||||
} else if (j.phase === "install") {
|
||||
bar.style.width = "100%";
|
||||
$("wiMsg").textContent = j.detail || "Installing on the Frame…";
|
||||
wiButtons(["Stop download", false], "off");
|
||||
} else {
|
||||
wi.job = null;
|
||||
$("wiProg").hidden = true;
|
||||
if (j.phase === "done") {
|
||||
log(j.message, "ok"); toast(j.message);
|
||||
$("wiDlg").close();
|
||||
loadAndroid(); refresh();
|
||||
} else {
|
||||
$("wiMsg").textContent = j.error;
|
||||
log(`Install from link failed: ${j.error}`, "e"); toast(`Install failed: ${j.error}`, true);
|
||||
wiButtons(["Close", true], null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
setTimeout(() => wiPoll(gen), 500);
|
||||
}
|
||||
$("wiGo").onclick = async () => {
|
||||
const p = wi.plan, gen = wi.gen;
|
||||
if (!p) return;
|
||||
wi.plan = null; // one click, one install
|
||||
wiButtons(["Cancel", false], "off");
|
||||
$("wiMsg").textContent = "Starting…";
|
||||
wi.starting = true; // the dialog stays open until the job is known
|
||||
try {
|
||||
wi.job = (await api("/api/webinstall/start", { id: p.id })).job;
|
||||
log(`Installing ${p.name} from ${p.host}…`);
|
||||
wiPoll(gen);
|
||||
} catch (e) {
|
||||
$("wiMsg").textContent = e.message;
|
||||
wiButtons(["Close", true], null);
|
||||
} finally { wi.starting = false; }
|
||||
};
|
||||
$("wiCancel").onclick = async () => {
|
||||
if (!wi.job) return $("wiDlg").close();
|
||||
try { await api("/api/webinstall/cancel", { job: wi.job }); } catch (e) { $("wiMsg").textContent = e.message; }
|
||||
};
|
||||
// Escape doesn't close the dialog while an install runs; it keeps showing progress.
|
||||
$("wiDlg").addEventListener("cancel", e => { if (wi.job || wi.starting) e.preventDefault(); });
|
||||
$("wiDlg").addEventListener("close", () => { wi.gen++; wi.open = false; wi.plan = null; wi.job = null; setTimeout(wiNext); });
|
||||
if (window.frameApp && window.frameApp.onInstallLink) {
|
||||
window.frameApp.onInstallLink(req => { if (wi.queue.length < 5) wi.queue.push(req); wiNext(); });
|
||||
}
|
||||
|
||||
setView("headset");
|
||||
refresh().then(loadShots); // after status, so app names resolve
|
||||
setInterval(() => { if (!document.hidden) refresh(); }, 30000);
|
||||
|
||||
Reference in new issue
Block a user