// 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 = { _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, owadview, [class*="ad-container"], [class*="ad-banner"], [class*="AdView"], [class*="high-impact"], [data-ad], [data-ad-slot] { display: none !important; width: 0 !important; height: 0 !important; min-width: 0 !important; max-width: 0 !important; margin: 0 !important; padding: 0 !important; overflow: hidden !important; pointer-events: none !important; position: absolute !important; visibility: hidden !important; opacity: 0 !important; } [style*="grid-template"][style*="ads"] { grid-template-columns: 1fr !important; grid-template-areas: none !important; } `; (document.head || document.documentElement).appendChild(this._style); // Safe cleanup of ad text wrappers without removing DOM nodes (avoids breaking React Fiber) setTimeout(() => this._sweepAds(), 1500); setTimeout(() => this._sweepAds(), 4000); }, _sweepAds() { try { 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; } } } } catch {} }, }; // ═══════════════════════════════════════════════════════════════ // 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); } catch (err) { console.warn('[BetterCF] Themes section rendering failed:', err); } 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 && upsell.parentNode) { try { upsell.before(section); } catch { view.appendChild(section); } } 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); } })();