From c20abd87908eedd756b581ee5c8f098131445b67 Mon Sep 17 00:00:00 2001 From: tomasalias Date: Tue, 6 Oct 2026 17:03:16 +0200 Subject: [PATCH] feat: initial commit for BetterCurseForge client modification suite --- .gitignore | 38 + BetterCurseForge.spec | 38 + LICENSE | 21 + README.md | 165 ++ bettercf/core/main-process.js | 310 ++ bettercf/core/preload.js | 79 + bettercf/core/renderer.js | 933 ++++++ bettercf/plugins/compact-mode/index.js | 65 + bettercf/plugins/compact-mode/manifest.json | 6 + bettercf/themes/purple-night.css | 215 ++ bettercf/themes/purple-night.json | 10 + docs/.vitepress/config.ts | 81 + docs/.vitepress/theme/custom.css | 38 + docs/.vitepress/theme/index.ts | 6 + docs/api/index.md | 261 ++ docs/guide/installation.md | 179 ++ docs/guide/plugins.md | 215 ++ docs/guide/themes.md | 248 ++ docs/index.md | 67 + docs/package-lock.json | 2519 +++++++++++++++++ docs/package.json | 13 + docs/public/logo.svg | 20 + patcher/patch_core.py | 979 +++++++ patcher/patcher.py | 63 + patcher/patcher_gui.py | 427 +++ patcher/vendor/asar/node_modules/.bin/asar | 16 + .../vendor/asar/node_modules/.bin/asar.cmd | 17 + .../vendor/asar/node_modules/.bin/asar.ps1 | 28 + .../asar/node_modules/.package-lock.json | 110 + .../node_modules/@electron/asar/LICENSE.md | 20 + .../node_modules/@electron/asar/README.md | 233 ++ .../node_modules/@electron/asar/bin/asar.mjs | 182 ++ .../node_modules/@electron/asar/lib/asar.d.ts | 75 + .../node_modules/@electron/asar/lib/asar.js | 312 ++ .../@electron/asar/lib/asar.js.map | 1 + .../@electron/asar/lib/crawlfs.d.ts | 13 + .../@electron/asar/lib/crawlfs.js | 58 + .../@electron/asar/lib/crawlfs.js.map | 1 + .../node_modules/@electron/asar/lib/disk.d.ts | 50 + .../node_modules/@electron/asar/lib/disk.js | 340 +++ .../@electron/asar/lib/disk.js.map | 1 + .../@electron/asar/lib/filesystem.d.ts | 62 + .../@electron/asar/lib/filesystem.js | 256 ++ .../@electron/asar/lib/filesystem.js.map | 1 + .../@electron/asar/lib/integrity.d.ts | 8 + .../@electron/asar/lib/integrity.js | 58 + .../@electron/asar/lib/integrity.js.map | 1 + .../@electron/asar/lib/path-validation.d.ts | 6 + .../@electron/asar/lib/path-validation.js | 16 + .../@electron/asar/lib/path-validation.js.map | 1 + .../@electron/asar/lib/pickle.d.ts | 46 + .../node_modules/@electron/asar/lib/pickle.js | 205 ++ .../@electron/asar/lib/pickle.js.map | 1 + .../@electron/asar/lib/wrapped-fs.d.ts | 13 + .../@electron/asar/lib/wrapped-fs.js | 23 + .../@electron/asar/lib/wrapped-fs.js.map | 1 + .../node_modules/@electron/asar/package.json | 78 + .../node_modules/balanced-match/LICENSE.md | 23 + .../node_modules/balanced-match/README.md | 57 + .../node_modules/balanced-match/package.json | 68 + .../asar/node_modules/brace-expansion/LICENSE | 23 + .../node_modules/brace-expansion/README.md | 94 + .../node_modules/brace-expansion/package.json | 64 + .../vendor/asar/node_modules/glob/LICENSE.md | 63 + .../vendor/asar/node_modules/glob/README.md | 1203 ++++++++ .../asar/node_modules/glob/package.json | 98 + .../asar/node_modules/lru-cache/LICENSE.md | 55 + .../asar/node_modules/lru-cache/README.md | 469 +++ .../asar/node_modules/lru-cache/package.json | 154 + .../asar/node_modules/minimatch/LICENSE.md | 55 + .../asar/node_modules/minimatch/README.md | 528 ++++ .../asar/node_modules/minimatch/package.json | 73 + .../asar/node_modules/minipass/LICENSE.md | 55 + .../asar/node_modules/minipass/README.md | 825 ++++++ .../asar/node_modules/minipass/package.json | 77 + .../asar/node_modules/path-scurry/LICENSE.md | 55 + .../asar/node_modules/path-scurry/README.md | 636 +++++ .../node_modules/path-scurry/package.json | 72 + patcher/vendor/asar/package.json | 9 + 79 files changed, 13985 insertions(+) create mode 100644 .gitignore create mode 100644 BetterCurseForge.spec create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bettercf/core/main-process.js create mode 100644 bettercf/core/preload.js create mode 100644 bettercf/core/renderer.js create mode 100644 bettercf/plugins/compact-mode/index.js create mode 100644 bettercf/plugins/compact-mode/manifest.json create mode 100644 bettercf/themes/purple-night.css create mode 100644 bettercf/themes/purple-night.json create mode 100644 docs/.vitepress/config.ts create mode 100644 docs/.vitepress/theme/custom.css create mode 100644 docs/.vitepress/theme/index.ts create mode 100644 docs/api/index.md create mode 100644 docs/guide/installation.md create mode 100644 docs/guide/plugins.md create mode 100644 docs/guide/themes.md create mode 100644 docs/index.md create mode 100644 docs/package-lock.json create mode 100644 docs/package.json create mode 100644 docs/public/logo.svg create mode 100644 patcher/patch_core.py create mode 100644 patcher/patcher.py create mode 100644 patcher/patcher_gui.py create mode 100644 patcher/vendor/asar/node_modules/.bin/asar create mode 100644 patcher/vendor/asar/node_modules/.bin/asar.cmd create mode 100644 patcher/vendor/asar/node_modules/.bin/asar.ps1 create mode 100644 patcher/vendor/asar/node_modules/.package-lock.json create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/LICENSE.md create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/README.md create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/bin/asar.mjs create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/asar.d.ts create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/asar.js create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/asar.js.map create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/crawlfs.d.ts create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/crawlfs.js create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/crawlfs.js.map create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/disk.d.ts create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/disk.js create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/disk.js.map create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/filesystem.d.ts create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/filesystem.js create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/filesystem.js.map create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/integrity.d.ts create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/integrity.js create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/integrity.js.map create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/path-validation.d.ts create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/path-validation.js create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/path-validation.js.map create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/pickle.d.ts create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/pickle.js create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/pickle.js.map create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/wrapped-fs.d.ts create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/wrapped-fs.js create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/lib/wrapped-fs.js.map create mode 100644 patcher/vendor/asar/node_modules/@electron/asar/package.json create mode 100644 patcher/vendor/asar/node_modules/balanced-match/LICENSE.md create mode 100644 patcher/vendor/asar/node_modules/balanced-match/README.md create mode 100644 patcher/vendor/asar/node_modules/balanced-match/package.json create mode 100644 patcher/vendor/asar/node_modules/brace-expansion/LICENSE create mode 100644 patcher/vendor/asar/node_modules/brace-expansion/README.md create mode 100644 patcher/vendor/asar/node_modules/brace-expansion/package.json create mode 100644 patcher/vendor/asar/node_modules/glob/LICENSE.md create mode 100644 patcher/vendor/asar/node_modules/glob/README.md create mode 100644 patcher/vendor/asar/node_modules/glob/package.json create mode 100644 patcher/vendor/asar/node_modules/lru-cache/LICENSE.md create mode 100644 patcher/vendor/asar/node_modules/lru-cache/README.md create mode 100644 patcher/vendor/asar/node_modules/lru-cache/package.json create mode 100644 patcher/vendor/asar/node_modules/minimatch/LICENSE.md create mode 100644 patcher/vendor/asar/node_modules/minimatch/README.md create mode 100644 patcher/vendor/asar/node_modules/minimatch/package.json create mode 100644 patcher/vendor/asar/node_modules/minipass/LICENSE.md create mode 100644 patcher/vendor/asar/node_modules/minipass/README.md create mode 100644 patcher/vendor/asar/node_modules/minipass/package.json create mode 100644 patcher/vendor/asar/node_modules/path-scurry/LICENSE.md create mode 100644 patcher/vendor/asar/node_modules/path-scurry/README.md create mode 100644 patcher/vendor/asar/node_modules/path-scurry/package.json create mode 100644 patcher/vendor/asar/package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ccaf71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# PyInstaller build outputs +build/ +dist/ +*.manifest + +# Node / npm +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +!patcher/vendor/asar/node_modules/ + +# VitePress build outputs & cache +docs/.vitepress/dist/ +docs/.vitepress/cache/ + +# User data, backups, logs & temp files +*.vanilla +*.bak +*.log +*.tmp +repatch.log +watcher.log +temp* + +# OS and Editor files +.DS_Store +Thumbs.db +desktop.ini +.vscode/ +.idea/ +*.swp +*.swo diff --git a/BetterCurseForge.spec b/BetterCurseForge.spec new file mode 100644 index 0000000..fde9750 --- /dev/null +++ b/BetterCurseForge.spec @@ -0,0 +1,38 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['patcher\\patcher_gui.py'], + pathex=[], + binaries=[], + datas=[('bettercf', 'bettercf'), ('patcher\\vendor', 'vendor'), ('patcher\\patch_core.py', '.')], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='BetterCurseForge', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a5ceca5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 BetterCurseForge Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b13aa5 --- /dev/null +++ b/README.md @@ -0,0 +1,165 @@ +# BetterCurseForge + +
+ +BetterCurseForge Logo + +### The modern, lightweight client modification for CurseForge Launcher. + +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Platform](https://img.shields.io/badge/Platform-Windows-0078D6.svg?logo=windows)](https://github.com) +[![Zero Ads](https://img.shields.io/badge/Ads-Completely%20Blocked-success.svg)](#features) +[![Auto Repatch](https://img.shields.io/badge/Auto--Repatch-Autonomous-brightgreen.svg)](#-how-auto-repatch-works) + +**Zero Ads • Custom Themes • Unlocked Skins • Extensible Plugins • Autonomous Auto-Repatch** + +
+ +--- + +## ✨ Features + +- 🚫 **Complete Zero-Ad Experience**: Multi-layer ad elimination: + - Electron `session.webRequest` network request blocker stops Overwolf/Adrta/DoubleClick ad calls before they start. + - React component interception replaces ad renderers with clean empty nodes. + - Fail-safe CSS rules neutralize ad frames, banners, and swap buttons. +- 🎨 **First-Class Theme Engine**: + - Seamlessly embedded into CurseForge's native **Settings > Themes** screen. + - Live CSS injection with instant hot-reloading (no launcher restarts needed). + - Bundled with **Purple Night**—a custom dark theme featuring violet accents, sleek card elevations, and custom badge styling. +- 🔓 **Unlocked Native Premium Themes & Skins**: + - Unlocks all official CurseForge subscriber skins (PixelForge, LifeForge, FantasyForge, Halloween, etc.) without requiring a CurseForge subscription. +- 🧩 **Extensible Plugin System**: + - Modular plugin architecture with lifecycle hooks (`onEnable`, `onDisable`), safe IPC communication, and scoped styling. + - Includes sample plugins like **Compact Mode**. +- 🛡️ **Autonomous Auto-Repatch Engine**: + - **Survives in-app CurseForge updates automatically!** + - Offline, zero-delay ASAR extraction and repacking (< 3 seconds) using CurseForge's internal Node v24 runtime—no internet connection or external npm/npx required. + - Synchronizes Overwolf's binary `OWEASARSIG` integrity hashes directly inside `CurseForge.exe`. + - Background watcher daemon ensures your modifications are immediately re-applied when CurseForge updates. + +--- + +## 🚀 Quick Start + +### Easy Installation (GUI) + +1. Download the latest `BetterCurseForge.exe` from [Releases](https://github.com). +2. Run `BetterCurseForge.exe`. +3. Click **Patch CurseForge**. +4. Launch CurseForge Launcher—your modded client is ready to use! + +### Command-Line Interface (CLI) + +`BetterCurseForge.exe` can also be run directly from PowerShell or Command Prompt: + +```bash +# Check current patch and watcher status +.\BetterCurseForge.exe status + +# Apply BetterCurseForge patch +.\BetterCurseForge.exe patch + +# Restore original vanilla CurseForge +.\BetterCurseForge.exe unpatch +``` + +--- + +## 🔄 How Auto-Repatch Works + +Whenever CurseForge downloads an update, the Overwolf installer replaces `resources/app.asar` with a vanilla archive and restarts `CurseForge.exe`. BetterCurseForge guarantees you never lose your patch: + +```mermaid +flowchart TD + A[CurseForge Launcher Update] -->|Installer downloads update| B[CurseForge Quitting / Replacing Files] + B -->|Hook & Watcher Detect Update| C[Auto-Repatch Triggered] + C -->|Wait for Installer to Settle| D[Settle Verified] + D -->|Offline Extraction & Patch <3s| E[Dist & BetterCF Injected] + E -->|Recalculate ASAR SHA-256| F[Synchronize OWEASARSIG in CurseForge.exe] + F -->|Autonomous Relaunch| G[Modded CurseForge Launcher Ready!] +``` + +1. **Permanent Daemon Installation**: On first patch, BetterCurseForge installs to `%LOCALAPPDATA%\BetterCurseForge` with an autostart registry entry in `HKCU\...\Run`. +2. **Proactive In-App Update Interceptor**: BetterCurseForge monitors updater states on app quit (`before-quit`) to initiate patching before the new launcher process initializes. +3. **Resilient Win32 Watcher Daemon**: A silent background process monitors CurseForge's installation directory with 0% CPU consumption. If an unpatched state is detected (e.g., after an update), it terminates the unpatched process, reapplies the patch, and relaunches the modded client. +4. **100% Offline Zero-Delay ASAR Engine**: Unpacking and repacking use bundled tools combined with CurseForge's built-in Node runtime (`ELECTRON_RUN_AS_NODE=1`), completing the entire repatch cycle in 3–4 seconds without requiring an internet connection. + +--- + +## 🎨 Themes & Plugins + +User configurations, themes, and plugins are stored in your AppData directory: +``` +%APPDATA%\CurseForge\BetterCurseForge\ +├── themes/ +│ ├── purple-night.css +│ └── purple-night.json +└── plugins/ + └── compact-mode/ + ├── manifest.json + └── index.js +``` + +You can open these folders anytime via the **Open Themes Folder** and **Open Plugins Folder** buttons in the BetterCurseForge GUI. + +### Creating a Custom Theme + +Create a CSS file and manifest in `%APPDATA%\CurseForge\BetterCurseForge\themes\my-theme.css`: + +```json +{ + "name": "My Theme", + "description": "Custom color theme for CurseForge", + "author": "Your Name", + "version": "1.0.0", + "css": "my-theme.css" +} +``` + +```css +:root { + --surface-brand-brand: #10b981 !important; + --surface-brand-brand-hover: #059669 !important; +} +``` + +--- + +## 🛠️ Building from Source + +### Prerequisites + +- Windows 10 / 11 +- Python 3.10+ +- Node.js 20+ (for docs) + +### Build Instructions + +```powershell +# Clone the repository +git clone https://github.com/your-username/BetterCurseForge.git +cd BetterCurseForge + +# Build standalone executable with PyInstaller +python -m pip install pyinstaller +python -m PyInstaller BetterCurseForge.spec --noconfirm + +# Output executable will be in: +# dist/BetterCurseForge.exe +``` + +### Running VitePress Documentation + +```powershell +cd docs +npm install +npm run dev +``` + +--- + +## 📄 License + +This project is licensed under the [MIT License](LICENSE). +CurseForge and Overwolf are trademarks of their respective owners. BetterCurseForge is an independent open-source project and is not affiliated with or endorsed by Overwolf or CurseForge. diff --git a/bettercf/core/main-process.js b/bettercf/core/main-process.js new file mode 100644 index 0000000..54f3198 --- /dev/null +++ b/bettercf/core/main-process.js @@ -0,0 +1,310 @@ +// 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); +} diff --git a/bettercf/core/preload.js b/bettercf/core/preload.js new file mode 100644 index 0000000..aaba18f --- /dev/null +++ b/bettercf/core/preload.js @@ -0,0 +1,79 @@ +// BetterCurseForge — Preload Script +// ================================== +// Injected into every BrowserWindow's preload phase. +// Chain-loads the original CurseForge preload, then injects the renderer runtime. +// Since contextIsolation is false, we have direct access to the renderer's JS context. + +const { ipcRenderer } = require('electron'); +const path = require('path'); +const fs = require('fs'); + +// ── Find and load original preload ──────────────────────────── + +const args = process.argv; +let originalPreloadPath = null; + +for (const arg of args) { + if (arg.startsWith('--bcf-original-preload=')) { + originalPreloadPath = arg.split('=')[1]; + break; + } +} + +if (originalPreloadPath && fs.existsSync(originalPreloadPath)) { + try { + require(originalPreloadPath); + console.log('[BetterCF] Original preload loaded:', originalPreloadPath); + } catch (err) { + console.error('[BetterCF] Failed to load original preload:', err); + } +} + +// ── Inject Renderer Runtime ─────────────────────────────────── + +const bcfInjected = args.some(a => a === '--bcf-injected=true'); + +if (bcfInjected) { + // Wait for DOM to be ready, then inject our renderer + const injectRenderer = () => { + const bcfCorePath = path.join(__dirname, 'bettercf', 'core'); + const rendererPath = path.join(bcfCorePath, 'renderer.js'); + + if (!fs.existsSync(rendererPath)) { + console.warn('[BetterCF] renderer.js not found at', rendererPath); + return; + } + + // Read renderer code and inject via inline script + // CSP allows 'unsafe-inline' so this works + const rendererCode = fs.readFileSync(rendererPath, 'utf-8'); + + // Create the BetterCurseForge API on the window object + // This needs to bridge to the main process via IPC + const apiSetup = ` + window.__BCF_IPC__ = { + invoke: (...args) => require('electron').ipcRenderer.invoke(...args), + }; + window.__BCF_PRELOAD_PATH__ = ${JSON.stringify(bcfCorePath)}; + `; + + // Inject API setup immediately + const apiScript = document.createElement('script'); + apiScript.textContent = apiSetup; + (document.head || document.documentElement).appendChild(apiScript); + + // Inject renderer after a short delay to ensure DOM is ready + const rendererScript = document.createElement('script'); + rendererScript.type = 'module'; + rendererScript.textContent = rendererCode; + (document.head || document.documentElement).appendChild(rendererScript); + + console.log('[BetterCF] Renderer injected.'); + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', injectRenderer); + } else { + injectRenderer(); + } +} diff --git a/bettercf/core/renderer.js b/bettercf/core/renderer.js new file mode 100644 index 0000000..88e7bf5 --- /dev/null +++ b/bettercf/core/renderer.js @@ -0,0 +1,933 @@ +// BetterCurseForge — Renderer Runtime +// ==================================== +// Runs in the renderer process (the CurseForge UI). +// Handles ad removal, client themes in Settings -> Themes, plugins, and public API. + +(function () { + 'use strict'; + + if (window.__BCF_RENDERER_LOADED__) { + console.log('[BetterCF] Renderer runtime already loaded.'); + return; + } + window.__BCF_RENDERER_LOADED__ = true; + + // Fallback IPC bridge if not set by preload + if (!window.__BCF_IPC__) { + try { + const { ipcRenderer } = require('electron'); + window.__BCF_IPC__ = { + invoke: (...args) => ipcRenderer.invoke(...args), + }; + } catch {} + } + const ipc = window.__BCF_IPC__; + + // ═══════════════════════════════════════════════════════════════ + // AD REMOVAL + // ═══════════════════════════════════════════════════════════════ + + const AdBlocker = { + _observer: null, + _style: null, + + init() { + this._style = document.createElement('style'); + this._style.id = 'bcf-adblock'; + this._style.textContent = ` + /* ═══ BetterCurseForge Ad Blocker ═══ */ + .curseforge-ad, + .ad-inner, + .overwolf-ad, + .affiliate-banner, + .swap-ads-location, + .subscribe-link, + .subscribe-link-small-resolution, + .subscribe-link-large-resolution, + .promo-banner { + display: none !important; + width: 0 !important; + height: 0 !important; + min-width: 0 !important; + max-width: 0 !important; + overflow: hidden !important; + pointer-events: none !important; + position: absolute !important; + visibility: hidden !important; + } + + owadview { + display: none !important; + width: 0 !important; + height: 0 !important; + position: absolute !important; + visibility: hidden !important; + } + + [class*="ad-container"], + [class*="ad-banner"], + [class*="high-impact-ad"], + [data-ad], + [data-ad-slot] { + display: none !important; + } + + [style*="grid-template"][style*="ads"] { + grid-template-columns: 1fr !important; + grid-template-areas: none !important; + } + `; + (document.head || document.documentElement).appendChild(this._style); + + this._observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + if (node.nodeType !== Node.ELEMENT_NODE) continue; + this._removeAds(node); + } + } + }); + + this._observer.observe(document.documentElement, { + childList: true, + subtree: true, + }); + + setTimeout(() => this._sweepAds(), 1000); + setTimeout(() => this._sweepAds(), 3000); + setTimeout(() => this._sweepAds(), 6000); + }, + + _removeAds(node) { + if (node.tagName?.toLowerCase() === 'owadview') { + const parent = node.parentElement; + node.remove(); + if (parent && parent.children.length === 0) { + parent.style.display = 'none'; + } + return; + } + + const classes = node.className?.toString() || ''; + if (/\b(ad-container|ad-banner|ads-wrapper|AdView|high-impact|display-ad)\b/i.test(classes)) { + node.remove(); + return; + } + + const adChildren = node.querySelectorAll?.('owadview, [class*="ad-container"], [class*="AdView"], [class*="high-impact"]'); + if (adChildren) { + for (const child of adChildren) { + child.remove(); + } + } + }, + + _sweepAds() { + const adSelectors = [ + '.curseforge-ad', + '.ad-inner', + '.overwolf-ad', + '.affiliate-banner', + '.subscribe-link', + '.subscribe-link-small-resolution', + '.subscribe-link-large-resolution', + '.promo-banner', + 'owadview', + ]; + const els = document.querySelectorAll(adSelectors.join(', ')); + for (const el of els) { + el.remove(); + } + + const walker = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT, null); + while (walker.nextNode()) { + const text = walker.currentNode.textContent?.trim(); + if (text && /^Ads support authors/i.test(text)) { + let parent = walker.currentNode.parentElement; + while (parent && parent !== document.body) { + if (parent.children.length <= 2) { + parent.style.display = 'none'; + break; + } + parent = parent.parentElement; + } + } + } + }, + }; + + + // ═══════════════════════════════════════════════════════════════ + // THEME SYSTEM + // ═══════════════════════════════════════════════════════════════ + + const ThemeManager = { + _themes: [], + _activeThemeFileName: null, + _savedThemeFileName: null, + _styleElement: null, + _isPreviewing: false, + _isSuspendedForNative: false, + + async init() { + this._injectBaseStyles(); + + // Read stored active theme preference + this._savedThemeFileName = localStorage.getItem('bcf_active_custom_theme') || null; + + if (ipc) { + try { + const s = await ipc.invoke('bcf:settings:get'); + if (s && s.activeCustomTheme !== undefined) { + this._savedThemeFileName = s.activeCustomTheme; + } + } catch {} + } + + this._activeThemeFileName = this._savedThemeFileName; + + await this.loadThemes(); + + // If active theme is found, apply it + if (this._savedThemeFileName) { + const theme = this._themes.find(t => t.fileName === this._savedThemeFileName); + if (theme) { + this.applyTheme(theme, false); + } else { + // Stored theme file not found + this._savedThemeFileName = null; + this._activeThemeFileName = null; + } + } + + console.log(`[BetterCF] Theme manager ready (${this._themes.length} theme(s) found). Active: ${this._savedThemeFileName || 'None'}`); + }, + + _injectBaseStyles() { + if (document.getElementById('bcf-custom-themes-style')) return; + const style = document.createElement('style'); + style.id = 'bcf-custom-themes-style'; + style.textContent = ` + /* ═══ BetterCurseForge Settings Themes UI ═══ */ + #bcf-custom-themes-section { + margin-top: 0 !important; + padding: 0 !important; + } + .bcf-themes-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + } + .bcf-heading { + display: flex; + align-items: center; + gap: 10px; + margin: 0; + font-size: 18px; + font-weight: 700; + line-height: 24px; + color: var(--color-text-title, currentColor); + } + .bcf-heading > span:first-child { + display: inline-flex; + align-items: center; + } + .bcf-tag { + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 600; + line-height: 1; + color: #a78bfa; + background: rgba(139, 92, 246, 0.12); + border: 1px solid rgba(139, 92, 246, 0.3); + padding: 3px 8px; + border-radius: 10px; + letter-spacing: 0.3px; + } + .bcf-actions { + display: flex; + gap: 8px; + align-items: center; + } + .bcf-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + height: 28px; + font-size: 12px; + font-weight: 500; + line-height: 1; + color: var(--color-text-primary, currentColor); + background: var(--surface-on-surface, rgba(255, 255, 255, 0.08)); + border: 1px solid var(--border-tertiery, rgba(255, 255, 255, 0.15)); + border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease; + } + .bcf-btn svg { + display: block; + flex-shrink: 0; + width: 13px; + height: 13px; + } + .bcf-btn:hover { + background: var(--surface-on-surface-hover, rgba(255, 255, 255, 0.16)); + border-color: rgba(255, 255, 255, 0.3); + color: var(--color-text-title-hover, #ffffff); + } + .bcf-btn:active { + transform: scale(0.97); + } + .bcf-theme-thumb { + border-radius: 0; + } + .bcf-theme-thumb .thumb-icon svg { + display: block; + } + .bcf-theme-thumb.is-active .sprite-icon { + opacity: 1 !important; + color: #ffffff !important; + } + + /* ═══ Mutual Exclusivity ═══ */ + /* When a BetterCF client theme is active, hide active checkmark & active indicator on native theme cards */ + html.bcf-custom-theme-active .settings-themes-view .theme-thumb:not(.bcf-theme-thumb).is-active .sprite-icon { + opacity: 0 !important; + } + html.bcf-custom-theme-active .settings-themes-view .theme-thumb:not(.bcf-theme-thumb).is-active .thumb-icon { + background-color: transparent !important; + } + html.bcf-custom-theme-active .settings-themes-view .theme-thumb:not(.bcf-theme-thumb).is-active { + outline: none !important; + border-color: transparent !important; + box-shadow: none !important; + } + /* Neutralize native skin styles and title bar */ + html.bcf-custom-theme-active .title-bar, + html.bcf-custom-theme-active [class*="title-bar"], + html.bcf-custom-theme-active [class*="TitleBar"] { + background: #0d0818 !important; + --title-bar-background: #0d0818 !important; + } + html.bcf-custom-theme-active .pixelforge-skin, + html.bcf-custom-theme-active .lifeforge-skin, + html.bcf-custom-theme-active .fantasyforge-skin, + html.bcf-custom-theme-active .halloween-skin, + html.bcf-custom-theme-active .cf-light, + html.bcf-custom-theme-active .cf-light-colorblind { + --title-bar-background: #0d0818 !important; + --surface-brand-brand: #8b5cf6 !important; + --surface-brand-brand-hover: #9d74f7 !important; + --accent-font: var(--body-font, "Lato", sans-serif) !important; + background-color: transparent !important; + } + html.bcf-custom-theme-active .pixelforge-skin :not(.highlighted-project-container) .card:after { + display: none !important; + } + `; + (document.head || document.documentElement).appendChild(style); + }, + + async loadThemes() { + if (!ipc) return []; + try { + this._themes = await ipc.invoke('bcf:themes:list'); + } catch (err) { + console.error('[BetterCF] Failed to load themes:', err); + this._themes = []; + } + return this._themes; + }, + + applyTheme(theme, save = true) { + if (!theme || !theme.css) return; + + if (!this._styleElement) { + this._styleElement = document.createElement('style'); + this._styleElement.id = 'bcf-custom-theme'; + (document.head || document.documentElement).appendChild(this._styleElement); + } + + this._styleElement.disabled = false; + this._styleElement.textContent = theme.css; + this._styleElement.dataset.bcfTheme = theme.fileName; + this._activeThemeFileName = theme.fileName; + document.documentElement.classList.add('bcf-custom-theme-active'); + + const ALL_SKIN_CLASSES = [ + 'cf-light', + 'cf-light-colorblind', + 'pixelforge-skin', + 'lifeforge-skin', + 'fantasyforge-skin', + 'halloween-skin', + ]; + const app = document.querySelector('.cf-app'); + if (app) { + app.classList.remove(...ALL_SKIN_CLASSES); + if (!app.classList.contains('cf-classic')) { + app.classList.add('cf-classic'); + } + } + + if (save) { + this._savedThemeFileName = theme.fileName; + this._isPreviewing = false; + try { + localStorage.setItem('bcf_active_custom_theme', theme.fileName); + ipc?.invoke?.('bcf:settings:set', 'activeCustomTheme', theme.fileName); + } catch {} + } + + console.log(`[BetterCF] Applied custom theme: ${theme.name} (${theme.fileName}) [save=${save}]`); + }, + + removeTheme(save = true) { + if (this._styleElement) { + this._styleElement.remove(); + this._styleElement = null; + } + this._activeThemeFileName = null; + document.documentElement.classList.remove('bcf-custom-theme-active'); + + if (save) { + this._savedThemeFileName = null; + this._isPreviewing = false; + try { + localStorage.removeItem('bcf_active_custom_theme'); + ipc?.invoke?.('bcf:settings:set', 'activeCustomTheme', null); + } catch {} + } + + console.log(`[BetterCF] Reverted to native theme [save=${save}].`); + }, + + previewTheme(theme) { + if (!theme || !theme.css) return; + if (this._savedThemeFileName === theme.fileName) return; + this._isPreviewing = true; + this.applyTheme(theme, false); + }, + + cancelPreview() { + if (!this._isPreviewing) return; + this._isPreviewing = false; + if (this._savedThemeFileName) { + const saved = this._themes.find(t => t.fileName === this._savedThemeFileName); + if (saved) { + this.applyTheme(saved, false); + return; + } + } + this.removeTheme(false); + }, + + suspendForNativePreview() { + if (this._isSuspendedForNative) return; + if (this._savedThemeFileName || this._activeThemeFileName) { + this._isSuspendedForNative = true; + if (this._styleElement) { + this._styleElement.disabled = true; + } + document.documentElement.classList.remove('bcf-custom-theme-active'); + } + }, + + resumeFromNativePreview() { + if (!this._isSuspendedForNative) return; + this._isSuspendedForNative = false; + if (this._savedThemeFileName) { + if (this._styleElement) { + this._styleElement.disabled = false; + } + document.documentElement.classList.add('bcf-custom-theme-active'); + } + }, + + getActiveThemeName() { + return this._savedThemeFileName; + }, + + getThemes() { + return [...this._themes]; + }, + }; + + + // ═══════════════════════════════════════════════════════════════ + // NATIVE SETTINGS -> THEMES HOOK & MUTUAL EXCLUSIVITY + // ═══════════════════════════════════════════════════════════════ + + let _isSwitchingThemeInternally = false; + + function setupSettingsThemesHook() { + let isInjecting = false; + + // Listen for clicks on native theme cards to ensure mutual exclusivity: + // If user clicks a native theme card, deactivate any client theme. + document.addEventListener('click', (e) => { + if (_isSwitchingThemeInternally) return; + const nativeThumb = e.target.closest('.theme-thumb:not(.bcf-theme-thumb)'); + if (nativeThumb) { + ThemeManager._isSuspendedForNative = false; + if (ThemeManager.getActiveThemeName()) { + ThemeManager.removeTheme(true); + const grid = document.querySelector('#bcf-custom-themes-grid'); + if (grid) { + grid.querySelectorAll('.bcf-theme-thumb').forEach(c => c.classList.remove('is-active')); + } + } + } + }, true); + + // When hovering over native theme cards, suspend client theme so native hover preview works! + document.addEventListener('mouseover', (e) => { + const nativeThumb = e.target.closest('.theme-thumb:not(.bcf-theme-thumb)'); + if (nativeThumb) { + ThemeManager.suspendForNativePreview(); + } + }, true); + + document.addEventListener('mouseout', (e) => { + const nativeThumb = e.target.closest('.theme-thumb:not(.bcf-theme-thumb)'); + if (nativeThumb) { + const toThumb = e.relatedTarget?.closest?.('.theme-thumb:not(.bcf-theme-thumb)'); + if (!toThumb) { + ThemeManager.resumeFromNativePreview(); + } + } + }, true); + + const checkAndInject = async () => { + const view = document.querySelector('.settings-themes-view'); + if (!view) return; + + if (view.querySelector('#bcf-custom-themes-section')) { + return; + } + + if (isInjecting) return; + isInjecting = true; + + try { + await renderCustomThemesSection(view); + } finally { + isInjecting = false; + } + }; + + // Initial check + checkAndInject(); + + // Observe DOM mutations for when the user navigates to Settings -> Themes + const observer = new MutationObserver(() => { + const view = document.querySelector('.settings-themes-view'); + if (view && !view.querySelector('#bcf-custom-themes-section')) { + checkAndInject(); + } else if (!view) { + ThemeManager.cancelPreview(); + ThemeManager.resumeFromNativePreview(); + } + }); + + observer.observe(document.documentElement, { + childList: true, + subtree: true, + }); + + window.addEventListener('hashchange', () => setTimeout(checkAndInject, 100)); + window.addEventListener('popstate', () => setTimeout(checkAndInject, 100)); + } + + function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str || ''; + return div.innerHTML; + } + + async function renderCustomThemesSection(view) { + if (view.querySelector('#bcf-custom-themes-section')) return; + + const section = document.createElement('div'); + section.className = 'settings-item-row'; + section.id = 'bcf-custom-themes-section'; + + section.innerHTML = ` +
+
+

+ Client Themes + BetterCurseForge +

+
+ + +
+
+
+ `; + + // Insert section right after existing settings-item-row or before upsell + const rows = view.querySelectorAll('.settings-item-row:not(#bcf-custom-themes-section)'); + if (rows.length > 0) { + const lastRow = rows[rows.length - 1]; + lastRow.after(section); + } else { + const upsell = view.querySelector('.upsell') || view.querySelector('[class*="upsell"]'); + if (upsell) { + view.insertBefore(section, upsell); + } else { + view.appendChild(section); + } + } + + // Connect buttons + section.querySelector('#bcf-open-folder-btn')?.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + ipc?.invoke?.('bcf:open-themes-folder'); + }); + + section.querySelector('#bcf-refresh-themes-btn')?.addEventListener('click', async (e) => { + e.preventDefault(); + e.stopPropagation(); + const btn = section.querySelector('#bcf-refresh-themes-btn'); + if (btn) btn.style.opacity = '0.5'; + await ThemeManager.loadThemes(); + populateThemeCards(section); + if (btn) btn.style.opacity = '1'; + }); + + populateThemeCards(section); + } + + function populateThemeCards(section) { + const grid = section.querySelector('#bcf-custom-themes-grid'); + if (!grid) return; + + grid.innerHTML = ''; + const themes = ThemeManager.getThemes(); + const activeFileName = ThemeManager.getActiveThemeName(); + + if (themes.length === 0) { + const empty = document.createElement('div'); + empty.className = 'bcf-empty-msg'; + empty.style.cssText = 'color: var(--color-text-secondary, #71717a); font-size: 13px; padding: 10px 0;'; + empty.textContent = 'No custom themes found in themes folder. Click "Open Folder" to add .css themes.'; + grid.appendChild(empty); + return; + } + + for (const theme of themes) { + const isThisActive = activeFileName === theme.fileName; + const card = document.createElement('div'); + card.className = `theme-thumb bcf-theme-thumb ${isThisActive ? 'is-active' : ''}`; + card.dataset.themeFile = theme.fileName; + + const primary = theme.primaryColor || '#8b5cf6'; + const title = theme.titleColor || '#a78bfa'; + const body = theme.bodyColor || '#2e1065'; + const bg = theme.backgroundColor || '#161026'; + + card.style.setProperty('--color-thumb-primary', primary); + card.style.setProperty('--color-thumb-title', title); + card.style.setProperty('--color-thumb-body', body); + card.style.setProperty('--color-thumb-name', '#f1f5f9'); + card.style.setProperty('--color-thumb-icon-bg', primary); + card.style.setProperty('--color-thumb-check-color', '#ffffff'); + card.style.backgroundColor = bg; + + card.innerHTML = ` +
+
+
+
${escapeHtml(theme.name)}
+
+ +
+ `; + + card.addEventListener('mouseenter', () => { + ThemeManager.previewTheme(theme); + }); + + card.addEventListener('mouseleave', () => { + ThemeManager.cancelPreview(); + }); + + card.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + + if (ThemeManager.getActiveThemeName() === theme.fileName) { + // Toggle off -> revert to native theme + ThemeManager.removeTheme(true); + card.classList.remove('is-active'); + } else { + // Activate this client theme + _isSwitchingThemeInternally = true; + try { + // Ensure CurseForge native theme switches to Classic Dark baseline so no other skin remains active + const classicCard = [...document.querySelectorAll('.settings-themes-view .theme-thumb:not(.bcf-theme-thumb)')] + .find(c => { + const txt = c.querySelector('.name')?.textContent?.trim().toLowerCase() || ''; + return txt === 'curseforge classic' || (txt.includes('classic') && !txt.includes('colorblind')); + }); + if (classicCard && !classicCard.classList.contains('is-active')) { + classicCard.click(); + } + } catch (err) { + console.warn('[BetterCF] Error switching base theme:', err); + } finally { + _isSwitchingThemeInternally = false; + } + + ThemeManager.applyTheme(theme, true); + grid.querySelectorAll('.bcf-theme-thumb').forEach(c => { + c.classList.toggle('is-active', c.dataset.themeFile === theme.fileName); + }); + } + }); + + grid.appendChild(card); + } + } + + + // ═══════════════════════════════════════════════════════════════ + // PLUGIN SYSTEM + // ═══════════════════════════════════════════════════════════════ + + const PluginManager = { + _plugins: [], + _instances: new Map(), + _styleElements: new Map(), + + async init() { + if (!ipc) return; + try { + this._plugins = await ipc.invoke('bcf:plugins:list'); + for (const plugin of this._plugins) { + if (plugin.enabled) { + this.enablePlugin(plugin); + } + } + console.log(`[BetterCF] Plugin manager ready. ${this._plugins.length} plugin(s) found.`); + } catch (e) { + console.error('[BetterCF] Failed to initialize plugins:', e); + } + }, + + enablePlugin(plugin) { + if (this._instances.has(plugin.dirName)) return; + + try { + if (plugin.styles) { + const style = document.createElement('style'); + style.id = `bcf-plugin-style-${plugin.dirName}`; + style.textContent = plugin.styles; + (document.head || document.documentElement).appendChild(style); + this._styleElements.set(plugin.dirName, style); + } + + if (plugin.script) { + const instance = { enabled: true }; + const pluginFn = new Function('BetterCurseForge', 'plugin', plugin.script); + pluginFn(window.BetterCurseForge, instance); + this._instances.set(plugin.dirName, instance); + console.log(`[BetterCF] Plugin loaded: ${plugin.name}`); + } + } catch (err) { + console.error(`[BetterCF] Failed to load plugin ${plugin.name}:`, err); + } + }, + + disablePlugin(plugin) { + const instance = this._instances.get(plugin.dirName); + if (instance?.onDisable) { + try { + instance.onDisable(); + } catch (err) { + console.error(`[BetterCF] Error disabling plugin ${plugin.name}:`, err); + } + } + this._instances.delete(plugin.dirName); + + const style = this._styleElements.get(plugin.dirName); + if (style) { + style.remove(); + this._styleElements.delete(plugin.dirName); + } + }, + + async togglePlugin(dirName) { + if (!ipc) return false; + const enabled = await ipc.invoke('bcf:settings:toggle-plugin', dirName); + const plugin = this._plugins.find(p => p.dirName === dirName); + if (!plugin) return false; + + plugin.enabled = enabled; + if (enabled) { + this.enablePlugin(plugin); + } else { + this.disablePlugin(plugin); + } + return enabled; + }, + + getPlugins() { + return [...this._plugins]; + }, + }; + + + // ═══════════════════════════════════════════════════════════════ + // PUBLIC API — window.BetterCurseForge + // ═══════════════════════════════════════════════════════════════ + + const _injectedCSS = new Map(); + let _cssCounter = 0; + const _eventHandlers = {}; + + window.BetterCurseForge = { + version: '1.0.0', + + injectCSS(css) { + const id = `bcf-injected-${_cssCounter++}`; + const style = document.createElement('style'); + style.id = id; + style.textContent = css; + (document.head || document.documentElement).appendChild(style); + _injectedCSS.set(id, style); + + return () => { + style.remove(); + _injectedCSS.delete(id); + }; + }, + + settings: { + async get(key) { + if (!ipc) return null; + const all = await ipc.invoke('bcf:settings:get'); + return key ? all[key] : all; + }, + async set(key, value) { + if (!ipc) return false; + return ipc.invoke('bcf:settings:set', key, value); + }, + }, + + patcher: { + _patches: [], + + before(obj, method, callback) { + const original = obj[method]; + obj[method] = function (...args) { + callback(args); + return original.apply(this, args); + }; + const unpatch = () => { obj[method] = original; }; + this._patches.push(unpatch); + return unpatch; + }, + + after(obj, method, callback) { + const original = obj[method]; + obj[method] = function (...args) { + const result = original.apply(this, args); + callback(args, result); + return result; + }; + const unpatch = () => { obj[method] = original; }; + this._patches.push(unpatch); + return unpatch; + }, + + instead(obj, method, callback) { + const original = obj[method]; + obj[method] = function (...args) { + return callback(args, original.bind(this)); + }; + const unpatch = () => { obj[method] = original; }; + this._patches.push(unpatch); + return unpatch; + }, + }, + + on(event, callback) { + if (!_eventHandlers[event]) _eventHandlers[event] = []; + _eventHandlers[event].push(callback); + }, + + off(event, callback) { + if (!_eventHandlers[event]) return; + _eventHandlers[event] = _eventHandlers[event].filter(cb => cb !== callback); + }, + + emit(event, ...args) { + if (!_eventHandlers[event]) return; + for (const cb of _eventHandlers[event]) { + try { cb(...args); } catch (err) { + console.error(`[BetterCF] Event handler error (${event}):`, err); + } + } + }, + + themes: ThemeManager, + plugins: PluginManager, + ads: AdBlocker, + }; + + + // ═══════════════════════════════════════════════════════════════ + // INITIALIZATION + // ═══════════════════════════════════════════════════════════════ + + async function init() { + console.log('[BetterCF] Initializing renderer runtime...'); + + // 1. Block ads + AdBlocker.init(); + + // 2. Initialize theme manager + await ThemeManager.init(); + + // 3. Setup Native Settings -> Themes Hook + setupSettingsThemesHook(); + + // 4. Initialize plugins + await PluginManager.init(); + + // 5. Emit ready + window.BetterCurseForge.emit('ready'); + + console.log('[BetterCF] ✓ BetterCurseForge loaded successfully!'); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => setTimeout(init, 300)); + } else { + setTimeout(init, 300); + } +})(); diff --git a/bettercf/plugins/compact-mode/index.js b/bettercf/plugins/compact-mode/index.js new file mode 100644 index 0000000..debcef0 --- /dev/null +++ b/bettercf/plugins/compact-mode/index.js @@ -0,0 +1,65 @@ +// Compact Mode Plugin for BetterCurseForge +// Reduces padding and spacing for a denser UI layout + +const removeCSS = BetterCurseForge.injectCSS(` + /* Reduce card padding */ + [class*="card"], + [class*="Card"] { + padding: 8px !important; + margin-bottom: 6px !important; + } + + /* Reduce list item spacing */ + [class*="list-item"], + [class*="ListItem"], + [class*="mod-row"], + [class*="ModRow"] { + padding: 6px 12px !important; + min-height: auto !important; + } + + /* Reduce header sizes */ + [class*="section-header"], + [class*="SectionHeader"] { + padding: 8px 16px !important; + font-size: 14px !important; + } + + /* Reduce sidebar item padding */ + [class*="sidebar"] [class*="item"], + [class*="Sidebar"] [class*="Item"], + nav a, nav button { + padding: 6px 12px !important; + } + + /* Reduce gap between elements */ + [class*="grid"], + [class*="Grid"] { + gap: 8px !important; + } + + /* Smaller mod thumbnails */ + [class*="thumbnail"], + [class*="Thumbnail"], + [class*="mod-icon"], + [class*="ModIcon"] { + width: 40px !important; + height: 40px !important; + } + + /* Reduce page padding */ + [class*="page-content"], + [class*="PageContent"], + [class*="content-area"], + [class*="ContentArea"] { + padding: 12px !important; + } +`); + +// Plugin lifecycle +plugin.onDisable = () => { + removeCSS(); + console.log('[Compact Mode] Disabled.'); +}; + +console.log('[Compact Mode] Enabled — UI is now more compact.'); diff --git a/bettercf/plugins/compact-mode/manifest.json b/bettercf/plugins/compact-mode/manifest.json new file mode 100644 index 0000000..882367b --- /dev/null +++ b/bettercf/plugins/compact-mode/manifest.json @@ -0,0 +1,6 @@ +{ + "name": "Compact Mode", + "author": "BetterCF Team", + "description": "Reduces padding and spacing throughout the UI for a more compact layout", + "version": "1.0.0" +} diff --git a/bettercf/themes/purple-night.css b/bettercf/themes/purple-night.css new file mode 100644 index 0000000..eea8b97 --- /dev/null +++ b/bettercf/themes/purple-night.css @@ -0,0 +1,215 @@ +/* BetterCurseForge — Purple Night Theme */ +/* A rich, sleek, fully-integrated dark purple theme for CurseForge Launcher */ + +:root, +.cf-app, +.base-skin, +.dark-skin, +.cf-classic, +.theme-purple-night { + /* ── Accent & Brand ── */ + --surface-brand-brand: #8b5cf6 !important; + --surface-brand-brand-hover: #9d74f7 !important; + --color-primary: #8b5cf6 !important; + --color-primary-hover: #9d74f7 !important; + --color-primary-active: #7c3aed !important; + --color-accent: #a78bfa !important; + --color-highlight: #8b5cf6 !important; + --border-primary: #8b5cf6 !important; + --border-highlight: #8b5cf6 !important; + --selected-item-border: 3px solid #8b5cf6 !important; + + /* ── Backgrounds & Surfaces ── */ + --color-background: #0d0818 !important; + --surface-base: #07040d !important; + --surface-base-half: rgba(7, 4, 13, 0.5) !important; + --surface-primary: #110b20 !important; + --surface-secondary: #170e2b !important; + --surface-tertiery: #21153d !important; + --surface-tertiery-hover: #2c1c52 !important; + --surface-quartinery: #382468 !important; + --surface-on-surface: rgba(255, 255, 255, 0.08) !important; + --surface-on-surface-hover: rgba(255, 255, 255, 0.14) !important; + + /* ── Cards (Modpack / Project) ── */ + --surface-card: #170e2b !important; + --surface-card-hover: #221540 !important; + --color-surface-card: #170e2b !important; + --color-surface-card-hover: #221540 !important; + --color-surface-card-active: #2b1b50 !important; + --color-surface-card-active-hover: #352163 !important; + --color-surface-card-disabled: #100a1e !important; + --color-surface-info-block: #21153d !important; + --color-surface-folder-view-item: #21153d !important; + + /* ── Tables & Lists ── */ + --color-surface-table: #170e2b !important; + --color-surface-table-hover: #21153d !important; + --color-surface-table-head: #1c1134 !important; + --color-surface-table-head-hover: #261747 !important; + + /* ── Modals & Dialogs ── */ + --color-surface-modal: #170e2b !important; + --color-surface-modal-footer: #110b20 !important; + --color-modal-border: #2c1c52 !important; + --color-modal-backdrop: rgba(7, 4, 13, 0.85) !important; + --color-modal-backdrop-low: rgba(7, 4, 13, 0.5) !important; + + /* ── Title Bar ── */ + --title-bar-background: linear-gradient(90deg, #110b20 0%, #1a1032 50%, #110b20 100%) !important; + --btn-window-action: #a78bfa !important; + --btn-window-action-hover: #2c1c52 !important; + + /* ── Typography & Text ── */ + --text-pure: #ffffff !important; + --text-primary: #f1f5f9 !important; + --color-text-title: #f8fafc !important; + --color-text-title-hover: #ffffff !important; + --color-text-primary: #e2e8f0 !important; + --color-text-body: #cbd5e1 !important; + --color-text-secondary: #94a3b8 !important; + --color-text-secondary-icon: #94a3b8 !important; + --color-text-dropdown: #e2e8f0 !important; + --color-text-tooltip: #ede9fe !important; + + /* ── Buttons ── */ + --btn-primary-text: #ffffff !important; + --btn-secondary-hover: rgba(139, 92, 246, 0.16) !important; + --btn-secondary-border: rgba(139, 92, 246, 0.35) !important; + --btn-clean-hover: #21153d !important; + --btn-tertiary-background: #21153d !important; + --btn-tertiary-hover-background: #2c1c52 !important; + --btn-tertiary-text: #f1f5f9 !important; + --btn-tertiary-hover-text: #ffffff !important; + + /* ── Borders & Dividers ── */ + --border-tertiery: rgba(139, 92, 246, 0.18) !important; + --color-divider-1: #21153d !important; + --color-divider-2: #2c1c52 !important; + + /* ── Scrollbars ── */ + --color-scrollbar-thumb: #3b2266 !important; + + /* ── Theme Thumbnail Preview ── */ + --color-thumb-primary: #8b5cf6 !important; + --color-thumb-title: #3b2266 !important; + --color-thumb-body: #241442 !important; + --color-thumb-name: #ffffff !important; + --color-thumb-icon-bg: #8b5cf6 !important; + --color-thumb-check-color: #ffffff !important; +} + +/* ── Element-Specific Polish ── */ + +/* Title bar */ +.title-bar, +[class*="title-bar"], +[class*="TitleBar"] { + background: var(--title-bar-background) !important; +} + +/* Sidenav / left navigation */ +.sidenav, +[class*="sidenav"] { + background-color: var(--surface-secondary) !important; +} + +/* Sidebar game icon active indicator bar */ +.games-side-menu li a:after { + background-color: var(--surface-brand-brand) !important; +} + +/* User profile button & avatar border */ +#user-login-menu .user-profile-button:before, +.user-profile-button:before { + border-color: var(--surface-brand-brand) !important; +} +#user-login-menu .user-profile-button:hover:before, +#user-login-menu.is-open .user-profile-button:before, +.user-profile-button:hover:before { + border-color: var(--surface-brand-brand-hover) !important; +} + +/* Sidebar active tab indicator (settings, etc.) */ +.system-side-menu ul.user-menu .user-menu-item > a.tab.is-active:before, +.system-side-menu ul.user-menu .user-menu-item > button.tab.is-active:before, +.system-side-menu ul.user-menu .user-menu-item > .subscription-tooltip > a.tab.is-active:before { + border-left-color: var(--surface-brand-brand) !important; +} +.system-side-menu ul.user-menu .user-menu-item > a.tab.is-active, +.system-side-menu ul.user-menu .user-menu-item > button.tab.is-active { + color: var(--surface-brand-brand) !important; +} + +/* Active tab indicator and line in horizontal tabs */ +.tabs-menu ul .line, +.tabs-menu ul li a:before, +.tabs-menu ul li .tab-control:before { + background-color: var(--surface-brand-brand) !important; +} +.tabs-menu ul li a.is-active, +.tabs-menu ul li.is-active a { + color: var(--color-text-title, #ffffff) !important; +} + +/* Notification badges and dots */ +.highlight-indicator { + background-color: #8b5cf6 !important; + box-shadow: 0 0 6px rgba(139, 92, 246, 0.6) !important; +} +.badge-active:before, +.count-badge-wrapper .count-badge, +.popover-button button.btn-clean.btn-with-icon.has-badge:after { + background: var(--surface-brand-brand) !important; +} + +/* Primary buttons */ +.btn-primary:not(.btn-secondary), +button[class*="primary"]:not([class*="secondary"]) { + background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%) !important; + border: none !important; + color: #ffffff !important; +} + +.btn-primary:not(.btn-secondary):hover, +button[class*="primary"]:not([class*="secondary"]):hover { + background: linear-gradient(135deg, #9d74f7 0%, #8b5cf6 100%) !important; +} + + +/* Modpack card typography */ +.tile-view-item .title, +.tile-view-item .title .name-container .mod-name { + color: var(--color-text-title, #ffffff) !important; +} + +.tile-view-item .author { + color: var(--color-text-secondary, #94a3b8) !important; +} + +/* Modpack card version badge */ +.tile-view-item .art .modpack-version:before { + background-color: rgba(23, 14, 43, 0.85) !important; +} + +.tile-view-item .art .modpack-version { + border-radius: 4px; +} + +/* Selection */ +::selection { + background: rgba(139, 92, 246, 0.4) !important; + color: #ffffff !important; +} + +/* Scrollbars */ +::-webkit-scrollbar-track { + background: var(--surface-base); +} +::-webkit-scrollbar-thumb { + background: var(--color-scrollbar-thumb); + border-radius: 4px; +} +::-webkit-scrollbar-thumb:hover { + background: #52318c; +} diff --git a/bettercf/themes/purple-night.json b/bettercf/themes/purple-night.json new file mode 100644 index 0000000..76c3a24 --- /dev/null +++ b/bettercf/themes/purple-night.json @@ -0,0 +1,10 @@ +{ + "name": "Purple Night", + "author": "BetterCF Team", + "description": "A sleek purple-tinted dark theme for CurseForge", + "version": "1.0.0", + "primaryColor": "#8b5cf6", + "titleColor": "#a78bfa", + "bodyColor": "#2e1065", + "backgroundColor": "#130e20" +} diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts new file mode 100644 index 0000000..8aa787c --- /dev/null +++ b/docs/.vitepress/config.ts @@ -0,0 +1,81 @@ +import { defineConfig } from 'vitepress'; + +export default defineConfig({ + title: 'BetterCurseForge', + description: 'Client mod for CurseForge Launcher', + appearance: 'dark', + lastUpdated: true, + cleanUrls: true, + + head: [ + ['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }], + ['meta', { name: 'theme-color', content: '#9333ea' }], + ['meta', { name: 'og:type', content: 'website' }], + ['meta', { name: 'og:title', content: 'BetterCurseForge' }], + ['meta', { name: 'og:description', content: 'Client mod for CurseForge Launcher — Ad blocking, custom themes, and powerful plugin API.' }], + ], + + themeConfig: { + siteTitle: 'BetterCurseForge', + + nav: [ + { text: 'Guide', link: '/guide/installation' }, + { text: 'API', link: '/api/' }, + { text: 'GitHub', link: 'https://github.com/BetterCurseForge/BetterCurseForge' }, + ], + + sidebar: [ + { + text: 'Getting Started', + collapsed: false, + items: [ + { text: 'Installation', link: '/guide/installation' }, + ], + }, + { + text: 'Themes', + collapsed: false, + items: [ + { text: 'Custom Themes', link: '/guide/themes' }, + ], + }, + { + text: 'Plugins', + collapsed: false, + items: [ + { text: 'Plugin Development', link: '/guide/plugins' }, + ], + }, + { + text: 'API Reference', + collapsed: false, + items: [ + { text: 'Full API Reference', link: '/api/' }, + ], + }, + ], + + socialLinks: [ + { icon: 'github', link: 'https://github.com/BetterCurseForge/BetterCurseForge' }, + ], + + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2026 BetterCurseForge Contributors', + }, + + search: { + provider: 'local', + }, + + outline: { + level: [2, 3], + label: 'On this page', + }, + + docFooter: { + prev: 'Previous page', + next: 'Next page', + }, + }, +}); diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css new file mode 100644 index 0000000..92fc847 --- /dev/null +++ b/docs/.vitepress/theme/custom.css @@ -0,0 +1,38 @@ +/* BetterCurseForge Documentation Theme - Purple Accent */ +:root { + --vp-c-brand-1: #9333ea; + --vp-c-brand-2: #a855f7; + --vp-c-brand-3: #7e22ce; + --vp-c-brand-soft: rgba(168, 85, 247, 0.14); + + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #a855f7 30%, #ec4899); + --vp-home-hero-image-background-image: linear-gradient(-45deg, #9333ea 30%, #ec4899); + --vp-home-hero-image-filter: blur(44px); +} + +.dark { + --vp-c-brand-1: #a855f7; + --vp-c-brand-2: #c084fc; + --vp-c-brand-3: #9333ea; + --vp-c-brand-soft: rgba(168, 85, 247, 0.2); + + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #c084fc 25%, #f472b6); + --vp-home-hero-image-background-image: linear-gradient(-45deg, #a855f7 30%, #f472b6); + --vp-home-hero-image-filter: blur(56px); +} + +/* Custom styling enhancements */ +.custom-block.tip { + border-color: var(--vp-c-brand-1); +} + +.vp-doc a { + text-decoration-color: var(--vp-c-brand-soft); + text-underline-offset: 3px; +} + +.vp-doc a:hover { + text-decoration-color: var(--vp-c-brand-1); +} diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 0000000..f789d9f --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,6 @@ +import DefaultTheme from 'vitepress/theme'; +import './custom.css'; + +export default { + extends: DefaultTheme, +}; diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..1a75395 --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,261 @@ +# API Reference + +BetterCurseForge exposes a global runtime object on `window.BetterCurseForge` within the CurseForge Electron renderer process. This API allows themes, plugins, and developer scripts to inject styles, monkey-patch methods, manage settings, and listen to application events. + +--- + +## TypeScript Definition + +For TypeScript development and autocompletion: + +```typescript +declare global { + interface Window { + BetterCurseForge: BetterCurseForgeAPI; + } +} + +export interface BetterCurseForgeAPI { + /** Active BetterCurseForge runtime version */ + readonly version: string; + + /** Injects CSS dynamically into . Returns an unmount callback. */ + injectCSS(css: string): () => void; + + /** Persistent settings storage */ + settings: { + get(key?: string): Promise; + set(key: string, value: any): Promise; + }; + + /** Runtime method interception and monkey-patching */ + patcher: { + before(obj: any, method: string, callback: (args: any[]) => void): () => void; + after(obj: any, method: string, callback: (args: any[], result: any) => any): () => void; + instead(obj: any, method: string, callback: (args: any[], original: Function) => any): () => void; + }; + + /** Event bus */ + on(event: string, callback: (...args: any[]) => void): void; + off(event: string, callback: (...args: any[]) => void): void; + emit(event: string, ...args: any[]): void; + + /** Internal subsystem managers */ + themes: ThemeManager; + plugins: PluginManager; + ads: AdBlocker; +} +``` + +--- + +## Core API + +### `BetterCurseForge.version` + +- **Type**: `string` +- **Description**: The currently running BetterCurseForge runtime version. + +```javascript +console.log(BetterCurseForge.version); // "1.0.0" +``` + +--- + +### `BetterCurseForge.injectCSS(css)` + +Dynamically injects CSS rules into the document ``. + +- **Parameters**: + - `css` *(string)*: Raw CSS string to inject. +- **Returns**: `() => void` — Function that removes the injected `