Peercord/Peercord Source/index.js
0% [█ █ █ █ █ █ █ █ █ █] 100% dd2cdd23d7 v1.0.1 Update
2026-06-15 22:34:10 -05:00

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);