396 lines
12 KiB
JavaScript
396 lines
12 KiB
JavaScript
import { app, BrowserWindow, ipcMain, desktopCapturer, protocol, net, Tray, Menu } from 'electron';
|
|
import path from 'path';
|
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
import fs from 'fs';
|
|
import { spawn } from 'child_process';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
// We need to read package.json for version and upgrade link
|
|
const pkgPath = path.join(__dirname, 'package.json');
|
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
|
|
// Register custom protocol BEFORE app is ready to ensure localStorage persistence
|
|
protocol.registerSchemesAsPrivileged([
|
|
{ scheme: 'peercord', privileges: { standard: true, secure: true, supportFetchAPI: true, bypassCSP: true, corsEnabled: true, stream: true } }
|
|
]);
|
|
|
|
// Force app name and userData path BEFORE app.whenReady()
|
|
app.name = 'Peercord';
|
|
|
|
// Prevent duplicate taskbar icons on Windows for portable/ZIP builds.
|
|
if (process.platform === 'win32') {
|
|
app.setAppUserModelId(process.execPath);
|
|
}
|
|
|
|
const appDataPath = path.join(app.getPath('appData'), 'Peercord');
|
|
app.setPath('userData', appDataPath);
|
|
|
|
// Enforce Single Instance Lock
|
|
const gotTheLock = app.requestSingleInstanceLock();
|
|
if (!gotTheLock) {
|
|
app.quit();
|
|
process.exit(0);
|
|
}
|
|
|
|
// Release the Windows directory lock!
|
|
if (!fs.existsSync(appDataPath)) {
|
|
fs.mkdirSync(appDataPath, { recursive: true });
|
|
}
|
|
try {
|
|
process.chdir(appDataPath);
|
|
} catch (e) {
|
|
console.error("Failed to change CWD:", e);
|
|
}
|
|
|
|
function getAppDir() {
|
|
const execName = path.basename(process.execPath).toLowerCase();
|
|
const isDev = execName === 'electron.exe' || execName === 'electron';
|
|
|
|
if (isDev) return null;
|
|
|
|
if (process.platform === 'linux' && process.env.APPIMAGE) return process.env.APPIMAGE;
|
|
if (process.platform === 'darwin') return path.join(process.resourcesPath, '..', '..');
|
|
|
|
return path.dirname(process.execPath);
|
|
}
|
|
|
|
let globalWin = null;
|
|
let tray = null;
|
|
let isQuitting = false;
|
|
let isWindowReady = false;
|
|
let logQueue = [];
|
|
let closeToTray = true;
|
|
|
|
app.on('before-quit', () => {
|
|
isQuitting = true;
|
|
});
|
|
|
|
// Focus existing window if a second instance is launched
|
|
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
|
if (globalWin) {
|
|
if (globalWin.isMinimized()) globalWin.restore();
|
|
globalWin.show();
|
|
globalWin.focus();
|
|
}
|
|
});
|
|
|
|
// Custom logger to pipe Main Process logs to the React F10 Console
|
|
function logToRenderer(level, ...args) {
|
|
const formattedArgs = args.map(a => {
|
|
if (a instanceof Error) return `${a.name}: ${a.message}\n${a.stack}`;
|
|
if (typeof a === 'object') {
|
|
try { return JSON.stringify(a, Object.getOwnPropertyNames(a)); } catch(e) { return String(a); }
|
|
}
|
|
return String(a);
|
|
});
|
|
|
|
console[level](...formattedArgs);
|
|
|
|
if (globalWin && isWindowReady && !globalWin.isDestroyed()) {
|
|
globalWin.webContents.send('main-log', { level, args: formattedArgs });
|
|
} else {
|
|
logQueue.push({ level, args: formattedArgs });
|
|
}
|
|
}
|
|
|
|
ipcMain.on('renderer-ready', () => {
|
|
isWindowReady = true;
|
|
logQueue.forEach(log => {
|
|
if (globalWin && !globalWin.isDestroyed()) {
|
|
globalWin.webContents.send('main-log', log);
|
|
}
|
|
});
|
|
logQueue = [];
|
|
});
|
|
|
|
ipcMain.on('set-tray-setting', (e, val) => {
|
|
closeToTray = val;
|
|
});
|
|
|
|
async function boot() {
|
|
if (process.platform === 'win32') {
|
|
const cmd = process.argv[1];
|
|
if (cmd === '--squirrel-install' || cmd === '--squirrel-updated' || cmd === '--squirrel-uninstall' || cmd === '--squirrel-obsolete') {
|
|
app.quit();
|
|
return;
|
|
}
|
|
}
|
|
|
|
app.disableHardwareAcceleration();
|
|
|
|
await app.whenReady();
|
|
|
|
const appPath = app.getAppPath();
|
|
|
|
protocol.handle('peercord', (request) => {
|
|
if (request.url.startsWith('peercord://local/')) {
|
|
let filePath = decodeURIComponent(request.url.replace('peercord://local/', ''));
|
|
if (process.platform === 'win32' && filePath.startsWith('/')) {
|
|
filePath = filePath.substring(1);
|
|
}
|
|
return net.fetch(pathToFileURL(filePath).href);
|
|
}
|
|
|
|
let url = request.url.replace('peercord://app/', '');
|
|
url = url.split('?')[0].split('#')[0];
|
|
url = decodeURIComponent(url);
|
|
|
|
if (url.startsWith('/')) url = url.substring(1);
|
|
|
|
let filePath = path.join(appPath, url);
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
const distPath = path.join(appPath, 'dist', url);
|
|
if (fs.existsSync(distPath)) filePath = distPath;
|
|
}
|
|
|
|
return net.fetch(pathToFileURL(filePath).href);
|
|
});
|
|
|
|
const iconFile = process.platform === 'win32' ? 'icon.ico' : 'icon.png';
|
|
const iconPath = path.join(appPath, 'assets', iconFile);
|
|
|
|
const win = new BrowserWindow({
|
|
width: 1100,
|
|
height: 750,
|
|
title: "Peercord",
|
|
icon: iconPath,
|
|
frame: false,
|
|
resizable: true,
|
|
maximizable: true,
|
|
thickFrame: true,
|
|
backgroundColor: '#313338',
|
|
webPreferences: {
|
|
nodeIntegration: true,
|
|
contextIsolation: false
|
|
}
|
|
});
|
|
|
|
globalWin = win;
|
|
|
|
win.loadURL('peercord://app/dist/index.html');
|
|
|
|
function updateTrayVisibility() {
|
|
if (win.isVisible()) {
|
|
if (tray) {
|
|
tray.destroy();
|
|
tray = null;
|
|
}
|
|
} else {
|
|
if (!tray) {
|
|
tray = new Tray(iconPath);
|
|
const contextMenu = Menu.buildFromTemplate([
|
|
{ label: 'Show Peercord', click: () => { win.show(); win.focus(); } },
|
|
{ type: 'separator' },
|
|
{ label: 'Quit Peercord', click: () => { isQuitting = true; app.quit(); } }
|
|
]);
|
|
tray.setToolTip('Peercord');
|
|
tray.setContextMenu(contextMenu);
|
|
tray.on('click', () => {
|
|
win.show();
|
|
win.focus();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
win.on('show', updateTrayVisibility);
|
|
win.on('hide', updateTrayVisibility);
|
|
|
|
win.on('close', (event) => {
|
|
if (!isQuitting) {
|
|
if (closeToTray) {
|
|
event.preventDefault();
|
|
win.hide();
|
|
} else {
|
|
isQuitting = true;
|
|
app.quit();
|
|
}
|
|
}
|
|
});
|
|
|
|
win.webContents.on('before-input-event', (event, input) => {
|
|
if (input.key === 'F12') {
|
|
win.webContents.toggleDevTools();
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
|
|
ipcMain.on('window-action', (event, action) => {
|
|
if (action === 'minimize') win.minimize();
|
|
if (action === 'maximize') win.isMaximized() ? win.restore() : win.maximize();
|
|
if (action === 'close') win.close();
|
|
});
|
|
|
|
win.on('maximize', () => win.webContents.send('window-state-changed', true));
|
|
win.on('unmaximize', () => win.webContents.send('window-state-changed', false));
|
|
|
|
ipcMain.handle('get-desktop-sources', async () => {
|
|
const sources = await desktopCapturer.getSources({
|
|
types:['window', 'screen'],
|
|
thumbnailSize: { width: 320, height: 180 }
|
|
});
|
|
|
|
return sources.map(s => ({
|
|
id: s.id,
|
|
name: s.name,
|
|
thumbnailDataURL: s.thumbnail.toDataURL()
|
|
}));
|
|
});
|
|
|
|
// PEAR OTA UPDATER LOGIC
|
|
let pear = null;
|
|
try {
|
|
const { default: PearRuntime } = await import('pear-runtime');
|
|
|
|
const resolvedAppDir = getAppDir();
|
|
logToRenderer('info', '[Pear] Resolved App Directory for Updater:', resolvedAppDir);
|
|
|
|
pear = new PearRuntime({
|
|
...pkg,
|
|
dir: path.join(app.getPath('userData'), 'pear-data'),
|
|
app: resolvedAppDir
|
|
});
|
|
|
|
pear.on('error', (err) => logToRenderer('error', '[Pear Error]', err));
|
|
} catch (e) {
|
|
logToRenderer('error', '[Pear] Failed to initialize PearRuntime:', e.message, e.stack);
|
|
}
|
|
|
|
if (pear && pear.updater) {
|
|
logToRenderer('info', '[Pear] Updater initialized. Current version:', pkg.version);
|
|
|
|
pear.updater.on('updating', () => {
|
|
logToRenderer('info', '[Pear] updating event fired. Downloading update...');
|
|
if (win && !win.isDestroyed()) win.webContents.send('pear-updating');
|
|
});
|
|
|
|
pear.updater.on('updated', () => {
|
|
logToRenderer('info', '[Pear] updated event fired. Update downloaded and ready.');
|
|
if (win && !win.isDestroyed()) win.webContents.send('pear-updated');
|
|
});
|
|
|
|
pear.updater.on('error', (err) => {
|
|
logToRenderer('error', '[Pear] Updater Error:', err);
|
|
if (win && !win.isDestroyed()) {
|
|
win.webContents.send('pear-error', err instanceof Error ? err.message : String(err));
|
|
}
|
|
});
|
|
} else {
|
|
logToRenderer('warn', '[Pear] Updater not available on pear object');
|
|
}
|
|
|
|
ipcMain.on('normal-restart', () => {
|
|
isQuitting = true;
|
|
app.relaunch();
|
|
app.quit();
|
|
});
|
|
|
|
ipcMain.on('apply-update', async () => {
|
|
logToRenderer('info', '[Pear] apply-update requested by renderer');
|
|
try {
|
|
const baseDir = getAppDir();
|
|
if (!baseDir) throw new Error("Cannot apply update in dev mode");
|
|
|
|
const nextDir = path.join(appDataPath, 'pear-data', 'pear-runtime', 'next');
|
|
if (!fs.existsSync(nextDir)) throw new Error("Update cache directory not found");
|
|
|
|
const versions = fs.readdirSync(nextDir).filter(v => fs.statSync(path.join(nextDir, v)).isDirectory());
|
|
if (versions.length === 0) throw new Error("No downloaded updates found");
|
|
|
|
versions.sort((a, b) => parseFloat(b) - parseFloat(a));
|
|
const latestVersion = versions[0];
|
|
|
|
const archDir = `${process.platform}-${process.arch}`;
|
|
const updateAppPath = path.join(nextDir, latestVersion, 'by-arch', archDir, 'app', 'peercord');
|
|
|
|
if (!fs.existsSync(updateAppPath)) throw new Error(`Update files not found at ${updateAppPath}`);
|
|
|
|
if (process.platform === 'win32') {
|
|
logToRenderer('info', '[Pear] Windows detected. Using detached script to bypass OS file locks...');
|
|
|
|
const batPath = path.join(app.getPath('temp'), `peercord-update-${Date.now()}.bat`);
|
|
const vbsPath = path.join(app.getPath('temp'), `peercord-update-${Date.now()}.vbs`);
|
|
|
|
const batContent = `
|
|
@echo off
|
|
:wait
|
|
tasklist /FI "PID eq ${process.pid}" /NH | findstr /C:"${process.pid}" > nul
|
|
if %ERRORLEVEL% == 0 (
|
|
timeout /t 1 /nobreak > nul
|
|
goto wait
|
|
)
|
|
|
|
xcopy /E /Y /I /H /C "${updateAppPath}\\*" "${baseDir}\\"
|
|
|
|
start "" "${process.execPath}"
|
|
del "%~1"
|
|
del "%~f0"
|
|
`;
|
|
|
|
const vbsContent = `
|
|
Dim WshShell
|
|
Set WshShell = CreateObject("WScript.Shell")
|
|
WshShell.Run "cmd.exe /c """"" & WScript.Arguments(0) & """ """ & WScript.Arguments(1) & """""", 0, False
|
|
`;
|
|
|
|
fs.writeFileSync(batPath, batContent);
|
|
fs.writeFileSync(vbsPath, vbsContent);
|
|
|
|
const child = spawn('wscript.exe', [vbsPath, batPath, vbsPath], {
|
|
detached: true,
|
|
stdio: 'ignore',
|
|
windowsHide: true
|
|
});
|
|
child.unref();
|
|
|
|
logToRenderer('info', '[Pear] Detached script spawned. Quitting app to allow swap...');
|
|
isQuitting = true;
|
|
app.quit();
|
|
} else {
|
|
logToRenderer('info', '[Pear] macOS/Linux detected. Using detached bash script for reliable directory swap...');
|
|
|
|
const shPath = path.join(app.getPath('temp'), `peercord-update-${Date.now()}.sh`);
|
|
|
|
const shContent = `#!/bin/bash
|
|
# Wait for the Electron process to exit
|
|
while kill -0 ${process.pid} 2>/dev/null; do
|
|
sleep 0.5
|
|
done
|
|
|
|
# Copy the new app directory contents into place, overwriting existing files
|
|
cp -rf "${updateAppPath}/." "${baseDir}/"
|
|
|
|
# Ensure the new binary is executable
|
|
chmod -R 755 "${baseDir}"
|
|
|
|
# Launch the new app
|
|
"${process.execPath}" &
|
|
|
|
# Delete this script
|
|
rm "$0"
|
|
`;
|
|
fs.writeFileSync(shPath, shContent);
|
|
fs.chmodSync(shPath, '755');
|
|
|
|
const child = spawn(shPath, [], {
|
|
detached: true,
|
|
stdio: 'ignore'
|
|
});
|
|
child.unref();
|
|
|
|
logToRenderer('info', '[Pear] Detached script spawned. Quitting app to allow swap...');
|
|
isQuitting = true;
|
|
app.quit();
|
|
}
|
|
} catch (err) {
|
|
logToRenderer('error', '[Pear] Failed to apply update:', err);
|
|
if (globalWin && !globalWin.isDestroyed()) {
|
|
globalWin.webContents.send('pear-error', err instanceof Error ? err.message : String(err));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
boot().catch(console.error); |