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 { createWriteStream, mkdirSync } from "fs";
|
||||||
import { dirname, join } from "path";
|
import { dirname, join } from "path";
|
||||||
import { tmpdir } from "os";
|
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());
|
ipcMain.handle("checkForUpdates", () => checkForUpdates());
|
||||||
|
|
||||||
|
|
@ -25,12 +25,11 @@ interface Release {
|
||||||
export async function checkForUpdates() {
|
export async function checkForUpdates() {
|
||||||
try {
|
try {
|
||||||
console.log("[updater] checking:", RELEASES_URL);
|
console.log("[updater] checking:", RELEASES_URL);
|
||||||
notify("Checking for Updates", "Looking for a new version of Sanctum…");
|
|
||||||
|
|
||||||
const res = await fetch(RELEASES_URL);
|
const res = await fetch(RELEASES_URL);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
console.error("[updater] releases API returned", res.status, res.statusText);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,16 +48,16 @@ export async function checkForUpdates() {
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
const names = release.assets.map(a => a.name).join(", ");
|
const names = release.assets.map(a => a.name).join(", ");
|
||||||
console.error("[updater] no matching asset for platform:", process.platform, names);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[updater] update available: ${current} → ${latest}, downloading ${asset.name}`);
|
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);
|
await downloadAndInstall(asset.browser_download_url, latest);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[updater] update check failed:", 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 });
|
mkdirSync(tmpDir, { recursive: true });
|
||||||
const zipPath = join(tmpDir, "update.zip");
|
const zipPath = join(tmpDir, "update.zip");
|
||||||
const extractDir = join(tmpDir, "extracted");
|
const extractDir = join(tmpDir, "extracted");
|
||||||
|
const installDir = dirname(process.execPath);
|
||||||
|
|
||||||
console.log(`[updater] downloading from ${url}`);
|
console.log(`[updater] downloading from ${url}`);
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
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}`);
|
console.log(`[updater] download complete, extracting to ${installDir}`);
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
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.`);
|
setUpdateReady(version);
|
||||||
n.on("click", () => { app.relaunch(); app.exit(0); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function notify(title: string, body: string) {
|
function notify(title: string, body: string) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue