- DB + uploads stored outside app bundle (survives updates) - Router uses DOCUMENT_ROOT instead of __DIR__ (fixes AppImage paths) - Footer count shifts right below 1250px, min-width 1150px - Gallery opens gracefully on fetch errors - Stabilized gallery, export, and find/replace tests with poll loops Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
210 lines
6.4 KiB
JavaScript
210 lines
6.4 KiB
JavaScript
const { app, BrowserWindow, dialog, shell } = require('electron');
|
|
const { spawn, execFileSync } = require('child_process');
|
|
const path = require('path');
|
|
const net = require('net');
|
|
const fs = require('fs');
|
|
|
|
let phpProcess = null;
|
|
let mainWindow = null;
|
|
let serverPort = 0;
|
|
|
|
const isDev = !app.isPackaged;
|
|
|
|
function resolvePath(rel) {
|
|
if (isDev) return path.join(__dirname, '..', rel);
|
|
return path.join(process.resourcesPath, rel);
|
|
}
|
|
|
|
function findBundledPhp() {
|
|
const exe = process.platform === 'win32' ? 'php.exe' : 'php';
|
|
const bundled = isDev
|
|
? path.join(__dirname, 'php-bin', process.platform, exe)
|
|
: path.join(process.resourcesPath, 'php-bin', exe);
|
|
if (fs.existsSync(bundled)) {
|
|
try {
|
|
execFileSync(bundled, ['-v'], { stdio: 'ignore', timeout: 3000 });
|
|
return bundled;
|
|
} catch {}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function findSystemPhp() {
|
|
const candidates = process.platform === 'win32'
|
|
? ['php.exe', 'C:\\php\\php.exe', 'C:\\xampp\\php\\php.exe']
|
|
: ['php', '/usr/bin/php', '/usr/local/bin/php', '/opt/homebrew/bin/php'];
|
|
for (const bin of candidates) {
|
|
try {
|
|
execFileSync(bin, ['-v'], { stdio: 'ignore', timeout: 3000 });
|
|
return bin;
|
|
} catch {}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function findPhp() {
|
|
return findBundledPhp() || findSystemPhp();
|
|
}
|
|
|
|
function checkPhpExtensions(phpBin) {
|
|
try {
|
|
const mods = execFileSync(phpBin, ['-m'], { timeout: 3000 }).toString().toLowerCase();
|
|
const missing = [];
|
|
for (const ext of ['pdo_sqlite', 'zip']) {
|
|
if (!mods.includes(ext)) missing.push(ext);
|
|
}
|
|
return missing;
|
|
} catch { return []; }
|
|
}
|
|
|
|
function getFreePort() {
|
|
return new Promise((resolve, reject) => {
|
|
const srv = net.createServer();
|
|
srv.listen(0, '127.0.0.1', () => {
|
|
const port = srv.address().port;
|
|
srv.close(() => resolve(port));
|
|
});
|
|
srv.on('error', reject);
|
|
});
|
|
}
|
|
|
|
function getDataDir() {
|
|
if (isDev) return path.join(__dirname, 'appdata');
|
|
return path.join(app.getPath('userData'), 'data');
|
|
}
|
|
|
|
function ensureDataDir() {
|
|
const dataDir = getDataDir();
|
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
return dataDir;
|
|
}
|
|
|
|
async function startPhpServer() {
|
|
const phpBin = findPhp();
|
|
if (!phpBin) {
|
|
dialog.showErrorBox('PHP not found',
|
|
'hyprcrd needs PHP 8.1+ with pdo_sqlite.\n\n' +
|
|
'Install PHP and make sure it\'s in your PATH.\n\n' +
|
|
(process.platform === 'darwin' ? 'brew install php' :
|
|
process.platform === 'win32' ? 'https://windows.php.net/download/' :
|
|
'sudo apt install php php-sqlite3'));
|
|
app.quit();
|
|
return null;
|
|
}
|
|
|
|
const missing = checkPhpExtensions(phpBin);
|
|
if (missing.length) {
|
|
dialog.showErrorBox('Missing PHP extensions',
|
|
`hyprcrd needs these PHP extensions: ${missing.join(', ')}\n\n` +
|
|
(process.platform === 'darwin' ? 'They are included with "brew install php".' :
|
|
process.platform === 'win32' ? 'Enable them in php.ini (uncomment extension=...).' :
|
|
`Install with: sudo apt install ${missing.map(e => 'php-' + e.replace('pdo_', '')).join(' ')}`));
|
|
app.quit();
|
|
return null;
|
|
}
|
|
|
|
const dataDir = ensureDataDir();
|
|
serverPort = await getFreePort();
|
|
|
|
const docRoot = resolvePath('public');
|
|
const routerFile = isDev
|
|
? path.join(__dirname, 'router.php')
|
|
: path.join(process.resourcesPath, 'router.php');
|
|
|
|
console.log('[hyprcrd] PHP:', phpBin);
|
|
console.log('[hyprcrd] docRoot:', docRoot);
|
|
console.log('[hyprcrd] router:', routerFile);
|
|
console.log('[hyprcrd] data:', dataDir);
|
|
console.log('[hyprcrd] port:', serverPort);
|
|
|
|
phpProcess = spawn(phpBin, [
|
|
'-S', `127.0.0.1:${serverPort}`,
|
|
'-t', docRoot,
|
|
routerFile,
|
|
], {
|
|
cwd: resolvePath('.'),
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
env: { ...process.env, PHP_CLI_SERVER_WORKERS: '2', HYPRCRD_DATA_DIR: dataDir },
|
|
});
|
|
|
|
phpProcess.stderr.on('data', (d) => {
|
|
const line = d.toString().trim();
|
|
if (line && !line.includes('Accepted') && !line.includes('Closing'))
|
|
console.log('[php]', line);
|
|
});
|
|
|
|
phpProcess.on('exit', (code) => {
|
|
console.log(`[php] exited with code ${code}`);
|
|
phpProcess = null;
|
|
});
|
|
|
|
// Wait for the server to accept connections
|
|
for (let i = 0; i < 50; i++) {
|
|
const ok = await new Promise(resolve => {
|
|
const sock = net.createConnection(serverPort, '127.0.0.1');
|
|
sock.on('connect', () => { sock.destroy(); resolve(true); });
|
|
sock.on('error', () => resolve(false));
|
|
});
|
|
if (ok) return serverPort;
|
|
await new Promise(r => setTimeout(r, 100));
|
|
}
|
|
|
|
dialog.showErrorBox('Server failed', 'PHP server did not start in time.');
|
|
app.quit();
|
|
return null;
|
|
}
|
|
|
|
function createWindow() {
|
|
mainWindow = new BrowserWindow({
|
|
width: 1280,
|
|
height: 820,
|
|
minWidth: 1150,
|
|
minHeight: 500,
|
|
title: 'hyprcrd',
|
|
backgroundColor: '#0f172a',
|
|
autoHideMenuBar: true,
|
|
webPreferences: {
|
|
preload: path.join(__dirname, 'preload.js'),
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
},
|
|
});
|
|
|
|
mainWindow.loadURL(`http://127.0.0.1:${serverPort}/`);
|
|
|
|
mainWindow.webContents.on('did-fail-load', (e, code, desc) => {
|
|
console.log(`[hyprcrd] load failed: ${code} ${desc}`);
|
|
});
|
|
|
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
if (url.startsWith('http://127.0.0.1')) return { action: 'allow' };
|
|
shell.openExternal(url);
|
|
return { action: 'deny' };
|
|
});
|
|
|
|
mainWindow.webContents.on('before-input-event', (e, input) => {
|
|
if (input.key === 'F12') mainWindow.webContents.toggleDevTools();
|
|
if (input.key === 'r' && (input.control || input.meta)) mainWindow.webContents.reload();
|
|
});
|
|
|
|
mainWindow.on('closed', () => { mainWindow = null; });
|
|
}
|
|
|
|
app.on('ready', async () => {
|
|
const port = await startPhpServer();
|
|
if (port) createWindow();
|
|
});
|
|
|
|
app.on('window-all-closed', () => { app.quit(); });
|
|
|
|
app.on('before-quit', () => {
|
|
if (phpProcess) {
|
|
phpProcess.kill('SIGTERM');
|
|
phpProcess = null;
|
|
}
|
|
});
|
|
|
|
app.on('activate', () => {
|
|
if (!mainWindow && serverPort) createWindow();
|
|
});
|