This commit is contained in:
Bryan1029384756
2026-04-18 15:41:51 -05:00
parent 938df217f4
commit 593eaba82e
47 changed files with 3539 additions and 212 deletions

View File

@@ -5,6 +5,52 @@ autoUpdater.logger = log;
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
// Remembered across the process so the main window can ask for it
// once the renderer is ready. Cleared on successful download so we
// don't mislead about a pending install.
let lastCheckResult = {
hasUpdate: false,
required: false,
latestVersion: null,
currentVersion: null,
releaseNotes: null,
downloading: false,
downloaded: false,
progress: 0,
error: null,
};
const statusListeners = new Set();
function emitStatus() {
for (const cb of statusListeners) {
try { cb({ ...lastCheckResult }); } catch (err) { log.warn('update status listener threw', err); }
}
}
function onStatus(cb) {
statusListeners.add(cb);
return () => statusListeners.delete(cb);
}
function getStatus() {
return { ...lastCheckResult };
}
// A release is "required" when its notes start with the `[REQUIRED]`
// marker. Keeping the signal in release notes means no new feed file
// or schema change — publishers just prefix the message.
function isRequired(info) {
const notes = typeof info?.releaseNotes === 'string' ? info.releaseNotes : '';
return /^\s*\[REQUIRED\]/i.test(notes);
}
// Splash-phase check. Resolves once we know whether to open the main
// window (optional or no update) or to force an install (required).
// - No update / error → resolve true (main window should open)
// - Optional update → resolve true (main window should open; header
// icon surfaces the update inside the app)
// - Required update → download + quitAndInstall; never resolves
function checkForUpdates(splashWindow) {
return new Promise((resolve) => {
function sendToSplash(js) {
@@ -17,45 +63,133 @@ function checkForUpdates(splashWindow) {
sendToSplash('setStatus("Checking for updates...")');
});
autoUpdater.on('update-available', () => {
sendToSplash('setStatus("Downloading update...")');
autoUpdater.downloadUpdate();
autoUpdater.on('update-available', (info) => {
const required = isRequired(info);
lastCheckResult = {
hasUpdate: true,
required,
latestVersion: info?.version ?? null,
currentVersion: autoUpdater.currentVersion?.version ?? null,
releaseNotes: typeof info?.releaseNotes === 'string' ? info.releaseNotes : null,
downloading: required,
downloaded: false,
progress: 0,
error: null,
};
emitStatus();
if (required) {
sendToSplash('setStatus("Downloading required update...")');
autoUpdater.downloadUpdate().catch((err) => {
log.error('downloadUpdate (required) failed:', err);
sendToSplash('setStatus("Update failed — opening anyway")');
lastCheckResult.error = err?.message || 'download failed';
emitStatus();
setTimeout(() => resolve(true), 1500);
});
} else {
// Optional — fall through to open main window. The header
// icon will surface the offer inside the app.
sendToSplash('setStatus("Update available — continuing")');
setTimeout(() => resolve(true), 400);
}
});
autoUpdater.on('download-progress', (progress) => {
const percent = Math.round(progress.percent);
sendToSplash(`setProgress(${percent})`);
sendToSplash(`setStatus("Downloading update... ${percent}%")`);
const percent = Math.round(progress.percent || 0);
lastCheckResult.progress = percent;
lastCheckResult.downloading = true;
emitStatus();
if (lastCheckResult.required) {
sendToSplash(`setProgress(${percent})`);
sendToSplash(`setStatus("Downloading required update... ${percent}%")`);
}
});
autoUpdater.on('update-downloaded', () => {
sendToSplash('setStatus("Installing update...")');
sendToSplash('setProgress(100)');
setTimeout(() => {
autoUpdater.quitAndInstall();
}, 1500);
lastCheckResult.downloading = false;
lastCheckResult.downloaded = true;
lastCheckResult.progress = 100;
emitStatus();
if (lastCheckResult.required) {
sendToSplash('setStatus("Installing update...")');
sendToSplash('setProgress(100)');
setTimeout(() => {
autoUpdater.quitAndInstall();
}, 1200);
}
});
autoUpdater.on('update-not-available', () => {
lastCheckResult = {
hasUpdate: false,
required: false,
latestVersion: null,
currentVersion: autoUpdater.currentVersion?.version ?? null,
releaseNotes: null,
downloading: false,
downloaded: false,
progress: 0,
error: null,
};
emitStatus();
sendToSplash('setStatus("Up to date!")');
sendToSplash('hideProgress()');
setTimeout(() => resolve(false), 1000);
setTimeout(() => resolve(true), 500);
});
autoUpdater.on('error', (err) => {
log.error('Auto-updater error:', err);
lastCheckResult.error = err?.message || String(err);
lastCheckResult.downloading = false;
emitStatus();
sendToSplash('setStatus("Update check failed")');
sendToSplash('hideProgress()');
setTimeout(() => resolve(false), 2000);
setTimeout(() => resolve(true), 1500);
});
autoUpdater.checkForUpdates().catch((err) => {
log.error('checkForUpdates failed:', err);
lastCheckResult.error = err?.message || String(err);
emitStatus();
sendToSplash('setStatus("Update check failed")');
sendToSplash('hideProgress()');
setTimeout(() => resolve(false), 2000);
setTimeout(() => resolve(true), 1500);
});
});
}
module.exports = { checkForUpdates };
// Start downloading an optional update from the renderer. The
// splash flow handles required updates itself so the renderer only
// ever reaches here for optional ones.
async function downloadAndInstall() {
if (!lastCheckResult.hasUpdate) return { ok: false, error: 'no update' };
if (lastCheckResult.downloaded) {
autoUpdater.quitAndInstall();
return { ok: true };
}
if (lastCheckResult.downloading) return { ok: true };
try {
lastCheckResult.downloading = true;
lastCheckResult.progress = 0;
lastCheckResult.error = null;
emitStatus();
await autoUpdater.downloadUpdate();
// The 'update-downloaded' handler bumps status; install triggers
// the quit-and-install flow the next tick.
autoUpdater.quitAndInstall();
return { ok: true };
} catch (err) {
log.error('downloadAndInstall failed:', err);
lastCheckResult.downloading = false;
lastCheckResult.error = err?.message || String(err);
emitStatus();
return { ok: false, error: lastCheckResult.error };
}
}
module.exports = {
checkForUpdates,
getStatus,
onStatus,
downloadAndInstall,
};