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
28
chrome.html
28
chrome.html
|
|
@ -440,14 +440,30 @@
|
|||
const chip = $("upchip"); if (!chip) return;
|
||||
if (!d) { chip.hidden = true; return; }
|
||||
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 core = $("upDownload");
|
||||
core.textContent = `↓ Update to ${d.version}`;
|
||||
core.title = url ? `Open ${url}` : "Download the new version";
|
||||
core.onclick = () => { if (url) T.openUpdateDownload(url); };
|
||||
// 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.title = url ? `Retry download — ${url}` : "Retry download";
|
||||
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());
|
||||
|
||||
|
|
|
|||
87
main.js
87
main.js
|
|
@ -299,13 +299,13 @@ async function checkForUpdate() {
|
|||
if (!versionIsNewer(rel.version, app.getVersion())) {
|
||||
// Same version or older — nothing to offer. Clear any stale state so the
|
||||
// chip disappears after the user has updated + relaunched.
|
||||
if (updateAvailable) { updateAvailable = null; emitUpdateAvailable(); }
|
||||
if (updateAvailable) { updateAvailable = null; updateDownloadState = "idle"; updateDownloadPath = null; emitUpdateAvailable(); }
|
||||
return;
|
||||
}
|
||||
const files = rel.files || {};
|
||||
const setupFile = Object.keys(files).find((k) => /Setup/i.test(k));
|
||||
const portableFile = Object.keys(files).find((k) => /portable/i.test(k));
|
||||
updateAvailable = {
|
||||
const nextAvailable = {
|
||||
version: rel.version,
|
||||
date: rel.date || "",
|
||||
setupUrl: setupFile ? UPDATE_DOWNLOAD_BASE + setupFile : null,
|
||||
|
|
@ -313,11 +313,48 @@ async function checkForUpdate() {
|
|||
setupHash: setupFile ? files[setupFile] : 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();
|
||||
} 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() {
|
||||
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 {}
|
||||
}
|
||||
|
||||
|
|
@ -1557,6 +1594,34 @@ function emitDownloads() {
|
|||
// attachments, and manual save-as gestures).
|
||||
function installDownloadTracker() {
|
||||
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 rec = {
|
||||
id,
|
||||
|
|
@ -2205,6 +2270,22 @@ ipcMain.handle("open-update-download", (_e, url) => {
|
|||
});
|
||||
ipcMain.handle("dismiss-update", () => { updateDismissedThisSession = true; emitUpdateAvailable(); return true; });
|
||||
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
|
||||
// snoop the preload can't act on the local file.
|
||||
ipcMain.handle("home-cards-get", (e) => isHomePageSender(e.sender) ? loadHomeCards() : []);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"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.",
|
||||
"author": "Silent Mode",
|
||||
"main": "main.js",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ contextBridge.exposeInMainWorld("theseus", {
|
|||
// the current session.
|
||||
onUpdateAvailable: (cb) => ipcRenderer.on("update-available", (_e, d) => cb(d)),
|
||||
openUpdateDownload: (url) => ipcRenderer.invoke("open-update-download", url),
|
||||
installUpdateNow: () => ipcRenderer.invoke("install-update-now"),
|
||||
dismissUpdate: () => ipcRenderer.invoke("dismiss-update"),
|
||||
goHome: () => ipcRenderer.invoke("go-home"),
|
||||
back: () => ipcRenderer.invoke("go-back"),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue