Ship Theseus 0.3.1 fe59105d (one-click updates: silent prefetch + install-and-restart)
Setup fe59105d2e99a41b7000caeb86601a8e1675846d193e92204034669f5b368d60
Portable 1b6eda55b53894cf9889548116c7b6100888fb160edc84cc1592bb79f9d95b53
The update flow no longer asks the user to click Download. When
checkForUpdate detects a newer version, autoDownloadUpdate() kicks off
session.defaultSession.downloadURL against the setup URL immediately.
will-download recognises the update URL and routes the file to a
fixed %TEMP% path (bypassing the visible downloads panel entirely),
streams updateDownloadReceived/Total into the chip via
emitUpdateAvailable, and flips updateDownloadState to "ready" when
the transfer finishes.
Chip states:
idle first render before the fetch starts — clickable to
trigger the manual download (kept as a fallback).
downloading "↓ 42% — 0.3.2" — no click, just progress.
ready "✓ Install 0.3.2 & restart" — one click.
failed fall back to the pre-0.3.1 explicit-download click.
install-update-now IPC: spawns the cached setup with /S (detached,
stdio ignored), then app.quit() 400ms later so the installer can
overwrite the running exe. Our nsis/installer.nsh detects an existing
Ariadne install via the HKLM registry and skips its Ariadne prompt on
upgrades, so the /S run is fully unattended.
The one-click flow eliminates two long-standing sources of confusion:
- "Download opens a different browser" — Theseus's default session
fetches the installer itself, not a URL handoff to shell.
- "Update requires multiple wizard clicks" — /S skips them.
Extensions aren't touched by this. The framework lives in
addons-host.js + sidebar-preload.js; add-ons themselves live in
%APPDATA%\Theseus Navigator\addons\<id>\ and are a separate layer.
New extensions ship by drop-a-folder, no browser release required.
Deployed: scp + sia-upload, verified 200 + 0.3.1 in the manifest.
This commit is contained in:
parent
5d248541cf
commit
19ffde3bfa
4 changed files with 108 additions and 10 deletions
24
chrome.html
24
chrome.html
|
|
@ -440,14 +440,30 @@
|
||||||
const chip = $("upchip"); if (!chip) return;
|
const chip = $("upchip"); if (!chip) return;
|
||||||
if (!d) { chip.hidden = true; return; }
|
if (!d) { chip.hidden = true; return; }
|
||||||
chip.hidden = false;
|
chip.hidden = false;
|
||||||
// Prefer the installer URL when both are available — that's the flow
|
|
||||||
// that upgrades in place. Users on the portable can right-click the
|
|
||||||
// chip and choose the portable URL from the shell menu (future).
|
|
||||||
const url = d.setupUrl || d.portableUrl || "";
|
const url = d.setupUrl || d.portableUrl || "";
|
||||||
const core = $("upDownload");
|
const core = $("upDownload");
|
||||||
|
// State machine: idle (nothing yet) -> downloading (silent fetch running,
|
||||||
|
// show percent) -> ready (chip becomes "Install & restart, one click") ->
|
||||||
|
// failed (fall back to explicit user-triggered download).
|
||||||
|
const state = d.downloadState || "idle";
|
||||||
|
if (state === "downloading") {
|
||||||
|
const pct = d.downloadTotal ? Math.floor((d.downloadReceived / d.downloadTotal) * 100) : null;
|
||||||
|
core.textContent = pct != null ? `↓ ${pct}% — ${d.version}` : `↓ Downloading ${d.version}…`;
|
||||||
|
core.title = "Downloading update in the background";
|
||||||
|
core.onclick = () => {};
|
||||||
|
} else if (state === "ready") {
|
||||||
|
core.textContent = `✓ Install ${d.version} & restart`;
|
||||||
|
core.title = "One-click install: launches the installer silently and restarts Theseus";
|
||||||
|
core.onclick = () => T.installUpdateNow && T.installUpdateNow();
|
||||||
|
} else if (state === "failed") {
|
||||||
core.textContent = `↓ Update to ${d.version}`;
|
core.textContent = `↓ Update to ${d.version}`;
|
||||||
core.title = url ? `Open ${url}` : "Download the new version";
|
core.title = url ? `Retry download — ${url}` : "Retry download";
|
||||||
core.onclick = () => { if (url) T.openUpdateDownload(url); };
|
core.onclick = () => { if (url) T.openUpdateDownload(url); };
|
||||||
|
} else {
|
||||||
|
core.textContent = `↓ Update to ${d.version}`;
|
||||||
|
core.title = url ? `Download ${url}` : "Download the new version";
|
||||||
|
core.onclick = () => { if (url) T.openUpdateDownload(url); };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
$("upDismiss") && ($("upDismiss").onclick = () => T.dismissUpdate());
|
$("upDismiss") && ($("upDismiss").onclick = () => T.dismissUpdate());
|
||||||
|
|
||||||
|
|
|
||||||
87
main.js
87
main.js
|
|
@ -299,13 +299,13 @@ async function checkForUpdate() {
|
||||||
if (!versionIsNewer(rel.version, app.getVersion())) {
|
if (!versionIsNewer(rel.version, app.getVersion())) {
|
||||||
// Same version or older — nothing to offer. Clear any stale state so the
|
// Same version or older — nothing to offer. Clear any stale state so the
|
||||||
// chip disappears after the user has updated + relaunched.
|
// chip disappears after the user has updated + relaunched.
|
||||||
if (updateAvailable) { updateAvailable = null; emitUpdateAvailable(); }
|
if (updateAvailable) { updateAvailable = null; updateDownloadState = "idle"; updateDownloadPath = null; emitUpdateAvailable(); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const files = rel.files || {};
|
const files = rel.files || {};
|
||||||
const setupFile = Object.keys(files).find((k) => /Setup/i.test(k));
|
const setupFile = Object.keys(files).find((k) => /Setup/i.test(k));
|
||||||
const portableFile = Object.keys(files).find((k) => /portable/i.test(k));
|
const portableFile = Object.keys(files).find((k) => /portable/i.test(k));
|
||||||
updateAvailable = {
|
const nextAvailable = {
|
||||||
version: rel.version,
|
version: rel.version,
|
||||||
date: rel.date || "",
|
date: rel.date || "",
|
||||||
setupUrl: setupFile ? UPDATE_DOWNLOAD_BASE + setupFile : null,
|
setupUrl: setupFile ? UPDATE_DOWNLOAD_BASE + setupFile : null,
|
||||||
|
|
@ -313,11 +313,48 @@ async function checkForUpdate() {
|
||||||
setupHash: setupFile ? files[setupFile] : null,
|
setupHash: setupFile ? files[setupFile] : null,
|
||||||
portableHash: portableFile ? files[portableFile] : null,
|
portableHash: portableFile ? files[portableFile] : null,
|
||||||
};
|
};
|
||||||
|
const versionChanged = !updateAvailable || updateAvailable.version !== nextAvailable.version;
|
||||||
|
updateAvailable = nextAvailable;
|
||||||
|
if (versionChanged) {
|
||||||
|
// New candidate — reset any prior download state and kick off a fresh
|
||||||
|
// silent background fetch so the chip lands as "ready to install".
|
||||||
|
updateDownloadState = "idle";
|
||||||
|
updateDownloadPath = null;
|
||||||
|
autoDownloadUpdate();
|
||||||
|
}
|
||||||
emitUpdateAvailable();
|
emitUpdateAvailable();
|
||||||
} catch { /* offline / manifest unreachable — silent */ }
|
} catch { /* offline / manifest unreachable — silent */ }
|
||||||
}
|
}
|
||||||
|
// Silent background pre-download of the update installer. The user never
|
||||||
|
// has to click Download — clicking the chip goes straight to Install.
|
||||||
|
// State machine: idle -> downloading -> ready | failed.
|
||||||
|
let updateDownloadState = "idle";
|
||||||
|
let updateDownloadPath = null; // path on disk once "ready"
|
||||||
|
let updateDownloadReceived = 0; // bytes so far
|
||||||
|
let updateDownloadTotal = 0; // total bytes
|
||||||
|
function autoDownloadUpdate() {
|
||||||
|
if (!updateAvailable || !updateAvailable.setupUrl) return;
|
||||||
|
if (updateDownloadState !== "idle") return;
|
||||||
|
updateDownloadState = "downloading";
|
||||||
|
updateDownloadReceived = 0;
|
||||||
|
updateDownloadTotal = 0;
|
||||||
|
try {
|
||||||
|
session.defaultSession.downloadURL(updateAvailable.setupUrl);
|
||||||
|
console.log(`[update] silent fetch started: ${updateAvailable.setupUrl}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("update prefetch failed:", e?.message);
|
||||||
|
updateDownloadState = "failed";
|
||||||
|
}
|
||||||
|
emitUpdateAvailable();
|
||||||
|
}
|
||||||
function emitUpdateAvailable() {
|
function emitUpdateAvailable() {
|
||||||
const payload = (updateDismissedThisSession || !updateAvailable) ? null : updateAvailable;
|
const base = (updateDismissedThisSession || !updateAvailable) ? null : updateAvailable;
|
||||||
|
const payload = base ? {
|
||||||
|
...base,
|
||||||
|
downloadState: updateDownloadState, // idle | downloading | ready | failed
|
||||||
|
downloadReceived: updateDownloadReceived,
|
||||||
|
downloadTotal: updateDownloadTotal,
|
||||||
|
} : null;
|
||||||
try { chrome?.webContents.send("update-available", payload); } catch {}
|
try { chrome?.webContents.send("update-available", payload); } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1557,6 +1594,34 @@ function emitDownloads() {
|
||||||
// attachments, and manual save-as gestures).
|
// attachments, and manual save-as gestures).
|
||||||
function installDownloadTracker() {
|
function installDownloadTracker() {
|
||||||
session.defaultSession.on("will-download", (_e, item /*, wc */) => {
|
session.defaultSession.on("will-download", (_e, item /*, wc */) => {
|
||||||
|
const url = item.getURL();
|
||||||
|
// Update installer? Route it to a fixed temp path, keep it out of the
|
||||||
|
// visible downloads list, drive updateDownloadState instead so the chip
|
||||||
|
// can show "ready to install" and one-click install-and-restart.
|
||||||
|
const isUpdate = updateAvailable && (url === updateAvailable.setupUrl || url === updateAvailable.portableUrl);
|
||||||
|
if (isUpdate) {
|
||||||
|
const dst = path.join(app.getPath("temp"), item.getFilename());
|
||||||
|
try { item.setSavePath(dst); } catch {}
|
||||||
|
updateDownloadTotal = item.getTotalBytes() || 0;
|
||||||
|
updateDownloadReceived = 0;
|
||||||
|
item.on("updated", () => {
|
||||||
|
updateDownloadReceived = item.getReceivedBytes();
|
||||||
|
updateDownloadTotal = item.getTotalBytes() || updateDownloadTotal;
|
||||||
|
emitUpdateAvailable();
|
||||||
|
});
|
||||||
|
item.once("done", (_ev, state) => {
|
||||||
|
if (state === "completed") {
|
||||||
|
updateDownloadPath = item.getSavePath() || dst;
|
||||||
|
updateDownloadState = "ready";
|
||||||
|
console.log(`[update] silent fetch complete: ${updateDownloadPath}`);
|
||||||
|
} else {
|
||||||
|
updateDownloadState = "failed";
|
||||||
|
console.warn(`[update] silent fetch ${state}`);
|
||||||
|
}
|
||||||
|
emitUpdateAvailable();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const id = nextDlId++;
|
const id = nextDlId++;
|
||||||
const rec = {
|
const rec = {
|
||||||
id,
|
id,
|
||||||
|
|
@ -2205,6 +2270,22 @@ ipcMain.handle("open-update-download", (_e, url) => {
|
||||||
});
|
});
|
||||||
ipcMain.handle("dismiss-update", () => { updateDismissedThisSession = true; emitUpdateAvailable(); return true; });
|
ipcMain.handle("dismiss-update", () => { updateDismissedThisSession = true; emitUpdateAvailable(); return true; });
|
||||||
ipcMain.handle("recheck-update", async () => { await checkForUpdate(); return updateAvailable; });
|
ipcMain.handle("recheck-update", async () => { await checkForUpdate(); return updateAvailable; });
|
||||||
|
// One-click "Install & restart". Requires the silent pre-fetch to have
|
||||||
|
// finished (updateDownloadState === "ready"). Launches the setup with /S
|
||||||
|
// (skips the wizard; our nsis/installer.nsh's Ariadne prompt is bypassed
|
||||||
|
// too on upgrades because the Ariadne registry key is already present),
|
||||||
|
// then quits Theseus so the installer can overwrite it. When the installer
|
||||||
|
// finishes, the user re-launches Theseus and lands on the new version.
|
||||||
|
ipcMain.handle("install-update-now", () => {
|
||||||
|
if (updateDownloadState !== "ready" || !updateDownloadPath) return false;
|
||||||
|
try {
|
||||||
|
const p = spawn(updateDownloadPath, ["/S"], { detached: true, stdio: "ignore" });
|
||||||
|
p.unref();
|
||||||
|
} catch (e) { console.warn("update spawn failed:", e?.message); return false; }
|
||||||
|
// Give the child a moment to inherit our arguments before we exit.
|
||||||
|
setTimeout(() => app.quit(), 400);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
// Home page editable cards. Origin-gated to home.html — random pages that
|
// Home page editable cards. Origin-gated to home.html — random pages that
|
||||||
// snoop the preload can't act on the local file.
|
// snoop the preload can't act on the local file.
|
||||||
ipcMain.handle("home-cards-get", (e) => isHomePageSender(e.sender) ? loadHomeCards() : []);
|
ipcMain.handle("home-cards-get", (e) => isHomePageSender(e.sender) ? loadHomeCards() : []);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "theseus-navigator",
|
"name": "theseus-navigator",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||||
"author": "Silent Mode",
|
"author": "Silent Mode",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ contextBridge.exposeInMainWorld("theseus", {
|
||||||
// the current session.
|
// the current session.
|
||||||
onUpdateAvailable: (cb) => ipcRenderer.on("update-available", (_e, d) => cb(d)),
|
onUpdateAvailable: (cb) => ipcRenderer.on("update-available", (_e, d) => cb(d)),
|
||||||
openUpdateDownload: (url) => ipcRenderer.invoke("open-update-download", url),
|
openUpdateDownload: (url) => ipcRenderer.invoke("open-update-download", url),
|
||||||
|
installUpdateNow: () => ipcRenderer.invoke("install-update-now"),
|
||||||
dismissUpdate: () => ipcRenderer.invoke("dismiss-update"),
|
dismissUpdate: () => ipcRenderer.invoke("dismiss-update"),
|
||||||
goHome: () => ipcRenderer.invoke("go-home"),
|
goHome: () => ipcRenderer.invoke("go-home"),
|
||||||
back: () => ipcRenderer.invoke("go-back"),
|
back: () => ipcRenderer.invoke("go-back"),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue