310 lines
10 KiB
JavaScript
310 lines
10 KiB
JavaScript
// BetterCurseForge — Main Process Hooks
|
|
// ======================================
|
|
// Runs in Electron's main process BEFORE CurseForge loads.
|
|
// Blocks ad network requests via session.webRequest.
|
|
// Manages themes, plugins, and settings IPC channels.
|
|
|
|
import { app, session, ipcMain, shell } from 'electron';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
const USER_DATA = join(app.getPath('userData'), 'BetterCurseForge');
|
|
const THEMES_DIR = join(USER_DATA, 'themes');
|
|
const PLUGINS_DIR = join(USER_DATA, 'plugins');
|
|
const SETTINGS_FILE = join(USER_DATA, 'settings.json');
|
|
|
|
// Ensure user directories exist
|
|
[THEMES_DIR, PLUGINS_DIR].forEach(d => { try { mkdirSync(d, { recursive: true }); } catch {} });
|
|
|
|
// Seed built-in themes and plugins if user directories are empty
|
|
try {
|
|
const builtinThemes = join(__dirname, '..', 'themes');
|
|
if (existsSync(builtinThemes)) {
|
|
for (const f of readdirSync(builtinThemes)) {
|
|
try {
|
|
const src = join(builtinThemes, f);
|
|
const dst = join(THEMES_DIR, f);
|
|
if (!existsSync(dst)) {
|
|
writeFileSync(dst, readFileSync(src));
|
|
}
|
|
} catch {}
|
|
}
|
|
}
|
|
} catch {}
|
|
|
|
// ── Settings ──────────────────────────────────────────────────
|
|
|
|
let settings = {
|
|
adsBlocked: true,
|
|
enabledThemes: [],
|
|
enabledPlugins: [],
|
|
activeCustomTheme: null,
|
|
};
|
|
|
|
try {
|
|
if (existsSync(SETTINGS_FILE)) {
|
|
Object.assign(settings, JSON.parse(readFileSync(SETTINGS_FILE, 'utf-8')));
|
|
}
|
|
} catch {}
|
|
|
|
function saveSettings() {
|
|
try { writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2)); } catch {}
|
|
}
|
|
|
|
// ── Ad Blocking (Network Layer) ───────────────────────────────
|
|
|
|
const AD_URL_PATTERNS = [
|
|
'*://*.overwolf.com/ads/*',
|
|
'*://ads.overwolf.com/*',
|
|
'*://content.overwolf.com/ads/*',
|
|
'*://*.adrta.com/*',
|
|
'*://*.adnxs.com/*',
|
|
'*://*.doubleclick.net/*',
|
|
'*://*.googlesyndication.com/*',
|
|
'*://*.googleadservices.com/*',
|
|
'*://*.moatads.com/*',
|
|
'*://*.amazon-adsystem.com/*',
|
|
'*://*.adsrvr.org/*',
|
|
'*://*.adscorebr.com/*',
|
|
'*://*.serving-sys.com/*',
|
|
'*://*.adskeeper.co.uk/*',
|
|
];
|
|
|
|
app.whenReady().then(() => {
|
|
if (!settings.adsBlocked) return;
|
|
|
|
try {
|
|
const ses = session.defaultSession;
|
|
ses.webRequest.onBeforeRequest({ urls: AD_URL_PATTERNS }, (details, callback) => {
|
|
callback({ cancel: true });
|
|
});
|
|
console.log('[BetterCF] Network ad blocking enabled.');
|
|
} catch (err) {
|
|
console.error('[BetterCF] Failed to set up ad blocking:', err);
|
|
}
|
|
});
|
|
|
|
// ── Renderer Injection via app event ──────────────────────────
|
|
|
|
const rendererCode = (() => {
|
|
const rendererPath = join(__dirname, 'renderer.js');
|
|
if (existsSync(rendererPath)) {
|
|
return readFileSync(rendererPath, 'utf-8');
|
|
}
|
|
return null;
|
|
})();
|
|
|
|
app.on('browser-window-created', (_event, window) => {
|
|
if (!rendererCode) return;
|
|
|
|
window.webContents.on('did-finish-load', () => {
|
|
// Inject the IPC bridge first (since contextIsolation is false, this works)
|
|
const ipcBridge = `
|
|
if (!window.__BCF_IPC__) {
|
|
try {
|
|
const { ipcRenderer } = require('electron');
|
|
window.__BCF_IPC__ = {
|
|
invoke: (...args) => ipcRenderer.invoke(...args),
|
|
};
|
|
} catch (e) {}
|
|
}
|
|
window.__BCF_CORE_PATH__ = ${JSON.stringify(__dirname)};
|
|
`;
|
|
|
|
window.webContents.executeJavaScript(ipcBridge).then(() => {
|
|
return window.webContents.executeJavaScript(rendererCode);
|
|
}).then(() => {
|
|
console.log('[BetterCF] Renderer injected into window.');
|
|
}).catch(err => {
|
|
console.error('[BetterCF] Renderer injection failed:', err);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ── IPC Channels ──────────────────────────────────────────────
|
|
|
|
ipcMain.handle('bcf:themes:list', () => {
|
|
try {
|
|
if (!existsSync(THEMES_DIR)) return [];
|
|
const entries = readdirSync(THEMES_DIR, { withFileTypes: true });
|
|
const themes = [];
|
|
|
|
for (const entry of entries) {
|
|
if (entry.isFile() && entry.name.endsWith('.css')) {
|
|
const name = entry.name.replace('.css', '');
|
|
const css = readFileSync(join(THEMES_DIR, entry.name), 'utf-8');
|
|
const manifestPath = join(THEMES_DIR, `${name}.json`);
|
|
let manifest = {
|
|
name: name.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
|
|
author: 'Custom',
|
|
description: '',
|
|
version: '1.0.0',
|
|
primaryColor: '#8b5cf6',
|
|
titleColor: '#a78bfa',
|
|
bodyColor: '#2e1065',
|
|
backgroundColor: '#161026',
|
|
};
|
|
if (existsSync(manifestPath)) {
|
|
try { Object.assign(manifest, JSON.parse(readFileSync(manifestPath, 'utf-8'))); } catch {}
|
|
}
|
|
themes.push({
|
|
...manifest,
|
|
fileName: entry.name,
|
|
css,
|
|
enabled: settings.enabledThemes.includes(entry.name) || settings.activeCustomTheme === entry.name,
|
|
});
|
|
} else if (entry.isDirectory()) {
|
|
const cssPath = join(THEMES_DIR, entry.name, 'theme.css');
|
|
const manifestPath = join(THEMES_DIR, entry.name, 'theme.json');
|
|
if (existsSync(cssPath)) {
|
|
const css = readFileSync(cssPath, 'utf-8');
|
|
let manifest = {
|
|
name: entry.name.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
|
|
author: 'Custom',
|
|
description: '',
|
|
version: '1.0.0',
|
|
primaryColor: '#8b5cf6',
|
|
titleColor: '#a78bfa',
|
|
bodyColor: '#2e1065',
|
|
backgroundColor: '#161026',
|
|
};
|
|
if (existsSync(manifestPath)) {
|
|
try { Object.assign(manifest, JSON.parse(readFileSync(manifestPath, 'utf-8'))); } catch {}
|
|
}
|
|
themes.push({
|
|
...manifest,
|
|
fileName: entry.name,
|
|
css,
|
|
enabled: settings.enabledThemes.includes(entry.name) || settings.activeCustomTheme === entry.name,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return themes;
|
|
} catch { return []; }
|
|
});
|
|
|
|
ipcMain.handle('bcf:plugins:list', () => {
|
|
try {
|
|
if (!existsSync(PLUGINS_DIR)) return [];
|
|
const entries = readdirSync(PLUGINS_DIR, { withFileTypes: true });
|
|
const plugins = [];
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.isDirectory()) continue;
|
|
const manifestPath = join(PLUGINS_DIR, entry.name, 'manifest.json');
|
|
if (!existsSync(manifestPath)) continue;
|
|
try {
|
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
const indexPath = join(PLUGINS_DIR, entry.name, 'index.js');
|
|
const stylePath = join(PLUGINS_DIR, entry.name, 'styles.css');
|
|
plugins.push({
|
|
...manifest, dirName: entry.name,
|
|
hasScript: existsSync(indexPath), hasStyles: existsSync(stylePath),
|
|
enabled: settings.enabledPlugins.includes(entry.name),
|
|
script: existsSync(indexPath) ? readFileSync(indexPath, 'utf-8') : null,
|
|
styles: existsSync(stylePath) ? readFileSync(stylePath, 'utf-8') : null,
|
|
});
|
|
} catch {}
|
|
}
|
|
return plugins;
|
|
} catch { return []; }
|
|
});
|
|
|
|
ipcMain.handle('bcf:settings:get', () => ({ ...settings }));
|
|
ipcMain.handle('bcf:settings:set', (_, key, value) => {
|
|
settings[key] = value;
|
|
saveSettings();
|
|
return true;
|
|
});
|
|
|
|
ipcMain.handle('bcf:settings:toggle-theme', (_, fileName) => {
|
|
const idx = settings.enabledThemes.indexOf(fileName);
|
|
if (idx >= 0) settings.enabledThemes.splice(idx, 1);
|
|
else settings.enabledThemes.push(fileName);
|
|
saveSettings();
|
|
return settings.enabledThemes.includes(fileName);
|
|
});
|
|
|
|
ipcMain.handle('bcf:settings:toggle-plugin', (_, dirName) => {
|
|
const idx = settings.enabledPlugins.indexOf(dirName);
|
|
if (idx >= 0) settings.enabledPlugins.splice(idx, 1);
|
|
else settings.enabledPlugins.push(dirName);
|
|
saveSettings();
|
|
return settings.enabledPlugins.includes(dirName);
|
|
});
|
|
|
|
ipcMain.handle('bcf:open-themes-folder', async () => {
|
|
try {
|
|
await shell.openPath(THEMES_DIR);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('bcf:open-plugins-folder', async () => {
|
|
try {
|
|
await shell.openPath(PLUGINS_DIR);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('bcf:info', () => ({
|
|
version: '1.0.0',
|
|
bcfRoot: USER_DATA,
|
|
themesDir: THEMES_DIR,
|
|
pluginsDir: PLUGINS_DIR,
|
|
settingsFile: SETTINGS_FILE,
|
|
}));
|
|
|
|
console.log('[BetterCF] Main process hooks installed.');
|
|
|
|
// ── Auto-Repatch on Update Interceptor ────────────────────────
|
|
try {
|
|
const { spawn } = await import('node:child_process');
|
|
const patcherExe = join(process.env.LOCALAPPDATA || '', 'BetterCurseForge', 'BetterCurseForge.exe');
|
|
|
|
const onUpdateTriggered = () => {
|
|
try {
|
|
if (existsSync(patcherExe)) {
|
|
spawn(patcherExe, ['--wait-for-update', process.pid.toString()], {
|
|
detached: true,
|
|
stdio: 'ignore'
|
|
}).unref();
|
|
console.log('[BetterCF] Update pending. Spawned patcher --wait-for-update');
|
|
}
|
|
} catch (e) {
|
|
console.error('[BetterCF] Failed to spawn update patcher:', e);
|
|
}
|
|
};
|
|
|
|
// 1. Check if curseforge-updater has a pending update when quitting
|
|
app.on('before-quit', () => {
|
|
const pendingInstaller = join(process.env.LOCALAPPDATA || '', 'curseforge-updater', 'pending', 'setup.exe');
|
|
const directInstaller = join(process.env.LOCALAPPDATA || '', 'curseforge-updater', 'installer.exe');
|
|
if (existsSync(pendingInstaller) || existsSync(directInstaller)) {
|
|
onUpdateTriggered();
|
|
}
|
|
});
|
|
|
|
// 2. Ensure watcher daemon is running
|
|
setTimeout(() => {
|
|
try {
|
|
if (existsSync(patcherExe)) {
|
|
spawn(patcherExe, ['--ensure-watcher'], {
|
|
detached: true,
|
|
stdio: 'ignore'
|
|
}).unref();
|
|
}
|
|
} catch {}
|
|
}, 4000);
|
|
} catch (err) {
|
|
console.error('[BetterCF] Auto-repatch hook initialization error:', err);
|
|
}
|