1.1.3
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
const { app, BrowserWindow, dialog, ipcMain, shell, screen, safeStorage, powerMonitor } = require('electron');
|
||||
const { app, BrowserWindow, dialog, ipcMain, shell, screen, safeStorage, powerMonitor, Notification, nativeImage } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
@@ -6,7 +6,7 @@ const fs = require('fs');
|
||||
const SESSION_FILE = path.join(app.getPath('userData'), 'secure-session.dat');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const { checkForUpdates } = require('./updater.cjs');
|
||||
const { checkForUpdates, getStatus: getUpdateStatus, onStatus: onUpdateStatus, downloadAndInstall: downloadAndInstallUpdate } = require('./updater.cjs');
|
||||
|
||||
function loadEnvVar(varName) {
|
||||
const envFiles = [
|
||||
@@ -191,6 +191,16 @@ function createWindow() {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
|
||||
// Auto-clear attention state whenever the user comes back — no
|
||||
// point in flashing / badging a window the user is already
|
||||
// looking at.
|
||||
mainWindow.on('focus', () => {
|
||||
if (mainWindow.isDestroyed()) return;
|
||||
mainWindow.flashFrame(false);
|
||||
mainWindow.setOverlayIcon(null, '');
|
||||
try { app.setBadgeCount(0); } catch {}
|
||||
});
|
||||
|
||||
// Save window state on close
|
||||
mainWindow.on('close', () => {
|
||||
// Flush localStorage/sessionStorage to disk before renderer is destroyed
|
||||
@@ -249,14 +259,25 @@ app.whenReady().then(async () => {
|
||||
createWindow();
|
||||
} else {
|
||||
const splash = createSplashWindow();
|
||||
const noUpdate = await checkForUpdates(splash);
|
||||
if (noUpdate === false) {
|
||||
const shouldOpenMain = await checkForUpdates(splash);
|
||||
if (shouldOpenMain) {
|
||||
if (!splash.isDestroyed()) splash.close();
|
||||
createWindow();
|
||||
}
|
||||
// If update downloaded, quitAndInstall handles restart
|
||||
// Required-update path: splash stays; updater runs quitAndInstall itself.
|
||||
}
|
||||
|
||||
// Forward updater status changes to the renderer so the header
|
||||
// icon and the required-update blocker can react in real time.
|
||||
onUpdateStatus((status) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('update:status', status);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('update:get-status', () => getUpdateStatus());
|
||||
ipcMain.handle('update:download-and-install', () => downloadAndInstallUpdate());
|
||||
|
||||
ipcMain.on('window-minimize', () => {
|
||||
const win = BrowserWindow.getFocusedWindow();
|
||||
if (win) win.minimize();
|
||||
@@ -272,8 +293,49 @@ app.whenReady().then(async () => {
|
||||
const win = BrowserWindow.getFocusedWindow();
|
||||
if (win) win.close();
|
||||
});
|
||||
ipcMain.on('flash-frame', () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.flashFrame(true);
|
||||
ipcMain.on('flash-frame', (_event, on) => {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
// `on` optional for backward compat — legacy call sites pass
|
||||
// no arg and expect a one-shot flash.
|
||||
mainWindow.flashFrame(on !== false);
|
||||
});
|
||||
|
||||
ipcMain.on('notification:show', (_event, opts) => {
|
||||
if (!Notification.isSupported()) return;
|
||||
const { title, body, silent } = opts || {};
|
||||
const n = new Notification({
|
||||
title: String(title ?? 'Brycord'),
|
||||
body: String(body ?? ''),
|
||||
silent: !!silent,
|
||||
icon: path.join(__dirname, 'icon.png'),
|
||||
});
|
||||
n.on('click', () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
n.show();
|
||||
});
|
||||
|
||||
// Windows: overlay icon on the taskbar button (1x1 badge). Clearing
|
||||
// is `(null, '')`. `count <= 0` clears. Other OSes fall back to
|
||||
// `app.setBadgeCount` which is a no-op on platforms that don't
|
||||
// support it.
|
||||
ipcMain.on('notification:set-badge', (_event, count) => {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
const n = Math.max(0, Math.floor(Number(count) || 0));
|
||||
if (n === 0) {
|
||||
mainWindow.setOverlayIcon(null, '');
|
||||
} else {
|
||||
const badgePath = path.join(__dirname, 'icon.png');
|
||||
try {
|
||||
const img = nativeImage.createFromPath(badgePath);
|
||||
mainWindow.setOverlayIcon(img, `${n} unread`);
|
||||
} catch {}
|
||||
}
|
||||
try { app.setBadgeCount(n); } catch {}
|
||||
});
|
||||
|
||||
// Helper to fetch metadata (Zero-Knowledge: Client fetches previews)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@discord-clone/electron",
|
||||
"private": true,
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.3",
|
||||
"description": "Brycord - Electron app",
|
||||
"author": "Moyettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -25,7 +25,13 @@ contextBridge.exposeInMainWorld('windowControls', {
|
||||
minimize: () => ipcRenderer.send('window-minimize'),
|
||||
maximize: () => ipcRenderer.send('window-maximize'),
|
||||
close: () => ipcRenderer.send('window-close'),
|
||||
flashFrame: () => ipcRenderer.send('flash-frame'),
|
||||
flashFrame: (on) => ipcRenderer.send('flash-frame', on !== false),
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('notificationAPI', {
|
||||
show: (opts) => ipcRenderer.send('notification:show', opts),
|
||||
setBadge: (count) => ipcRenderer.send('notification:set-badge', count),
|
||||
flashFrame: (on) => ipcRenderer.send('flash-frame', on !== false),
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('appSettings', {
|
||||
@@ -35,6 +41,13 @@ contextBridge.exposeInMainWorld('appSettings', {
|
||||
|
||||
contextBridge.exposeInMainWorld('updateAPI', {
|
||||
checkFlatpakUpdate: () => ipcRenderer.invoke('check-flatpak-update'),
|
||||
getStatus: () => ipcRenderer.invoke('update:get-status'),
|
||||
downloadAndInstall: () => ipcRenderer.invoke('update:download-and-install'),
|
||||
onStatusChanged: (callback) => {
|
||||
const handler = (_event, status) => callback(status);
|
||||
ipcRenderer.on('update:status', handler);
|
||||
return () => ipcRenderer.removeListener('update:status', handler);
|
||||
},
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('sessionPersistence', {
|
||||
|
||||
@@ -52,7 +52,13 @@ const electronPlatform = {
|
||||
minimize: () => window.windowControls.minimize(),
|
||||
maximize: () => window.windowControls.maximize(),
|
||||
close: () => window.windowControls.close(),
|
||||
flashFrame: () => window.windowControls.flashFrame(),
|
||||
flashFrame: (on) => window.windowControls.flashFrame(on),
|
||||
},
|
||||
notifications: {
|
||||
show: (opts) => window.notificationAPI.show(opts),
|
||||
setBadge: (count) => window.notificationAPI.setBadge(count),
|
||||
flashFrame: (on) => window.notificationAPI.flashFrame(on),
|
||||
ensurePermission: async () => 'granted',
|
||||
},
|
||||
recording: {
|
||||
getDefaultFolder: () => window.recordingAPI.getDefaultFolder(),
|
||||
@@ -70,6 +76,9 @@ const electronPlatform = {
|
||||
},
|
||||
updates: {
|
||||
checkUpdate: () => window.updateAPI.checkFlatpakUpdate(),
|
||||
getStatus: () => window.updateAPI.getStatus(),
|
||||
downloadAndInstall: () => window.updateAPI.downloadAndInstall(),
|
||||
onStatusChanged: (cb) => window.updateAPI.onStatusChanged(cb),
|
||||
},
|
||||
systemBars: null,
|
||||
searchDB,
|
||||
@@ -80,6 +89,7 @@ const electronPlatform = {
|
||||
hasSearch: true,
|
||||
hasSystemBars: false,
|
||||
hasRecording: true,
|
||||
hasNotifications: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user