feat: add update progress window with restart button
All checks were successful
Build & Release / build (push) Successful in 2m30s
All checks were successful
Build & Release / build (push) Successful in 2m30s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
23823a9b59
commit
fa250dda5f
2 changed files with 101 additions and 11 deletions
79
src/native/update-window.ts
Normal file
79
src/native/update-window.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { BrowserWindow, app, ipcMain } from "electron";
|
||||
|
||||
let win: BrowserWindow | null = null;
|
||||
|
||||
const HTML = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:system-ui,sans-serif;background:#1e1e2e;color:#cdd6f4;
|
||||
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
height:100vh;padding:28px 32px;gap:14px;user-select:none;-webkit-app-region:drag}
|
||||
h2{font-size:15px;font-weight:600;letter-spacing:.3px}
|
||||
#status{font-size:13px;color:#a6adc8}
|
||||
.track{width:100%;height:6px;background:#313244;border-radius:3px}
|
||||
#bar{height:6px;background:#89b4fa;border-radius:3px;width:0%;transition:width .4s ease}
|
||||
#btn{display:none;margin-top:6px;padding:9px 24px;background:#89b4fa;color:#1e1e2e;
|
||||
border:none;border-radius:6px;font-size:13px;font-weight:700;cursor:pointer;
|
||||
-webkit-app-region:no-drag}
|
||||
#btn:hover{background:#b4befe}
|
||||
</style></head><body>
|
||||
<h2 id="title">Updating Sanctum</h2>
|
||||
<div id="status">Downloading…</div>
|
||||
<div class="track"><div id="bar"></div></div>
|
||||
<button id="btn">Restart Now</button>
|
||||
<script>
|
||||
const {ipcRenderer}=require('electron');
|
||||
ipcRenderer.on('upd-progress',(_,p)=>{
|
||||
document.getElementById('bar').style.width=p+'%';
|
||||
document.getElementById('status').textContent=p<100?'Downloading… '+p+'%':'Installing…';
|
||||
});
|
||||
ipcRenderer.on('upd-ready',(_,v)=>{
|
||||
document.getElementById('title').textContent='Sanctum '+v+' Ready';
|
||||
document.getElementById('status').textContent='Restart to apply the update.';
|
||||
document.getElementById('bar').style.width='100%';
|
||||
document.getElementById('btn').style.display='block';
|
||||
});
|
||||
ipcRenderer.on('upd-error',(_,msg)=>{
|
||||
document.getElementById('title').textContent='Update Failed';
|
||||
document.getElementById('status').textContent=msg;
|
||||
document.getElementById('bar').style.background='#f38ba8';
|
||||
});
|
||||
document.getElementById('btn').onclick=()=>ipcRenderer.send('upd-restart');
|
||||
</script></body></html>`;
|
||||
|
||||
export function showUpdateWindow() {
|
||||
if (win) { win.focus(); return; }
|
||||
win = new BrowserWindow({
|
||||
width: 400,
|
||||
height: 180,
|
||||
resizable: false,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
fullscreenable: false,
|
||||
title: "Sanctum Update",
|
||||
frame: false,
|
||||
alwaysOnTop: true,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
});
|
||||
win.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(HTML));
|
||||
win.on("closed", () => { win = null; });
|
||||
}
|
||||
|
||||
export function setUpdateProgress(percent: number) {
|
||||
win?.webContents.send("upd-progress", Math.round(percent));
|
||||
}
|
||||
|
||||
export function setUpdateReady(version: string) {
|
||||
win?.webContents.send("upd-ready", version);
|
||||
}
|
||||
|
||||
export function setUpdateError(msg: string) {
|
||||
win?.webContents.send("upd-error", msg);
|
||||
}
|
||||
|
||||
ipcMain.on("upd-restart", () => {
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
});
|
||||
|
|
@ -3,8 +3,8 @@ import { exec } from "child_process";
|
|||
import { createWriteStream, mkdirSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { tmpdir } from "os";
|
||||
import { pipeline } from "stream/promises";
|
||||
import { Readable } from "stream";
|
||||
|
||||
import { showUpdateWindow, setUpdateProgress, setUpdateReady, setUpdateError } from "./update-window";
|
||||
|
||||
ipcMain.handle("checkForUpdates", () => checkForUpdates());
|
||||
|
||||
|
|
@ -25,12 +25,11 @@ interface Release {
|
|||
export async function checkForUpdates() {
|
||||
try {
|
||||
console.log("[updater] checking:", RELEASES_URL);
|
||||
notify("Checking for Updates", "Looking for a new version of Sanctum…");
|
||||
|
||||
const res = await fetch(RELEASES_URL);
|
||||
if (!res.ok) {
|
||||
console.error("[updater] releases API returned", res.status, res.statusText);
|
||||
notify("Update Check Failed", `API returned ${res.status} — check console.`);
|
||||
notify("Update Check Failed", `API returned ${res.status}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -49,16 +48,16 @@ export async function checkForUpdates() {
|
|||
if (!asset) {
|
||||
const names = release.assets.map(a => a.name).join(", ");
|
||||
console.error("[updater] no matching asset for platform:", process.platform, names);
|
||||
notify("Update Failed", `No ${process.platform} asset found. Assets: ${names}`);
|
||||
notify("Update Failed", `No ${process.platform} asset found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[updater] update available: ${current} → ${latest}, downloading ${asset.name}`);
|
||||
notify("Update Downloading", `Sanctum ${latest} is downloading in the background…`);
|
||||
showUpdateWindow();
|
||||
await downloadAndInstall(asset.browser_download_url, latest);
|
||||
} catch (err) {
|
||||
console.error("[updater] update check failed:", err);
|
||||
notify("Update Check Failed", String(err));
|
||||
setUpdateError(String(err));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,14 +73,27 @@ async function downloadAndInstall(url: string, version: string) {
|
|||
mkdirSync(tmpDir, { recursive: true });
|
||||
const zipPath = join(tmpDir, "update.zip");
|
||||
const extractDir = join(tmpDir, "extracted");
|
||||
const installDir = dirname(process.execPath);
|
||||
|
||||
console.log(`[updater] downloading from ${url}`);
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
||||
|
||||
const installDir = dirname(process.execPath);
|
||||
const total = Number(res.headers.get("content-length")) || 0;
|
||||
let downloaded = 0;
|
||||
const writer = createWriteStream(zipPath);
|
||||
const reader = res.body.getReader();
|
||||
|
||||
await pipeline(Readable.fromWeb(res.body as Parameters<typeof Readable.fromWeb>[0]), createWriteStream(zipPath));
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
writer.write(value);
|
||||
downloaded += value.length;
|
||||
if (total > 0) setUpdateProgress(Math.round((downloaded / total) * 90));
|
||||
}
|
||||
await new Promise<void>(resolve => writer.end(resolve));
|
||||
|
||||
setUpdateProgress(95);
|
||||
console.log(`[updater] download complete, extracting to ${installDir}`);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
|
|
@ -96,8 +108,7 @@ async function downloadAndInstall(url: string, version: string) {
|
|||
});
|
||||
});
|
||||
|
||||
const n = notify("Update Ready", `Sanctum ${version} is ready — click to restart.`);
|
||||
n.on("click", () => { app.relaunch(); app.exit(0); });
|
||||
setUpdateReady(version);
|
||||
}
|
||||
|
||||
function notify(title: string, body: string) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue