933 lines
32 KiB
JavaScript
933 lines
32 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 = {
|
|
_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 = `
|
|
<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) {
|
|
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 = `
|
|
<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);
|
|
}
|
|
})();
|