293 lines
9 KiB
JavaScript
293 lines
9 KiB
JavaScript
const { app, BrowserWindow, session, desktopCapturer, ipcMain, clipboard } = require('electron');
|
|
const { autoUpdater } = require('electron-updater');
|
|
const path = require('path');
|
|
const packageJson = require('./package.json');
|
|
|
|
const updateState = {
|
|
status: 'idle', // idle | checking | available | downloading | downloaded | installing | 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 isNewerVersionAvailable(info) {
|
|
const next = info && typeof info.version === 'string' ? info.version : '';
|
|
return Boolean(next) && next !== app.getVersion();
|
|
}
|
|
|
|
let updateCheckInProgress = false;
|
|
let installInProgress = false;
|
|
|
|
function resolveBackendUrl() {
|
|
const configuredUrl = (process.env.CHATTZ_URL || process.env.APP_BASE_URL || '').trim();
|
|
if (configuredUrl) {
|
|
return configuredUrl.replace(/\/$/, '');
|
|
}
|
|
|
|
const packagedFallback = typeof packageJson.homepage === 'string'
|
|
? packageJson.homepage.trim()
|
|
: '';
|
|
if (app.isPackaged && packagedFallback) {
|
|
return packagedFallback.replace(/\/$/, '');
|
|
}
|
|
|
|
return 'http://localhost:3000';
|
|
}
|
|
|
|
function webContentsOrigin(webContents) {
|
|
if (!webContents || typeof webContents.getURL !== 'function') {
|
|
return null;
|
|
}
|
|
return safeOrigin(webContents.getURL());
|
|
}
|
|
|
|
async function runUpdateCheck(reason = 'manual') {
|
|
if (installInProgress) {
|
|
console.log(`Update check skipped (${reason}): install already in progress`);
|
|
return;
|
|
}
|
|
if (updateCheckInProgress) {
|
|
console.log(`Update check skipped (${reason}): another check is already in progress`);
|
|
return;
|
|
}
|
|
updateCheckInProgress = true;
|
|
updateState.status = 'checking';
|
|
updateState.error = null;
|
|
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;
|
|
} else {
|
|
updateState.status = 'not-available';
|
|
updateState.info = info;
|
|
updateState.error = null;
|
|
}
|
|
} catch (err) {
|
|
updateState.status = 'error';
|
|
updateState.error = err && err.message ? err.message : String(err);
|
|
console.error(`Update check failed (${reason})`, err);
|
|
} finally {
|
|
updateCheckInProgress = false;
|
|
broadcastUpdateState();
|
|
}
|
|
}
|
|
|
|
function installDownloadedUpdate() {
|
|
if (installInProgress) {
|
|
console.log('Update install already in progress');
|
|
return;
|
|
}
|
|
|
|
installInProgress = true;
|
|
updateState.status = 'installing';
|
|
updateState.error = null;
|
|
broadcastUpdateState();
|
|
|
|
const wins = BrowserWindow.getAllWindows();
|
|
for (const win of wins) {
|
|
if (win.isDestroyed()) continue;
|
|
try {
|
|
win.removeAllListeners('close');
|
|
win.destroy();
|
|
} catch (err) {
|
|
console.warn('Failed to destroy window before update install', err);
|
|
}
|
|
}
|
|
|
|
setImmediate(() => {
|
|
try {
|
|
autoUpdater.quitAndInstall(false, true);
|
|
} catch (err) {
|
|
installInProgress = false;
|
|
updateState.status = 'error';
|
|
updateState.error = err && err.message ? err.message : String(err);
|
|
broadcastUpdateState();
|
|
console.error('quitAndInstall failed', err);
|
|
}
|
|
});
|
|
|
|
setTimeout(() => {
|
|
if (installInProgress) {
|
|
console.warn('Update install is still waiting for app shutdown');
|
|
}
|
|
}, 15000);
|
|
}
|
|
|
|
function createWindow() {
|
|
const sess = session.fromPartition('persist:chattz');
|
|
const backendUrl = resolveBackendUrl();
|
|
const backendOrigin = new URL(backendUrl).origin;
|
|
console.log(`Desktop backend URL: ${backendUrl}`);
|
|
|
|
const win = new BrowserWindow({
|
|
width: 1200,
|
|
height: 800,
|
|
title: "Chattz Desktop",
|
|
webPreferences: {
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
session: sess,
|
|
preload: path.join(__dirname, 'desktop', 'preload.js')
|
|
}
|
|
});
|
|
|
|
win.setAutoHideMenuBar(true);
|
|
win.setMenuBarVisibility(true);
|
|
|
|
sess.setPermissionCheckHandler((webContents, permission) => {
|
|
const origin = webContentsOrigin(webContents);
|
|
if (origin !== backendOrigin) return false;
|
|
return permission === 'media' || permission === 'clipboard-write';
|
|
});
|
|
|
|
sess.setPermissionRequestHandler((webContents, permission, callback) => {
|
|
const origin = webContentsOrigin(webContents);
|
|
if (origin === backendOrigin && (permission === 'media' || permission === 'clipboard-write')) {
|
|
callback(true);
|
|
} else {
|
|
callback(false);
|
|
}
|
|
});
|
|
|
|
sess.setDisplayMediaRequestHandler((request, callback) => {
|
|
const origin = safeOrigin(request.frame?.url || win.webContents.getURL());
|
|
if (origin !== backendOrigin) {
|
|
callback(null);
|
|
return;
|
|
}
|
|
desktopCapturer.getSources({ types: ['screen', 'window'] }).then((sources) => {
|
|
if (sources && sources.length > 0) {
|
|
const screenSource = sources.find(s => s.id.startsWith('screen')) || sources[0];
|
|
callback({ video: screenSource, audio: 'loopback' });
|
|
} else {
|
|
callback(null);
|
|
}
|
|
}).catch(err => {
|
|
console.error("Failed to get desktop sources for screen share", err);
|
|
callback(null);
|
|
});
|
|
});
|
|
|
|
win.loadURL(backendUrl).catch((err) => {
|
|
console.error(`Failed to load desktop app: ${err}`);
|
|
});
|
|
|
|
// Ensure renderer receives latest updater state after any (re)load.
|
|
// Delay broadcast by 300ms to give the renderer time to register its
|
|
// onUpdateState IPC listener before we push state.
|
|
win.webContents.on('did-finish-load', () => {
|
|
setTimeout(() => {
|
|
if (!win.isDestroyed()) broadcastUpdateState();
|
|
}, 300);
|
|
});
|
|
|
|
// Uncomment to debug
|
|
// win.webContents.openDevTools();
|
|
}
|
|
|
|
app.commandLine.appendSwitch('disable-webrtc-hw-encoding'); // Sometime helps resolve codec mismatch behavior
|
|
app.commandLine.appendSwitch('log-level', '3'); // Silences standard Chromium warning logs (like SRTP transport unprotect logs which are benign timing issues)
|
|
|
|
app.whenReady().then(() => {
|
|
ipcMain.handle('clipboard-write', (event, text) => {
|
|
clipboard.writeText(text);
|
|
return true;
|
|
});
|
|
|
|
ipcMain.handle('get-update-state', () => ({ ...updateState }));
|
|
ipcMain.handle('check-for-updates-now', async () => {
|
|
await runUpdateCheck('renderer-direct');
|
|
return { ...updateState };
|
|
});
|
|
|
|
createWindow();
|
|
|
|
// Configure Auto-Updater
|
|
autoUpdater.autoDownload = false;
|
|
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', () => {
|
|
void runUpdateCheck('manual');
|
|
});
|
|
|
|
ipcMain.on('download-update', () => {
|
|
if (installInProgress) return;
|
|
updateState.status = 'downloading';
|
|
updateState.error = null;
|
|
broadcastUpdateState();
|
|
autoUpdater.downloadUpdate();
|
|
});
|
|
|
|
ipcMain.on('quit-and-install', () => {
|
|
installDownloadedUpdate();
|
|
});
|
|
|
|
// Check once on startup
|
|
setTimeout(() => {
|
|
void runUpdateCheck('startup');
|
|
}, 5000);
|
|
|
|
app.on('activate', () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
createWindow();
|
|
}
|
|
});
|
|
});
|
|
|
|
app.on('window-all-closed', () => {
|
|
if (process.platform !== 'darwin') {
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
function safeOrigin(value) {
|
|
try {
|
|
return new URL(value).origin;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|