checkpoint
This commit is contained in:
parent
3735162f41
commit
208ab8042e
3 changed files with 296 additions and 26 deletions
|
|
@ -3,9 +3,15 @@ const { contextBridge, ipcRenderer } = require('electron');
|
|||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
copyToClipboard: (text) => ipcRenderer.invoke('clipboard-write', text),
|
||||
getConfig: () => ipcRenderer.invoke('get-config'),
|
||||
storageGet: (key) => ipcRenderer.invoke('storage-get', key),
|
||||
storageSet: (key, value) => ipcRenderer.invoke('storage-set', key, value),
|
||||
storageRemove: (key) => ipcRenderer.invoke('storage-remove', key),
|
||||
getUpdateState: () => ipcRenderer.invoke('get-update-state'),
|
||||
checkForUpdatesNow: () => ipcRenderer.invoke('check-for-updates-now'),
|
||||
checkForUpdates: () => ipcRenderer.send('check-for-updates'),
|
||||
downloadUpdate: () => ipcRenderer.send('download-update'),
|
||||
quitAndInstall: () => ipcRenderer.send('quit-and-install'),
|
||||
onUpdateState: (callback) => ipcRenderer.on('update-state', (event, state) => callback(state)),
|
||||
onUpdateAvailable: (callback) => ipcRenderer.on('update-available', (event, info) => callback(info)),
|
||||
onUpdateDownloaded: (callback) => ipcRenderer.on('update-downloaded', (event, info) => callback(info)),
|
||||
onUpdateError: (callback) => ipcRenderer.on('update-error', (event, error) => callback(error))
|
||||
|
|
|
|||
137
main.js
137
main.js
|
|
@ -1,8 +1,82 @@
|
|||
const { app, BrowserWindow, session, desktopCapturer, ipcMain, clipboard } = require('electron');
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const url = require('url');
|
||||
|
||||
const updateState = {
|
||||
status: 'idle', // idle | checking | available | downloading | downloaded | not-available | error
|
||||
info: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
function broadcastUpdateState() {
|
||||
const wins = BrowserWindow.getAllWindows();
|
||||
for (const win of wins) {
|
||||
if (!win.isDestroyed()) {
|
||||
win.webContents.send('update-state', updateState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getStorageFilePath() {
|
||||
return path.join(app.getPath('userData'), 'renderer-storage.json');
|
||||
}
|
||||
|
||||
function readPersistentStore() {
|
||||
const filePath = getStorageFilePath();
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return {};
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
return parsed && typeof parsed === 'object' ? parsed : {};
|
||||
} catch (err) {
|
||||
console.error('Failed to read persistent store', err);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writePersistentStore(store) {
|
||||
const filePath = getStorageFilePath();
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify(store), 'utf8');
|
||||
} catch (err) {
|
||||
console.error('Failed to write persistent store', err);
|
||||
}
|
||||
}
|
||||
|
||||
function isNewerVersionAvailable(info) {
|
||||
const next = info && typeof info.version === 'string' ? info.version : '';
|
||||
return Boolean(next) && next !== app.getVersion();
|
||||
}
|
||||
|
||||
async function runUpdateCheck(reason = 'manual') {
|
||||
updateState.status = 'checking';
|
||||
updateState.error = null;
|
||||
broadcastUpdateState();
|
||||
try {
|
||||
const result = await autoUpdater.checkForUpdates();
|
||||
const info = result && result.updateInfo ? result.updateInfo : null;
|
||||
if (isNewerVersionAvailable(info)) {
|
||||
updateState.status = 'available';
|
||||
updateState.info = info;
|
||||
updateState.error = null;
|
||||
broadcastUpdateState();
|
||||
} else {
|
||||
updateState.status = 'not-available';
|
||||
updateState.info = info;
|
||||
updateState.error = null;
|
||||
broadcastUpdateState();
|
||||
}
|
||||
} catch (err) {
|
||||
updateState.status = 'error';
|
||||
updateState.error = err && err.message ? err.message : String(err);
|
||||
broadcastUpdateState();
|
||||
console.error(`Update check failed (${reason})`, err);
|
||||
}
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
// Create a persistent session for chattz to keep the user logged in
|
||||
const sess = session.fromPartition('persist:chattz');
|
||||
|
|
@ -34,6 +108,34 @@ function createWindow() {
|
|||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('storage-get', (event, key) => {
|
||||
if (typeof key !== 'string' || key.length === 0) return null;
|
||||
const store = readPersistentStore();
|
||||
return Object.prototype.hasOwnProperty.call(store, key) ? store[key] : null;
|
||||
});
|
||||
|
||||
ipcMain.handle('storage-set', (event, key, value) => {
|
||||
if (typeof key !== 'string' || key.length === 0) return false;
|
||||
const store = readPersistentStore();
|
||||
store[key] = value;
|
||||
writePersistentStore(store);
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('storage-remove', (event, key) => {
|
||||
if (typeof key !== 'string' || key.length === 0) return false;
|
||||
const store = readPersistentStore();
|
||||
delete store[key];
|
||||
writePersistentStore(store);
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('get-update-state', () => ({ ...updateState }));
|
||||
ipcMain.handle('check-for-updates-now', async () => {
|
||||
await runUpdateCheck('renderer-direct');
|
||||
return { ...updateState };
|
||||
});
|
||||
|
||||
// Auto-approve media permissions (camera, microphone)
|
||||
sess.setPermissionCheckHandler((webContents, permission) => {
|
||||
if (permission === 'media' || permission === 'clipboard-read' || permission === 'clipboard-write') {
|
||||
|
|
@ -112,6 +214,14 @@ function createWindow() {
|
|||
|
||||
loadDesktopApp();
|
||||
|
||||
// Ensure renderer receives latest updater state after any (re)load.
|
||||
win.webContents.on('did-finish-load', () => {
|
||||
broadcastUpdateState();
|
||||
// Login flow swaps pages; force a check after desktop UI reloads so
|
||||
// renderer always gets fresh updater state.
|
||||
void runUpdateCheck('did-finish-load');
|
||||
});
|
||||
|
||||
// Uncomment to debug
|
||||
// win.webContents.openDevTools();
|
||||
}
|
||||
|
|
@ -127,27 +237,46 @@ app.whenReady().then(() => {
|
|||
autoUpdater.logger = console;
|
||||
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
updateState.status = 'available';
|
||||
updateState.info = info;
|
||||
updateState.error = null;
|
||||
broadcastUpdateState();
|
||||
const wins = BrowserWindow.getAllWindows();
|
||||
if (wins.length > 0) wins[0].webContents.send('update-available', info);
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', (info) => {
|
||||
updateState.status = 'not-available';
|
||||
updateState.info = info || null;
|
||||
updateState.error = null;
|
||||
broadcastUpdateState();
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
updateState.status = 'downloaded';
|
||||
updateState.info = info;
|
||||
updateState.error = null;
|
||||
broadcastUpdateState();
|
||||
const wins = BrowserWindow.getAllWindows();
|
||||
if (wins.length > 0) wins[0].webContents.send('update-downloaded', info);
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (err) => {
|
||||
updateState.status = 'error';
|
||||
updateState.error = err.message;
|
||||
broadcastUpdateState();
|
||||
const wins = BrowserWindow.getAllWindows();
|
||||
if (wins.length > 0) wins[0].webContents.send('update-error', err.message);
|
||||
});
|
||||
|
||||
ipcMain.on('check-for-updates', () => {
|
||||
autoUpdater.checkForUpdatesAndNotify().catch(err => {
|
||||
console.error("Manual update check failed", err);
|
||||
});
|
||||
void runUpdateCheck('manual');
|
||||
});
|
||||
|
||||
ipcMain.on('download-update', () => {
|
||||
updateState.status = 'downloading';
|
||||
updateState.error = null;
|
||||
broadcastUpdateState();
|
||||
autoUpdater.downloadUpdate();
|
||||
});
|
||||
|
||||
|
|
@ -157,7 +286,7 @@ app.whenReady().then(() => {
|
|||
|
||||
// Check once on startup
|
||||
setTimeout(() => {
|
||||
autoUpdater.checkForUpdatesAndNotify().catch(() => { });
|
||||
void runUpdateCheck('startup');
|
||||
}, 5000);
|
||||
|
||||
app.on('activate', () => {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,80 @@ const state = {
|
|||
|
||||
// --- Desktop Backend Configuration ---
|
||||
let API_BASE_URL = ''; // Will be initialized via IPC
|
||||
const LAST_GUILD_STORAGE_KEY = "chattz:lastGuildId";
|
||||
const PERSISTED_STORAGE_KEYS = [
|
||||
"chattz_token",
|
||||
"chattz_refresh_token",
|
||||
LAST_GUILD_STORAGE_KEY,
|
||||
"active_voice_channel",
|
||||
];
|
||||
|
||||
function storageGet(key) {
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
|
||||
function storageSet(key, value) {
|
||||
localStorage.setItem(key, value);
|
||||
if (window.electronAPI?.storageSet) {
|
||||
void window.electronAPI.storageSet(key, value).catch((err) => {
|
||||
console.warn("Failed to persist storage key", key, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function storageRemove(key) {
|
||||
localStorage.removeItem(key);
|
||||
if (window.electronAPI?.storageRemove) {
|
||||
void window.electronAPI.storageRemove(key).catch((err) => {
|
||||
console.warn("Failed to remove persisted storage key", key, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function storageSetCritical(key, value) {
|
||||
localStorage.setItem(key, value);
|
||||
if (window.electronAPI?.storageSet) {
|
||||
try {
|
||||
await window.electronAPI.storageSet(key, value);
|
||||
} catch (err) {
|
||||
console.warn("Failed to persist critical storage key", key, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function storageRemoveCritical(key) {
|
||||
localStorage.removeItem(key);
|
||||
if (window.electronAPI?.storageRemove) {
|
||||
try {
|
||||
await window.electronAPI.storageRemove(key);
|
||||
} catch (err) {
|
||||
console.warn("Failed to remove critical storage key", key, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function hydrateDesktopStorage() {
|
||||
if (!window.electronAPI?.storageGet) return;
|
||||
for (const key of PERSISTED_STORAGE_KEYS) {
|
||||
const localVal = localStorage.getItem(key);
|
||||
if (localVal !== null) {
|
||||
try {
|
||||
await window.electronAPI.storageSet(key, localVal);
|
||||
} catch (err) {
|
||||
console.warn("Failed to backfill storage key", key, err);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const val = await window.electronAPI.storageGet(key);
|
||||
if (typeof val === "string") {
|
||||
localStorage.setItem(key, val);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Failed to hydrate storage key", key, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeConfig() {
|
||||
try {
|
||||
|
|
@ -73,7 +147,7 @@ function getWsUrl(path) {
|
|||
urlStr = `${proto}://${url.host}${normalizedPath}`;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem("chattz_token");
|
||||
const token = storageGet("chattz_token");
|
||||
if (token) {
|
||||
urlStr += (urlStr.includes('?') ? '&' : '?') + `token=${token}`;
|
||||
}
|
||||
|
|
@ -180,7 +254,7 @@ async function api(path, options = {}) {
|
|||
...(options.headers || {}),
|
||||
};
|
||||
|
||||
const token = localStorage.getItem("chattz_token");
|
||||
const token = storageGet("chattz_token");
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
|
@ -192,7 +266,7 @@ async function api(path, options = {}) {
|
|||
|
||||
// Handle 401 Unauthorized via Refresh Token
|
||||
if (res.status === 401 && !path.includes('/auth/refresh')) {
|
||||
const refreshToken = localStorage.getItem("chattz_refresh_token");
|
||||
const refreshToken = storageGet("chattz_refresh_token");
|
||||
if (refreshToken) {
|
||||
try {
|
||||
const refreshUrl = API_BASE_URL ? `${API_BASE_URL}/auth/refresh` : '/auth/refresh';
|
||||
|
|
@ -204,8 +278,8 @@ async function api(path, options = {}) {
|
|||
|
||||
if (refreshRes.ok) {
|
||||
const newTokens = await refreshRes.json();
|
||||
localStorage.setItem("chattz_token", newTokens.access_token);
|
||||
localStorage.setItem("chattz_refresh_token", newTokens.refresh_token);
|
||||
await storageSetCritical("chattz_token", newTokens.access_token);
|
||||
await storageSetCritical("chattz_refresh_token", newTokens.refresh_token);
|
||||
|
||||
// Retry original request with new token
|
||||
headers["Authorization"] = `Bearer ${newTokens.access_token}`;
|
||||
|
|
@ -215,8 +289,8 @@ async function api(path, options = {}) {
|
|||
throw new Error("Refresh token expired or invalid");
|
||||
}
|
||||
} catch (err) {
|
||||
localStorage.removeItem("chattz_token");
|
||||
localStorage.removeItem("chattz_refresh_token");
|
||||
await storageRemoveCritical("chattz_token");
|
||||
await storageRemoveCritical("chattz_refresh_token");
|
||||
location.href = API_BASE_URL ? `${API_BASE_URL}/auth/login` : "/auth/login";
|
||||
throw new Error("Session expired, please log in again");
|
||||
}
|
||||
|
|
@ -330,8 +404,6 @@ function playSound(type) {
|
|||
console.warn("playSound failed", err);
|
||||
}
|
||||
}
|
||||
const LAST_GUILD_STORAGE_KEY = "chattz:lastGuildId";
|
||||
|
||||
// --- Renderers ---
|
||||
|
||||
function renderGuilds() {
|
||||
|
|
@ -348,7 +420,7 @@ function renderGuilds() {
|
|||
state.selectedVoiceChannelId = null;
|
||||
state.selectedDmUserId = null;
|
||||
state.selectedDmDisplayName = null;
|
||||
localStorage.setItem(LAST_GUILD_STORAGE_KEY, guild.id);
|
||||
storageSet(LAST_GUILD_STORAGE_KEY, guild.id);
|
||||
renderGuilds();
|
||||
renderDMs();
|
||||
await loadChannels();
|
||||
|
|
@ -1258,7 +1330,7 @@ async function joinVoice() {
|
|||
|
||||
state.voice.ws = ws;
|
||||
state.voice.joinedChannelId = state.selectedVoiceChannelId;
|
||||
localStorage.setItem("active_voice_channel", state.selectedVoiceChannelId);
|
||||
storageSet("active_voice_channel", state.selectedVoiceChannelId);
|
||||
|
||||
// Start mic setup in parallel so channel join is not blocked by device init.
|
||||
createLocalVoiceStream()
|
||||
|
|
@ -1390,7 +1462,7 @@ async function joinVoice() {
|
|||
}
|
||||
|
||||
async function leaveVoice() {
|
||||
localStorage.removeItem("active_voice_channel");
|
||||
storageRemove("active_voice_channel");
|
||||
if (state.voice.ws) state.voice.ws.close();
|
||||
}
|
||||
|
||||
|
|
@ -1612,6 +1684,7 @@ function closeMobileMenus() {
|
|||
|
||||
let inactivityTimer = null;
|
||||
let isCurrentlyIdle = false;
|
||||
let updaterInitialized = false;
|
||||
|
||||
function resetInactivityTimer() {
|
||||
if (isCurrentlyIdle) {
|
||||
|
|
@ -1636,6 +1709,8 @@ document.addEventListener('click', resetInactivityTimer);
|
|||
|
||||
async function init() {
|
||||
lucide.createIcons();
|
||||
await hydrateDesktopStorage();
|
||||
initUpdater();
|
||||
|
||||
// 1. Handle tokens in URL (from successful login redirects)
|
||||
try {
|
||||
|
|
@ -1644,8 +1719,8 @@ async function init() {
|
|||
const refreshToken = searchParams.get("refresh_token");
|
||||
|
||||
if (jwtToken || refreshToken) {
|
||||
if (jwtToken) localStorage.setItem("chattz_token", jwtToken);
|
||||
if (refreshToken) localStorage.setItem("chattz_refresh_token", refreshToken);
|
||||
if (jwtToken) await storageSetCritical("chattz_token", jwtToken);
|
||||
if (refreshToken) await storageSetCritical("chattz_refresh_token", refreshToken);
|
||||
|
||||
searchParams.delete("token");
|
||||
searchParams.delete("refresh_token");
|
||||
|
|
@ -1665,8 +1740,8 @@ async function init() {
|
|||
|
||||
el.logoutBtn.onclick = async () => {
|
||||
await leaveVoice();
|
||||
localStorage.removeItem("chattz_token");
|
||||
localStorage.removeItem("chattz_refresh_token");
|
||||
await storageRemoveCritical("chattz_token");
|
||||
await storageRemoveCritical("chattz_refresh_token");
|
||||
try { await api("/auth/logout", { method: "POST" }); } catch (e) { }
|
||||
location.reload();
|
||||
};
|
||||
|
|
@ -1705,7 +1780,7 @@ async function init() {
|
|||
state.selectedVoiceChannelId = null;
|
||||
state.selectedDmUserId = null;
|
||||
state.selectedDmDisplayName = null;
|
||||
localStorage.setItem(LAST_GUILD_STORAGE_KEY, guild.id);
|
||||
storageSet(LAST_GUILD_STORAGE_KEY, guild.id);
|
||||
renderGuilds();
|
||||
await loadChannels();
|
||||
await loadGuildMembers();
|
||||
|
|
@ -1889,7 +1964,7 @@ async function init() {
|
|||
if (inviteCode) {
|
||||
try {
|
||||
const joinedGuild = await api(`/invites/${encodeURIComponent(inviteCode)}/join`, { method: "POST" });
|
||||
localStorage.setItem(LAST_GUILD_STORAGE_KEY, joinedGuild.id);
|
||||
storageSet(LAST_GUILD_STORAGE_KEY, joinedGuild.id);
|
||||
} catch (err) { alert(`Failed to join invite: ${err.message}`); } finally {
|
||||
params.delete("invite");
|
||||
const nextQuery = params.toString();
|
||||
|
|
@ -1908,10 +1983,10 @@ async function init() {
|
|||
} catch (err) { console.warn("presence sync failed", err); }
|
||||
|
||||
if (state.guilds.length > 0) {
|
||||
const lastGuildId = localStorage.getItem(LAST_GUILD_STORAGE_KEY);
|
||||
const lastGuildId = storageGet(LAST_GUILD_STORAGE_KEY);
|
||||
const guild = state.guilds.find((g) => g.id === lastGuildId) || state.guilds[0];
|
||||
state.selectedGuildId = guild.id;
|
||||
localStorage.setItem(LAST_GUILD_STORAGE_KEY, guild.id);
|
||||
storageSet(LAST_GUILD_STORAGE_KEY, guild.id);
|
||||
renderGuilds();
|
||||
await loadChannels();
|
||||
await loadGuildMembers();
|
||||
|
|
@ -1927,7 +2002,7 @@ async function init() {
|
|||
startVoicePresencePolling();
|
||||
|
||||
// Auto-reconnect to voice if previously joined
|
||||
const savedVoiceChannel = localStorage.getItem("active_voice_channel");
|
||||
const savedVoiceChannel = storageGet("active_voice_channel");
|
||||
if (savedVoiceChannel) {
|
||||
console.log("Auto-reconnecting to voice channel:", savedVoiceChannel);
|
||||
state.selectedVoiceChannelId = savedVoiceChannel;
|
||||
|
|
@ -1936,7 +2011,6 @@ async function init() {
|
|||
}, 800);
|
||||
}
|
||||
|
||||
initUpdater();
|
||||
lucide.createIcons();
|
||||
} catch (err) {
|
||||
console.error("init failed", err);
|
||||
|
|
@ -1947,6 +2021,53 @@ async function init() {
|
|||
|
||||
function initUpdater() {
|
||||
if (!window.electronAPI || !el.updateNotifier || !el.updateDownloadBtn || !el.updateInstallBtn) return;
|
||||
if (updaterInitialized) return;
|
||||
updaterInitialized = true;
|
||||
|
||||
const applyUpdateState = (state) => {
|
||||
if (!state || !state.status) return;
|
||||
if (state.status === 'available') {
|
||||
const v = state.info?.version ? ` ${state.info.version}` : '';
|
||||
el.updateNotifier.classList.remove("hidden");
|
||||
el.updateDownloadBtn.classList.remove("hidden");
|
||||
el.updateInstallBtn.classList.add("hidden");
|
||||
el.updateDownloadBtn.disabled = false;
|
||||
el.updateDownloadBtn.style.opacity = "";
|
||||
el.updateDownloadBtn.title = `Download Update${v}`;
|
||||
return;
|
||||
}
|
||||
if (state.status === 'downloading') {
|
||||
el.updateNotifier.classList.remove("hidden");
|
||||
el.updateDownloadBtn.classList.remove("hidden");
|
||||
el.updateInstallBtn.classList.add("hidden");
|
||||
el.updateDownloadBtn.disabled = true;
|
||||
el.updateDownloadBtn.style.opacity = "0.5";
|
||||
el.updateDownloadBtn.title = "Downloading update...";
|
||||
return;
|
||||
}
|
||||
if (state.status === 'downloaded') {
|
||||
const v = state.info?.version ? ` ${state.info.version}` : '';
|
||||
el.updateNotifier.classList.remove("hidden");
|
||||
el.updateDownloadBtn.classList.add("hidden");
|
||||
el.updateInstallBtn.classList.remove("hidden");
|
||||
el.updateInstallBtn.title = `Install Update${v}`;
|
||||
return;
|
||||
}
|
||||
if (state.status === 'not-available') {
|
||||
el.updateNotifier.classList.add("hidden");
|
||||
el.updateDownloadBtn.disabled = false;
|
||||
el.updateDownloadBtn.style.opacity = "";
|
||||
return;
|
||||
}
|
||||
if (state.status === 'error') {
|
||||
el.updateDownloadBtn.disabled = false;
|
||||
el.updateDownloadBtn.style.opacity = "";
|
||||
}
|
||||
};
|
||||
|
||||
window.electronAPI.onUpdateState((state) => {
|
||||
applyUpdateState(state);
|
||||
});
|
||||
|
||||
window.electronAPI.onUpdateAvailable((info) => {
|
||||
console.log("Update available:", info.version);
|
||||
|
|
@ -1978,6 +2099,20 @@ function initUpdater() {
|
|||
el.updateInstallBtn.onclick = () => {
|
||||
window.electronAPI.quitAndInstall();
|
||||
};
|
||||
|
||||
// Recover missed startup events and then run a fresh check now that listeners exist.
|
||||
window.electronAPI.getUpdateState?.()
|
||||
.then((state) => applyUpdateState(state))
|
||||
.catch((err) => console.warn("Failed to fetch initial update state", err))
|
||||
.finally(() => {
|
||||
if (window.electronAPI.checkForUpdatesNow) {
|
||||
void window.electronAPI.checkForUpdatesNow()
|
||||
.then((state) => applyUpdateState(state))
|
||||
.catch((err) => console.warn("Direct update check failed", err));
|
||||
} else {
|
||||
window.electronAPI.checkForUpdates?.();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Config fetcher will trigger init()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue