BetterCurseForge/bettercf/core/main-process.js

269 lines
8.7 KiB
JavaScript
Raw Permalink Normal View History

// 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.');