BetterCurseForge/bettercf/core/renderer.js

880 lines
30 KiB
JavaScript

// 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 = `
<div class="horizontal-divider"></div>
<div class="bcf-themes-header">
<h2 class="bcf-heading">
<span>Client Themes</span>
<span class="bcf-tag">BetterCurseForge</span>
</h2>
<div class="bcf-actions">
<button class="bcf-btn" id="bcf-open-folder-btn" type="button" title="Open themes folder">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor">
<path d="M2 5C2 4.44772 2.44772 4 3 4H9.61257C10.043 4 10.4251 4.27543 10.5613 4.68377L11 6H2V5Z"/>
<path d="M2 8V19C2 19.5523 2.44772 20 3 20H21C21.5523 20 22 19.5523 22 19V9C22 8.44772 21.5523 8 21 8H2Z"/>
</svg>
<span>Open Folder</span>
</button>
<button class="bcf-btn" id="bcf-refresh-themes-btn" type="button" title="Reload custom themes">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor">
<g transform="translate(2, 2)">
<path d="M10 4C6.68629 4 4 6.68629 4 10H2C2 5.58172 5.58172 2 10 2C12.261 2 14.3029 2.93795 15.7579 4.44597L17.1314 2.94754C17.4396 2.61139 18 2.8294 18 3.2854V7.5C18 7.69203 17.8918 7.85877 17.7329 7.94253C17.738 7.96166 17.743 7.98082 17.748 8L13.6366 8C13.2016 8 12.9741 7.48284 13.268 7.16214L14.403 5.92399C13.3071 4.7407 11.74 4 10 4Z"/>
<path d="M10 16C13.3137 16 16 13.3137 16 10H18C18 14.4183 14.4183 18 10 18C7.73902 18 5.69711 17.062 4.24214 15.554L2.86858 17.0525C2.56044 17.3886 2 17.1706 2 16.7146V12.5C2 12.308 2.10825 12.1412 2.26705 12.0575C2.26198 12.0383 2.25697 12.0192 2.25204 12H6.36338C6.79843 12 7.02594 12.5172 6.73196 12.8379L5.59699 14.076C6.69293 15.2593 8.25997 16 10 16Z"/>
</g>
</svg>
<span>Refresh</span>
</button>
</div>
</div>
<div class="themes-list" id="bcf-custom-themes-grid"></div>
`;
// 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 = `
<div class="primary"></div>
<div class="title"></div>
<div class="body"></div>
<div class="name" title="${escapeHtml(theme.description || theme.name)}">${escapeHtml(theme.name)}</div>
<div class="thumb-icon">
<svg class="sprite-icon" aria-hidden="true" focusable="false">
<use href="../../assets/images/sprite.svg#icon-check"></use>
</svg>
</div>
`;
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);
}
})();