FC2PPVDB Enhanced

图形化设置面板,提供悬浮/点击播放、磁力链接、快捷搜索、额外预览、历史高亮和隐藏、数据统计与成就。

// ==UserScript==
// @name            FC2PPVDB Enhanced
// @name:en         FC2PPVDB Enhanced
// @description     图形化设置面板,提供悬浮/点击播放、磁力链接、快捷搜索、额外预览、历史高亮和隐藏、数据统计与成就。
// @description:en  Graphical settings panel with hover/click-to-play, magnet links, quick search, extra previews, history highlighting & hiding, data statistics, and achievements.
// @namespace       https://greasyfork.org/zh-CN/scripts/552583-fc2ppvdb-enhanced
// @version         1.5.1
// @author          Icarusle
// @license         MIT
// @icon            https://www.google.com/s2/favicons?sz=64&domain=fc2ppvdb.com
// @match           https://fc2ppvdb.com/*
// @match           https://fd2ppv.cc/*
// @grant           GM_addStyle
// @grant           GM_xmlhttpRequest
// @grant           GM_setValue
// @grant           GM_getValue
// @grant           GM_deleteValue
// @grant           GM_setClipboard
// @grant           GM_registerMenuCommand
// @grant           GM_unregisterMenuCommand
// @connect         sukebei.nyaa.si
// @connect         wumaobi.com
// @require         https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js
// ==/UserScript==

(() => {
    'use strict';

    // =============================================================================
    // 第一部分:内核 - 通用模块与配置中心
    // =============================================================================

    const Config = {
        CACHE_KEY: 'magnet_cache_v1',
        SETTINGS_KEY: 'settings_v1',
        HISTORY_KEY: 'history_v1',
        STATS_KEY: 'stats_v1',
        ACHIEVEMENTS_KEY: 'achievements_v1',
        MAX_HISTORY_SIZE: 1000,
        CACHE_EXPIRATION_DAYS: 7,
        CACHE_MAX_SIZE: 500,
        DEBOUNCE_DELAY: 400,
        COPIED_BADGE_DURATION: 1500,
        PREVIEW_VIDEO_TIMEOUT: 5000,
        NETWORK: {
            API_TIMEOUT: 20000,
            CHUNK_SIZE: 12,
            MAX_RETRIES: 2,
            RETRY_DELAY: 2000,
        },
        CLASSES: {
            cardRebuilt: 'card-rebuilt',
            processedCard: 'processed-card',
            hideNoMagnet: 'hide-no-magnet',
            videoPreviewContainer: 'video-preview-container',
            staticPreview: 'static-preview',
            previewElement: 'preview-element',
            hidden: 'hidden',
            infoArea: 'info-area',
            customTitle: 'custom-card-title',
            fc2IdBadge: 'fc2-id-badge',
            badgeCopied: 'copied',
            preservedIconsContainer: 'preserved-icons-container',
            resourceLinksContainer: 'resource-links-container',
            resourceBtn: 'resource-btn',
            btnLoading: 'is-loading',
            btnMagnet: 'magnet',
            tooltip: 'tooltip',
            buttonText: 'button-text',
            extraPreviewContainer: 'preview-container',
            extraPreviewTitle: 'preview-title',
            extraPreviewGrid: 'preview-grid',
            isCensored: 'is-censored',
            hideCensored: 'hide-censored',
            isViewed: 'is-viewed',
            hideViewed: 'hide-viewed',
        },
        SITES: {
            'fd2ppv.cc': {
                routes: [
                    { path: /^\/articles\/\d+/, processor: 'FD2PPV_DetailPageProcessor' },
                    { path: /^\/actresses\//, processor: 'FD2PPV_ActressPageProcessor' },
                    { path: /.*/, processor: 'FD2PPV_ListPageProcessor' },
                ]
            },
            'fc2ppvdb.com': {
                routes: [
                    { path: /^\/articles\/\d+/, processor: 'FC2PPVDB_DetailPageProcessor' },
                    { path: /.*/, processor: 'FC2PPVDB_ListPageProcessor' },
                ]
            }
        }
    };

    const Utils = {
        debounce(func, delay) {
            let timeout;
            return function(...args) {
                clearTimeout(timeout);
                timeout = setTimeout(() => func.apply(this, args), delay);
            };
        },
        chunk: (arr, size) => Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => arr.slice(i * size, i * size + size)),
        sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
        extractFC2Id: (url) => url?.match(/articles\/(\d+)/)?.[1] ?? null,
        getIconSortScore: (node) => {
            if (node.querySelector('.icon-mosaic_free')) return 0;
            if (node.querySelector('.icon-face_free')) return 1;
            return 2;
        },
    };

    const Localization = {
        _lang: 'en',
        _translations: {
            en: {
                settingsTitle: "FC2PPVDB Enhanced",
                tabSettings: "Settings",
                tabStatistics: "Statistics",
                groupFilters: "General Filters",
                optionHideNoMagnet: "Hide results with no magnet links",
                optionHideCensored: "Hide censored works",
                optionHideViewed: "Hide viewed works",
                groupAppearance: "Appearance & Interaction",
                labelPreviewMode: "Preview Mode (Refresh required)",
                previewModeStatic: "Static Image",
                previewModeHover: "Hover/Click to Play",
                labelCardLayout: "Card Layout",
                layoutDefault: "Default",
                layoutCompact: "Compact",
                labelButtonStyle: "Quick Button Style",
                buttonStyleIcon: "Icon Only",
                buttonStyleText: "Icon and Text",
                groupDataHistory: "Data & History",
                optionEnableHistory: "Enable history feature (Highlight/Hide)",
                optionLoadExtraPreviews: "Load extra previews on detail pages (Refresh required)",
                labelCacheManagement: "Cache Management",
                btnClearCache: "Clear magnet link cache",
                labelHistoryManagement: "History Management",
                btnClearHistory: "Clear browsing history",
                btnSaveAndApply: "Save and Apply",
                alertSettingsSaved: "Settings saved! Some changes may require a page refresh to take full effect.",
                alertCacheCleared: "Magnet link cache has been cleared!",
                alertHistoryCleared: "Browsing history has been cleared!",
                menuOpenSettings: "⚙️ Open Settings Panel",
                tooltipCopyMagnet: "Copy Magnet Link",
                tooltipCopied: "Copied!",
                tooltipLoading: "Loading...",
                extraPreviewTitle: "Extra Previews",
                statTotalViews: "Total Views",
                statCachedMagnets: "Cached Magnets",
                statCacheHits: "Cache Hits",
                chartLoading: "Loading chart...",
                chartActivityTitle: "Browsing Activity Trend",
                chartActivityLabel: "Views in the last 30 days",
                chartCacheTitle: "Cache Usage",
                chartCacheUsed: "Used Cache",
                chartCacheFree: "Free Space",
                achievementsTitle: "Achievement Milestones",
                statusUnlocked: "Unlocked",
                statusLocked: "Locked",
                ach_view10_title: "First Steps", ach_view10_desc: "View 10 works in total.",
                ach_view100_title: "Getting the Hang", ach_view100_desc: "View 100 works in total.",
                ach_view1000_title: "Seasoned Connoisseur", ach_view1000_desc: "View 1000 works in total.",
                ach_cache50_title: "Cache Master", ach_cache50_desc: "Load 50 magnet links from cache.",
                ach_cache500_title: "Efficiency Expert", ach_cache500_desc: "Load 500 magnet links from cache.",
                ach_nightOwl_title: "Night Owl", ach_nightOwl_desc: "Browsed between 2 AM and 4 AM.",
                ach_earlyBird_title: "Early Bird", ach_earlyBird_desc: "Browsed between 5 AM and 7 AM.",
                ach_weekendWarrior_title: "Weekend Warrior", ach_weekendWarrior_desc: "Viewed over 30 works during a single weekend.",
                ach_endurance_title: "Endurance Runner", ach_endurance_desc: "Browsed every day for 7 consecutive days.",
                ach_fullThrottle_title: "Full Throttle", ach_fullThrottle_desc: "Viewed over 20 works within 1 hour.",
                ach_luckyNumber_title: "Lucky Number", ach_luckyNumber_desc: "Viewed a work with '666' or '888' in its ID.",
                ach_veteranDriver_title: "Veteran Driver", ach_veteranDriver_desc: "First and last view records are more than 365 days apart.",
                ach_achievementHunter_title: "Achievement Hunter", ach_achievementHunter_desc: "Unlock your first achievement."
            },
            zh: {
                settingsTitle: "FC2PPVDB Enhanced",
                tabSettings: "设置",
                tabStatistics: "统计",
                groupFilters: "通用过滤",
                optionHideNoMagnet: "隐藏无磁力结果",
                optionHideCensored: "隐藏有码作品",
                optionHideViewed: "隐藏已浏览的作品",
                groupAppearance: "外观与交互",
                labelPreviewMode: "预览模式 (需要刷新)",
                previewModeStatic: "静态图片",
                previewModeHover: "悬浮/点击播放",
                labelCardLayout: "卡片布局",
                layoutDefault: "默认",
                layoutCompact: "紧凑",
                labelButtonStyle: "快捷按钮样式",
                buttonStyleIcon: "仅图标",
                buttonStyleText: "图标和文字",
                groupDataHistory: "数据与历史",
                optionEnableHistory: "启用浏览历史功能 (高亮/隐藏)",
                optionLoadExtraPreviews: "在详情页加载额外预览 (需要刷新)",
                labelCacheManagement: "缓存管理",
                btnClearCache: "清理磁力链接缓存",
                labelHistoryManagement: "历史记录管理",
                btnClearHistory: "清除浏览历史",
                btnSaveAndApply: "保存并应用",
                alertSettingsSaved: "设置已保存!部分更改可能需要刷新页面才能完全生效。",
                alertCacheCleared: "磁力链接缓存已清除!",
                alertHistoryCleared: "浏览历史已清除!",
                menuOpenSettings: "⚙️ 打开设置面板",
                tooltipCopyMagnet: "复制磁力链接",
                tooltipCopied: "已复制!",
                tooltipLoading: "获取中...",
                extraPreviewTitle: "额外预览",
                statTotalViews: "浏览总数",
                statCachedMagnets: "缓存磁链数",
                statCacheHits: "缓存命中",
                chartLoading: "正在加载图表...",
                chartActivityTitle: "浏览活动趋势",
                chartActivityLabel: "过去30天浏览量",
                chartCacheTitle: "缓存使用率",
                chartCacheUsed: "已用缓存",
                chartCacheFree: "可用空间",
                achievementsTitle: "成就里程碑",
                statusUnlocked: "已解锁",
                statusLocked: "未解锁",
                ach_view10_title: "初窥门径", ach_view10_desc: "累计浏览10个作品",
                ach_view100_title: "渐入佳境", ach_view100_desc: "累计浏览100个作品",
                ach_view1000_title: "博览群片", ach_view1000_desc: "累计浏览1000个作品",
                ach_cache50_title: "缓存大师", ach_cache50_desc: "通过缓存加载50次磁力链接",
                ach_cache500_title: "效率专家", ach_cache500_desc: "通过缓存加载500次磁力链接",
                ach_nightOwl_title: "夜猫子", ach_nightOwl_desc: "在凌晨2点至4点之间进行过浏览",
                ach_earlyBird_title: "闻鸡起舞", ach_earlyBird_desc: "在清晨5点至7点之间进行过浏览",
                ach_weekendWarrior_title: "周末勇士", ach_weekendWarrior_desc: "在一个周末内浏览超过30个作品",
                ach_endurance_title: "持之以恒", ach_endurance_desc: "连续7天每天都有浏览记录",
                ach_fullThrottle_title: "火力全开", ach_fullThrottle_desc: "在1小时内浏览超过20个作品",
                ach_luckyNumber_title: "幸运数字", ach_luckyNumber_desc: "浏览过ID包含 \"666\" 或 \"888\" 的作品",
                ach_veteranDriver_title: "老司机", ach_veteranDriver_desc: "首次与最近一次浏览记录相隔超过365天",
                ach_achievementHunter_title: "成就猎人", ach_achievementHunter_desc: "解锁您的第一个成就"
            }
        },
        init() {
            const browserLang = (navigator.language || navigator.userLanguage).split('-')[0].toLowerCase();
            if (browserLang === 'zh') this._lang = 'zh';
            else this._lang = 'en';
        },
        t(key) {
            return this._translations[this._lang]?.[key] || this._translations['en']?.[key] || key;
        }
    };
    const t = Localization.t.bind(Localization);

    class EventEmitter {
        constructor() { this.events = {}; }
        on(eventName, listener) {
            if (!this.events[eventName]) this.events[eventName] = [];
            this.events[eventName].push(listener);
        }
        emit(eventName, payload) {
            this.events[eventName]?.forEach(listener => listener(payload));
        }
    }
    const AppEvents = new EventEmitter();

    class StorageManager {
        static get(key, def) { return GM_getValue(key, def); }
        static set(key, val) { GM_setValue(key, val); }
        static delete(key) { GM_deleteValue(key); }
    }

    class StatsTracker {
        static stats = {};
        static load() { this.stats = StorageManager.get(Config.STATS_KEY, {}); }
        static save() { StorageManager.set(Config.STATS_KEY, this.stats); }
        static get(key, def = 0) { return this.stats[key] ?? def; }
        static increment(key) { this.stats[key] = (this.stats[key] || 0) + 1; this.save(); }
    }

    class HistoryManager {
        static history = [];
        static load() {
            if (!SettingsManager.get('enableHistory')) return;
            try {
                const storedHistory = JSON.parse(StorageManager.get(Config.HISTORY_KEY, '[]'));
                if (!Array.isArray(storedHistory)) { this.history = []; return; }
                if (storedHistory.length > 0 && typeof storedHistory[0] === 'string') {
                    this.history = storedHistory.map(id => ({ id: String(id), timestamp: Date.now() }));
                    this.save();
                } else { this.history = storedHistory; }
            } catch (e) { this.history = []; }
        }
        static save() {
            if (!SettingsManager.get('enableHistory')) return;
            if (this.history.length > Config.MAX_HISTORY_SIZE) {
                this.history.splice(0, this.history.length - Config.MAX_HISTORY_SIZE);
            }
            StorageManager.set(Config.HISTORY_KEY, JSON.stringify(this.history));
        }
        static add(id) {
            if (!SettingsManager.get('enableHistory') || !id) return;
            this.history = this.history.filter(item => item.id !== id);
            this.history.push({ id, timestamp: Date.now() });
            this.save();
        }
        static has(id) {
            if (!SettingsManager.get('enableHistory')) return false;
            return this.history.some(item => item.id === id);
        }
        static getRawData() { return this.history; }
        static clear() { this.history = []; StorageManager.delete(Config.HISTORY_KEY); }
    }

    class SettingsManager {
        static settings = {};
        static defaults = {
            previewMode: 'static',
            hideNoMagnet: false,
            hideCensored: false,
            cardLayoutMode: 'default',
            buttonStyle: 'icon',
            loadExtraPreviews: false,
            enableHistory: true,
            hideViewed: false,
        };
        static load() { this.settings = { ...this.defaults, ...StorageManager.get(Config.SETTINGS_KEY, {}) }; }
        static get(key) { return this.settings[key]; }
        static set(key, value) {
            const oldValue = this.settings[key];
            if (oldValue !== value) {
                this.settings[key] = value;
                this.save();
                AppEvents.emit('settingsChanged', { key, newValue: value, oldValue });
            }
        }
        static save() { StorageManager.set(Config.SETTINGS_KEY, this.settings); }
    }

    class AchievementManager {
        static unlockedIds = new Set();
        static _achievements = [
            { id: 'view10', titleKey: 'ach_view10_title', descriptionKey: 'ach_view10_desc', icon: 'fa-seedling', isUnlocked: stats => stats.historyData.length >= 10 },
            { id: 'view100', titleKey: 'ach_view100_title', descriptionKey: 'ach_view100_desc', icon: 'fa-tree', isUnlocked: stats => stats.historyData.length >= 100 },
            { id: 'view1000', titleKey: 'ach_view1000_title', descriptionKey: 'ach_view1000_desc', icon: 'fa-forest', isUnlocked: stats => stats.historyData.length >= 1000 },
            { id: 'cache50', titleKey: 'ach_cache50_title', descriptionKey: 'ach_cache50_desc', icon: 'fa-bolt-lightning', isUnlocked: stats => stats.cacheStats.hits >= 50 },
            { id: 'cache500', titleKey: 'ach_cache500_title', descriptionKey: 'ach_cache500_desc', icon: 'fa-rocket', isUnlocked: stats => stats.cacheStats.hits >= 500 },
            { id: 'nightOwl', titleKey: 'ach_nightOwl_title', descriptionKey: 'ach_nightOwl_desc', icon: 'fa-moon', isUnlocked: stats => stats.historyData.some(item => { const hour = new Date(item.timestamp).getHours(); return hour >= 2 && hour < 4; })},
            { id: 'earlyBird', titleKey: 'ach_earlyBird_title', descriptionKey: 'ach_earlyBird_desc', icon: 'fa-sun', isUnlocked: stats => stats.historyData.some(item => { const hour = new Date(item.timestamp).getHours(); return hour >= 5 && hour < 7; })},
            { id: 'weekendWarrior', titleKey: 'ach_weekendWarrior_title', descriptionKey: 'ach_weekendWarrior_desc', icon: 'fa-calendar-week', isUnlocked: stats => {
                const weekendViews = new Map();
                stats.historyData.forEach(item => {
                    const date = new Date(item.timestamp);
                    const day = date.getDay();
                    if (day === 0 || day === 6) {
                        const weekStart = new Date(date);
                        weekStart.setDate(date.getDate() - day);
                        const weekKey = weekStart.toISOString().slice(0, 10);
                        weekendViews.set(weekKey, (weekendViews.get(weekKey) || 0) + 1);
                    }
                });
                return [...weekendViews.values()].some(count => count > 30);
            }},
            { id: 'endurance', titleKey: 'ach_endurance_title', descriptionKey: 'ach_endurance_desc', icon: 'fa-calendar-days', isUnlocked: stats => {
                if (stats.historyData.length < 7) return false;
                const uniqueDays = new Set(stats.historyData.map(item => new Date(item.timestamp).toISOString().slice(0, 10)));
                if (uniqueDays.size < 7) return false;
                const sortedDays = [...uniqueDays].sort();
                let consecutiveCount = 1;
                for (let i = 1; i < sortedDays.length; i++) {
                    const prevDate = new Date(sortedDays[i - 1]);
                    const currentDate = new Date(sortedDays[i]);
                    if ((currentDate - prevDate) / (1000 * 60 * 60 * 24) === 1) {
                        consecutiveCount++;
                        if (consecutiveCount >= 7) return true;
                    } else { consecutiveCount = 1; }
                }
                return false;
            }},
            { id: 'fullThrottle', titleKey: 'ach_fullThrottle_title', descriptionKey: 'ach_fullThrottle_desc', icon: 'fa-gauge-high', isUnlocked: stats => {
                if (stats.historyData.length < 20) return false;
                const sortedHistory = [...stats.historyData].sort((a, b) => a.timestamp - b.timestamp);
                for (let i = 0; i <= sortedHistory.length - 20; i++) {
                    if (sortedHistory[i + 19].timestamp - sortedHistory[i].timestamp <= 3600000) return true;
                }
                return false;
            }},
            { id: 'luckyNumber', titleKey: 'ach_luckyNumber_title', descriptionKey: 'ach_luckyNumber_desc', icon: 'fa-clover', isUnlocked: stats => stats.historyData.some(item => item.id.includes('666') || item.id.includes('888')) },
            { id: 'veteranDriver', titleKey: 'ach_veteranDriver_title', descriptionKey: 'ach_veteranDriver_desc', icon: 'fa-award', isUnlocked: stats => {
                if (stats.historyData.length < 2) return false;
                const timestamps = stats.historyData.map(item => item.timestamp);
                const minTimestamp = Math.min(...timestamps);
                const maxTimestamp = Math.max(...timestamps);
                return (maxTimestamp - minTimestamp) > (365 * 24 * 60 * 60 * 1000);
            }},
            { id: 'achievementHunter', titleKey: 'ach_achievementHunter_title', descriptionKey: 'ach_achievementHunter_desc', icon: 'fa-trophy', isUnlocked: () => AchievementManager.getUnlockedIds().size >= 1 },
        ];
        static load() { this.unlockedIds = new Set(StorageManager.get(Config.ACHIEVEMENTS_KEY, [])); }
        static checkAll(stats) {
            let newUnlocked = false;
            this._achievements.forEach(ach => {
                if (!this.unlockedIds.has(ach.id) && ach.isUnlocked(stats)) {
                    this.unlockedIds.add(ach.id);
                    newUnlocked = true;
                }
            });
            if (newUnlocked) {
                this._achievements.forEach(ach => {
                    if (ach.id === 'achievementHunter' && !this.unlockedIds.has(ach.id) && ach.isUnlocked(stats)) {
                        this.unlockedIds.add(ach.id);
                    }
                });
                StorageManager.set(Config.ACHIEVEMENTS_KEY, [...this.unlockedIds]);
            }
        }
        static getAll() { return this._achievements; }
        static getUnlockedIds() { return this.unlockedIds; }
    }

    class CacheManager {
        constructor() {
            this.key = Config.CACHE_KEY; this.maxSize = Config.CACHE_MAX_SIZE;
            this.expirationMs = Config.CACHE_EXPIRATION_DAYS * 24 * 60 * 60 * 1000;
            this.data = new Map(); this.load();
        }
        load() {
            try {
                const data = JSON.parse(StorageManager.get(this.key) || '{}');
                const now = Date.now();
                Object.entries(data)
                    .filter(([, value]) => value?.t && now - value.t < this.expirationMs)
                    .forEach(([key, value]) => this.data.set(key, value));
            } catch (e) { this.data = new Map(); }
        }
        get(id) {
            const item = this.data.get(id);
            if (!item || Date.now() - item.t >= this.expirationMs) {
                this.data.delete(id); return null;
            }
            this.data.delete(id); this.data.set(id, item);
            StatsTracker.increment('cacheHits');
            return item.v;
        }
        set(id, value) {
            if (this.data.size >= this.maxSize && !this.data.has(id)) {
                this.data.delete(this.data.keys().next().value);
            }
            this.data.set(id, { v: value, t: Date.now() });
        }
        save() { StorageManager.set(this.key, JSON.stringify(Object.fromEntries(this.data))); }
        clear() { this.data.clear(); StorageManager.delete(this.key); }
        getSize() { return this.data.size; }
    }

    class NetworkManager {
        static async fetchMagnetLinks(fc2Ids) {
            if (!fc2Ids || fc2Ids.length === 0) return new Map();
            for (let attempt = 0; attempt <= Config.NETWORK.MAX_RETRIES; attempt++) {
                try {
                    if (attempt > 0) await Utils.sleep(Config.NETWORK.RETRY_DELAY * attempt);
                    return await this._doFetchMagnets(fc2Ids);
                } catch (e) {
                    if (attempt === Config.NETWORK.MAX_RETRIES) return new Map();
                }
            }
            return new Map();
        }
        static _doFetchMagnets(ids) {
            return new Promise((resolve, reject) => {
                const query = ids.map(id => `fc2-ppv-${id}`).join('|');
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://sukebei.nyaa.si/?f=0&c=0_0&q=${encodeURIComponent(query)}&s=seeders&o=desc`,
                    timeout: Config.NETWORK.API_TIMEOUT,
                    onload: (res) => {
                        if (res.status !== 200) return reject();
                        const magnetMap = new Map();
                        const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
                        doc.querySelectorAll('table.torrent-list tbody tr').forEach(row => {
                            const title = row.querySelector('td[colspan="2"] a:not(.comments)')?.textContent;
                            const magnetLink = row.querySelector("a[href^='magnet:?']")?.href;
                            const match = title?.match(/fc2-ppv-(\d+)/i);
                            if (match?.[1] && magnetLink && !magnetMap.has(match[1])) {
                                magnetMap.set(match[1], magnetLink);
                            }
                        });
                        resolve(magnetMap);
                    },
                    onerror: reject,
                    ontimeout: reject
                });
            });
        }
        static async fetchExtraPreviews(fc2Id) {
            return new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://wumaobi.com/fc2daily/detail/FC2-PPV-${fc2Id}`,
                    onload: (res) => {
                        if (res.status !== 200) return resolve([]);
                        const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
                        const results = [];
                        doc.querySelectorAll('img').forEach(img => {
                            try {
                                const p = new URL(img.src).pathname;
                                if (!img.src.includes('watch/103281970') && !p.includes('qrcode') && p !== '/static/moechat_ads.jpg' && !p.endsWith('/cover.jpg')) {
                                    results.push({ type: 'image', src: 'https://wumaobi.com' + p });
                                }
                            } catch {}
                        });
                        doc.querySelectorAll('video').forEach(v => {
                             try { results.push({ type: 'video', src: 'https://wumaobi.com' + new URL(v.src).pathname }); } catch {}
                        });
                        resolve(results);
                    }
                });
            });
        }
    }

    class PreviewManager {
        static activePreview = null;
        static init(container, cardSelector) {
            const mode = SettingsManager.get('previewMode');
            if (mode === 'static') return;
            const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
            if (mode === 'hover' && !isTouchDevice) {
                container.addEventListener('mouseenter', (e) => this.handleMouseEnter(e, cardSelector), true);
                container.addEventListener('mouseleave', (e) => this.handleMouseLeave(e, cardSelector), true);
            } else if (mode === 'hover' && isTouchDevice) {
                 container.addEventListener('click', (e) => this.handleClick(e, cardSelector), false);
            }
        }
        static handleMouseEnter(event, cardSelector) {
            const card = event.target.closest(cardSelector);
            if (card) this._showPreview(card);
        }
        static handleMouseLeave(event, cardSelector) {
            const card = event.target.closest(cardSelector);
            if (card && this.activePreview && this.activePreview.card === card) {
                this.activePreview.hidePreview();
            }
        }
        static handleClick(event, cardSelector) {
            const card = event.target.closest(cardSelector);
            if (!card) return;
            const isAlreadyPreviewing = this.activePreview?.card === card;
            if (isAlreadyPreviewing) return;
            if (this.activePreview && this.activePreview.card !== card) {
                this.activePreview.hidePreview();
            }
            if (!card.dataset.previewStarted) {
                event.preventDefault();
                this._showPreview(card);
                card.dataset.previewStarted = "true";
            }
        }
        static async _showPreview(card) {
            if (card.dataset.previewFailed) return;
            if (this.activePreview) this.activePreview.hidePreview();
            const fc2Id = card.dataset.fc2id;
            const container = card.querySelector(`.${Config.CLASSES.videoPreviewContainer}`);
            const img = container?.querySelector('img');
            if (!fc2Id || !container || !img) return;
            let video = container.querySelector('video');
            if (!video) {
                 video = this._createVideoElement(`https://fourhoi.com/fc2-ppv-${fc2Id}/preview.mp4`, card);
                 container.appendChild(video);
            }
            img.classList.add(Config.CLASSES.hidden);
            video.classList.remove(Config.CLASSES.hidden);
            const hidePreview = () => {
                video.pause();
                video.classList.add(Config.CLASSES.hidden);
                img.classList.remove(Config.CLASSES.hidden);
                if (this.activePreview?.card === card) this.activePreview = null;
                card.dataset.previewStarted = "";
            };
            try {
                await video.play();
                this.activePreview = { video, card, hidePreview };
            } catch (e) { hidePreview(); }
        }
        static _createVideoElement(src, card) {
            const video = UIBuilder.createElement('video', {
                src: src,
                className: `${Config.CLASSES.previewElement} ${Config.CLASSES.hidden}`,
                loop: true, muted: true, playsInline: true, preload: 'auto'
            });
            const loadTimeout = setTimeout(() => video.remove(), Config.PREVIEW_VIDEO_TIMEOUT);
            video.addEventListener('loadeddata', () => clearTimeout(loadTimeout), { once: true });
            video.addEventListener('error', () => {
                video.remove();
                if (card) card.dataset.previewFailed = 'true';
            }, { once: true });
            return video;
        }
    }

    class StyleManager {
        static inject() {
            const C = Config.CLASSES;
            GM_addStyle(`
                /* --- 全局字体与基础 --- */
                body {
                    --fc2-enh-bg: #1e1e2e; --fc2-enh-surface: rgba(30, 30, 46, 0.8);
                    --fc2-enh-text: #cdd6f4; --fc2-enh-text-dim: #a6adc8;
                    --fc2-enh-border: rgba(205, 214, 244, 0.1); --fc2-enh-primary: #89b4fa;
                    --fc2-enh-accent-grad: linear-gradient(135deg, #cba6f7, #f5c2e7);
                    --fc2-enh-radius: 12px; --fc2-enh-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
                    --fc2-enh-transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
                }
                .${C.cardRebuilt} { padding: 0 !important; border-radius: var(--fc2-enh-radius); overflow: hidden; background: #252528; border: 1px solid var(--fc2-enh-border); }
                .${C.processedCard} { position: relative; overflow: hidden; border-radius: var(--fc2-enh-radius); transition: var(--fc2-enh-transition); background: #1a1a1a; border: 2px solid transparent; }
                .${C.processedCard}:hover { transform: translateY(-6px); box-shadow: 0 10px 20px rgba(0,0,0,.4), 0 0 15px rgba(203, 166, 247, 0.3); }
                .${C.processedCard}.${C.isViewed} { border-color: #cba6f7; box-shadow: 0 0 15px rgba(203, 166, 247, 0.4); }
                .${C.processedCard}.${C.isViewed}:hover > div { opacity: 1; }
                .${C.videoPreviewContainer} { position: relative; width: 100%; height: 20rem; background: #000; border-radius: var(--fc2-enh-radius) var(--fc2-enh-radius) 0 0; overflow: hidden; }
                @media (max-width: 768px) { .${C.videoPreviewContainer} { height: auto; aspect-ratio: 16 / 10; } }
                .${C.videoPreviewContainer} video, .${C.videoPreviewContainer} img.${C.staticPreview} { width: 100%; height: 100%; object-fit: contain; transition: transform .4s ease; }
                .${C.processedCard}:hover .${C.videoPreviewContainer} video, .${C.processedCard}:hover .${C.videoPreviewContainer} img.${C.staticPreview} { transform: scale(1.05); }
                .${C.previewElement} { position: absolute; top: 0; left: 0; transition: opacity 0.3s ease; }
                .${C.previewElement}.${C.hidden} { opacity: 0; pointer-events: none; }
                .${C.fc2IdBadge} { position: absolute; top: 10px; right: 10px; padding: 4px 10px; background: rgba(0,0,0,.5); backdrop-filter: blur(8px); color: var(--fc2-enh-text); font-size: 12px; font-weight: 700; border-radius: 8px; z-index: 10; cursor: pointer; transition: var(--fc2-enh-transition); border: 1px solid rgba(255,255,255,0.1); }
                .${C.fc2IdBadge}:hover { background: rgba(0,0,0,.7); transform: scale(1.05); }
                .${C.fc2IdBadge}.${C.badgeCopied} { background: #a6e3a1 !important; color: #111; }
                .${C.infoArea} { padding: 1rem 1.25rem; background: #252528; }
                .${C.customTitle} { color: var(--fc2-enh-text); font-size: 14px; font-weight: 600; line-height: 1.4; margin: 0 0 12px; height: 40px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
                .${C.resourceLinksContainer} { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
                .${C.resourceBtn} { position: relative; display: inline-flex; align-items: center; justify-content: center; color: var(--fc2-enh-text-dim); text-decoration: none; transition: var(--fc2-enh-transition); cursor: pointer; padding: .5em; aspect-ratio: 1; border-radius: 8px; background: rgba(255,255,255,.1); }
                .${C.resourceBtn}:hover { transform: scale(1.1); color: #fff; background: rgba(255,255,255,.15); }
                .${C.resourceBtn} i { font-size: .9em; }
                .${C.resourceBtn} .${C.tooltip} { position: absolute; bottom: 130%; left: 50%; transform: translateX(-50%); background: #111; color: #fff; padding: .4em .8em; border-radius: 6px; font-size: .8em; white-space: nowrap; opacity: 0; visibility: hidden; transition: var(--fc2-enh-transition); pointer-events: none; z-index: 1000; }
                .${C.resourceBtn}:hover .${C.tooltip} { opacity: 1; visibility: visible; }
                .${C.resourceBtn} .${C.buttonText} { display: none; }
                .${C.resourceBtn} i, .${C.resourceBtn} svg { pointer-events: none; }
                .${C.resourceBtn}.${C.btnLoading} { cursor: not-allowed; background: #4b5563; }
                .${C.resourceBtn}.${C.btnLoading} i { animation: spin 1s linear infinite; }
                @keyframes spin { from{transform:rotate(0)} to{transform:rotate(360deg)} }
                .${C.preservedIconsContainer} { position: absolute; top: 10px; left: 10px; z-index: 10; display: flex; flex-direction: row; gap: 6px; }
                .${C.cardRebuilt}.${C.hideNoMagnet}, .${C.cardRebuilt}.${C.isCensored}.${C.hideCensored}, .${C.cardRebuilt}.${C.isViewed}.${C.hideViewed} { display: none !important; }
                .${C.extraPreviewContainer} { margin-top: 2rem; }
                .${C.extraPreviewTitle} { font-size: 1.5rem; font-weight: 700; color: #fff; text-align: center; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--fc2-enh-border); }
                .${C.extraPreviewGrid} { display: flex; flex-wrap: wrap; )); justify-content: center; gap: 1rem; }
                .${C.extraPreviewGrid} img, .${C.extraPreviewGrid} video { max-width: 100%; height: auto; border-radius: var(--fc2-enh-radius); background: #000; }
                .layout-compact .${C.videoPreviewContainer} { height: 16rem; }
                @media (max-width: 768px) { .layout-compact .${C.videoPreviewContainer} { height: auto; aspect-ratio: 16 / 9; } }
                .layout-compact .${C.infoArea} { padding: 0.5rem 0.75rem; }
                .layout-compact .${C.customTitle} { font-size: 13px; height: 34px; line-height: 1.3; margin-bottom: 6px; }
                .layout-compact .${C.resourceLinksContainer} { gap: 5px; }
                .layout-compact .${C.resourceBtn} { padding: .3em; border-radius: 6px; }
                .layout-compact .${C.resourceBtn} i { font-size: .8em; }
                .buttons-text .${C.resourceBtn} { aspect-ratio: auto; padding: .4em .7em; }
                .buttons-text .${C.resourceBtn} .${C.buttonText} { display: inline; font-size: 0.8em; margin-left: 0.4em; }
                .buttons-text .layout-compact .${C.resourceBtn} { padding: .3em .6em; }
                .fc2-enh-settings-backdrop { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(17, 17, 27, 0.5); z-index: 9998; backdrop-filter: blur(10px); }
                .fc2-enh-settings-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 750px; max-height: 90vh; background: var(--fc2-enh-surface); color: var(--fc2-enh-text); border-radius: 16px; box-shadow: var(--fc2-enh-shadow); z-index: 9999; display: flex; flex-direction: column; border: 1px solid var(--fc2-enh-border); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; }
                .fc2-enh-settings-header { padding: 1.25rem 2rem; border-bottom: 1px solid var(--fc2-enh-border); display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; }
                .fc2-enh-settings-header h2 { margin: 0; font-size: 1.4rem; font-weight: 600; }
                .fc2-enh-settings-header .close-btn { background: none; border: none; color: var(--fc2-enh-text-dim); font-size: 1.8rem; cursor: pointer; transition: var(--fc2-enh-transition); }
                .fc2-enh-settings-header .close-btn:hover { color: #fff; transform: rotate(90deg); }
                .fc2-enh-settings-tabs { display: flex; padding: 0.5rem 2rem 0; border-bottom: 1px solid var(--fc2-enh-border); flex-shrink: 0; }
                .fc2-enh-tab-btn { background: none; border: none; color: var(--fc2-enh-text-dim); padding: 1rem 1.25rem; cursor: pointer; border-bottom: 3px solid transparent; font-size: 1rem; font-weight: 500; transition: var(--fc2-enh-transition); margin-bottom: -1px; }
                .fc2-enh-tab-btn:hover { color: var(--fc2-enh-text); }
                .fc2-enh-tab-btn.active { color: var(--fc2-enh-text); border-image: var(--fc2-enh-accent-grad) 1; }
                .fc2-enh-settings-content { padding: 2rem; overflow-y: auto; flex-grow: 1; }
                .fc2-enh-tab-content { display: none; }
                .fc2-enh-tab-content.active { display: block; animation: fadeIn 0.5s ease; }
                .fc2-enh-settings-content::-webkit-scrollbar { width: 8px; background-color: transparent; }
                .fc2-enh-settings-content::-webkit-scrollbar-track { background-color: rgba(0, 0, 0, 0.2); border-radius: 10px; }
                .fc2-enh-settings-content::-webkit-scrollbar-thumb { background-color: rgba(205, 214, 244, 0.25); border-radius: 10px; transition: background-color 0.3s ease; }
                .fc2-enh-settings-content::-webkit-scrollbar-thumb:hover { background-color: rgba(137, 180, 250, 0.5); }
                @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
                .fc2-enh-settings-group { margin-bottom: 2rem; }
                .fc2-enh-settings-group h3 { margin-top: 0; margin-bottom: 1.25rem; border-bottom: 1px solid var(--fc2-enh-border); padding-bottom: 0.75rem; font-size: 1.1rem; font-weight: 600; }
                .fc2-enh-form-row { margin-bottom: 1.25rem; display: flex; flex-direction: column; gap: 0.5rem; }
                .fc2-enh-form-row label { font-weight: 500; }
                .fc2-enh-form-row select { width: 100%; background: rgba(0,0,0,0.2); border: 1px solid var(--fc2-enh-border); border-radius: 8px; padding: 0.75rem; color: var(--fc2-enh-text); box-sizing: border-box; transition: var(--fc2-enh-transition); }
                .fc2-enh-form-row select:focus { border-color: var(--fc2-enh-primary); box-shadow: 0 0 0 3px rgba(137, 180, 250, 0.3); }
                .fc2-enh-form-row label[for^="setting-"] { display: flex; align-items: center; cursor: pointer; }
                .fc2-enh-form-row select option { background: var(--fc2-enh-bg, #1e1e2e); color: var(--fc2-enh-text, #cdd6f4); }
                input[type="checkbox"] { appearance: none; width: 1.2em; height: 1.2em; border: 2px solid var(--fc2-enh-border); border-radius: 4px; margin-right: 0.75rem; display: grid; place-content: center; transition: var(--fc2-enh-transition); }
                input[type="checkbox"]::before { content: ""; width: 0.65em; height: 0.65em; transform: scale(0); transition: 120ms transform ease-in-out; background: var(--fc2-enh-accent-grad); clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); }
                input[type="checkbox"]:checked { background: var(--fc2-enh-primary); border-color: var(--fc2-enh-primary); }
                input[type="checkbox"]:checked::before { transform: scale(1.2); }
                .fc2-enh-settings-footer { padding: 1.25rem 2rem; border-top: 1px solid var(--fc2-enh-border); display: flex; justify-content: flex-end; gap: 1rem; background: rgba(0,0,0,0.2); border-radius: 0 0 16px 16px; }
                .fc2-enh-btn { background: rgba(205, 214, 244, 0.1); border: 1px solid var(--fc2-enh-border); color: var(--fc2-enh-text); padding: 0.75rem 1.5rem; border-radius: 8px; cursor: pointer; font-weight: 600; transition: var(--fc2-enh-transition); }
                .fc2-enh-btn:hover { transform: translateY(-2px); background: rgba(205, 214, 244, 0.2); box-shadow: 0 4px 10px rgba(0,0,0,0.2); }
                .fc2-enh-btn.primary { background: var(--fc2-enh-accent-grad); border: none; color: white; }
                .fc2-enh-btn.primary:hover { box-shadow: 0 6px 15px rgba(203, 166, 247, 0.4); }
                .fc2-enh-stats-overview { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
                .fc2-enh-stat-card { background: rgba(0,0,0,0.2); padding: 1.5rem; border-radius: var(--fc2-enh-radius); text-align: center; border: 1px solid var(--fc2-enh-border); transition: var(--fc2-enh-transition); }
                .fc2-enh-stat-card:hover { transform: translateY(-4px); background: rgba(0,0,0,0.3); }
                .fc2-enh-stat-card-value { font-size: 2rem; font-weight: bold; color: var(--fc2-enh-primary); }
                .fc2-enh-stat-card-label { font-size: 0.9rem; color: var(--fc2-enh-text-dim); margin-top: 0.5rem; }
                .fc2-enh-chart-container, .fc2-enh-achievements-container { background: rgba(0,0,0,0.2); padding: 1.5rem; border-radius: var(--fc2-enh-radius); margin-top: 1.5rem; border: 1px solid var(--fc2-enh-border); }
                .fc2-enh-achievements-container h3 { margin: 0 0 1.5rem; font-size: 1.1rem; text-align: center; font-weight: 600; }
                .fc2-enh-achievements-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 1rem; }
                .fc2-enh-achievement-badge { display: flex; align-items: center; background: rgba(255,255,255,0.05); padding: 0.75rem 1rem; border-radius: 8px; transition: var(--fc2-enh-transition); border: 1px solid transparent; cursor: help; }
                .fc2-enh-achievement-badge.locked { filter: grayscale(1); opacity: 0.6; }
                .fc2-enh-achievement-badge.unlocked { background: linear-gradient(135deg, rgba(203, 166, 247, 0.1), rgba(245, 194, 231, 0.1)); border-color: rgba(203, 166, 247, 0.3); }
                .fc2-enh-achievement-badge:hover { transform: scale(1.03); border-color: rgba(205, 214, 244, 0.3); }
                .fc2-enh-achievement-badge .icon { font-size: 1.8rem; margin-right: 1rem; width: 35px; text-align: center; }
                .fc2-enh-achievement-badge.unlocked .icon { color: #f9e2af; }
                .fc2-enh-achievement-badge .details { display: flex; flex-direction: column; }
                .fc2-enh-achievement-badge .title { font-weight: bold; font-size: 0.95rem; }
                .fc2-enh-achievement-badge .description { font-size: 0.85rem; color: var(--fc2-enh-text-dim); }
            `);
        }
    }

    class UIBuilder {
        static createElement(tag, options = {}) {
            const el = document.createElement(tag);
            Object.entries(options).forEach(([k, v]) => k === 'className' ? el.className = v : el[k] = v);
            return el;
        }
        static createResourceButton(type, title, icon, url) {
            const C = Config.CLASSES;
            const btn = this.createElement('a', { href: url, className: `${C.resourceBtn} ${type}` });
            if (type !== 'magnet') { btn.target = '_blank'; btn.rel = 'noopener noreferrer'; }
            btn.innerHTML = `<i class="fa-solid ${icon}"></i><span class="${C.buttonText}">${title}</span><span class="${C.tooltip}">${title}</span>`;
            return btn;
        }
        static createEnhancedCard(data) {
            const C = Config.CLASSES;
            const card = this.createElement('div', { className: C.processedCard });
            card.dataset.fc2id = data.fc2Id;
            const preview = this.createElement('div', { className: C.videoPreviewContainer });
            const previewImage = this.createElement('img', { src: data.imageUrl, className: `${C.staticPreview} ${C.previewElement}` });
            preview.append(previewImage);
            const badge = this.createElement('div', { className: C.fc2IdBadge, textContent: data.fc2Id });
            badge.addEventListener('click', (e) => {
                e.preventDefault(); e.stopPropagation();
                GM_setClipboard(data.fc2Id);
                badge.textContent = t('tooltipCopied');
                badge.classList.add(C.badgeCopied);
                setTimeout(() => { if (badge.isConnected) { badge.textContent = data.fc2Id; badge.classList.remove(C.badgeCopied); } }, Config.COPIED_BADGE_DURATION);
            });
            preview.appendChild(badge);
            if (data.preservedIconsHTML) {
                const iconsContainer = this.createElement('div', { className: C.preservedIconsContainer, innerHTML: data.preservedIconsHTML });
                preview.appendChild(iconsContainer);
                const temp = this.createElement('div', { innerHTML: data.preservedIconsHTML });
                if (temp.querySelector('.icon-mosaic_free')?.parentElement.classList.contains('color_free0')) {
                    card.classList.add(C.isCensored);
                }
            }
            const info = this.createElement('div', { className: C.infoArea });
            if (data.title) info.appendChild(this.createElement('div', { className: C.customTitle, textContent: data.title }));
            const links = this.createElement('div', { className: C.resourceLinksContainer });
            const defaultLinks = [
                { name: 'MissAV', icon: 'fa-globe', urlTemplate: 'https://missav.ws/cn/fc2-ppv-%ID%' },
                { name: 'Supjav', icon: 'fa-bolt', urlTemplate: 'https://supjav.com/zh/?s=%ID%' },
                { name: 'Sukebei', icon: 'fa-magnifying-glass', urlTemplate: 'https://sukebei.nyaa.si/?f=0&c=0_0&q=%ID%' }
            ];
            defaultLinks.forEach(link => links.append(this.createResourceButton('default-search', link.name, link.icon, link.urlTemplate.replace('%ID%', data.fc2Id))));
            info.appendChild(links);
            card.append(preview, info);
            let finalElement = card;
            if (data.articleUrl) {
                finalElement = this.createElement('a', { href: data.articleUrl, style: 'text-decoration:none;' });
                finalElement.appendChild(card);
            }
            if (SettingsManager.get('enableHistory')) {
                if (HistoryManager.has(data.fc2Id)) card.classList.add(C.isViewed);
                if (finalElement.tagName === 'A') {
                    finalElement.addEventListener('mousedown', () => {
                        HistoryManager.add(data.fc2Id);
                        card.classList.add(C.isViewed);
                        const outerCard = card.closest(`.${C.cardRebuilt}`);
                        this.applyHistoryVisibility(outerCard);
                    });
                }
            }
            return { finalElement, linksContainer: links, newCard: card };
        }
        static createExtraPreviewsGrid(previews) {
            if (!previews || previews.length === 0) return null;
            const C = Config.CLASSES;
            const container = this.createElement('div', { className: C.extraPreviewContainer });
            container.innerHTML = `<h2 class="${C.extraPreviewTitle}">${t('extraPreviewTitle')}</h2>`;
            const grid = this.createElement('div', { className: C.extraPreviewGrid });
            const fragment = document.createDocumentFragment();
            previews.forEach(p => {
                if (p.type === 'image') fragment.appendChild(this.createElement('img', { src: p.src, loading: 'lazy' }));
                else if (p.type === 'video') fragment.appendChild(this.createElement('video', { src: p.src, autoplay: true, loop: true, muted: true, controls: true }));
            });
            grid.appendChild(fragment);
            container.appendChild(grid);
            return container;
        }
        static toggleLoading(container, show) {
            if (!container?.isConnected) return;
            const loadingButton = container.querySelector(`.${Config.CLASSES.btnLoading}`);
            if (show && !loadingButton) container.appendChild(this.createResourceButton(Config.CLASSES.btnLoading, t('tooltipLoading'), 'fa-spinner', '#'));
            else if (!show && loadingButton) loadingButton.remove();
        }
        static addMagnetButton(container, url) {
            if (container && !container.querySelector(`.${Config.CLASSES.btnMagnet}`)) {
                const btn = this.createResourceButton('magnet', t('tooltipCopyMagnet'), 'fa-magnet', 'javascript:void(0);');
                btn.addEventListener('click', (e) => {
                    e.preventDefault(); e.stopPropagation();
                    GM_setClipboard(url);
                    const tooltip = btn.querySelector(`.${Config.CLASSES.tooltip}`);
                    if (tooltip) {
                        tooltip.textContent = t('tooltipCopied');
                        setTimeout(() => { tooltip.textContent = t('tooltipCopyMagnet'); }, Config.COPIED_BADGE_DURATION);
                    }
                });
                container.appendChild(btn);
            }
        }
        static applyCardVisibility(card, hasMagnet) { card?.classList.toggle(Config.CLASSES.hideNoMagnet, SettingsManager.get('hideNoMagnet') && !hasMagnet); }
        static applyCensoredFilter(card) { if (card?.classList.contains(Config.CLASSES.isCensored)) card.classList.toggle(Config.CLASSES.hideCensored, SettingsManager.get('hideCensored')); }
        static applyHistoryVisibility(card) {
            if (!card) return;
            const isViewed = card.classList.contains(Config.CLASSES.isViewed);
            card.classList.toggle(Config.CLASSES.hideViewed, SettingsManager.get('hideViewed') && isViewed);
        }
    }

    class DynamicStyleApplier {
        static init() { AppEvents.on('settingsChanged', this.handleSettingsChange.bind(this)); }
        static handleSettingsChange({ key, newValue }) {
            switch (key) {
                case 'hideNoMagnet': this.applyAllCardVisibilities(); break;
                case 'hideCensored': this.applyAllCensoredFilters(); break;
                case 'hideViewed': this.applyAllHistoryVisibilities(); break;
                case 'cardLayoutMode':
                    document.body.classList.remove('layout-default', 'layout-compact');
                    document.body.classList.add(`layout-${newValue}`);
                    break;
                case 'buttonStyle':
                    document.body.classList.remove('buttons-icon', 'buttons-text');
                    document.body.classList.add(`buttons-${newValue}`);
                    break;
            }
        }
        static applyAllCardVisibilities() {
            document.querySelectorAll(`.${Config.CLASSES.cardRebuilt}`).forEach(card => {
                const hasMagnet = !!card.querySelector(`.${Config.CLASSES.btnMagnet}`);
                UIBuilder.applyCardVisibility(card, hasMagnet);
            });
        }
        static applyAllCensoredFilters() { document.querySelectorAll(`.${Config.CLASSES.cardRebuilt}`).forEach(card => UIBuilder.applyCensoredFilter(card)); }
        static applyAllHistoryVisibilities() { document.querySelectorAll(`.${Config.CLASSES.cardRebuilt}`).forEach(card => UIBuilder.applyHistoryVisibility(card)); }
    }

    // =============================================================================
    // 第二部分:基础处理器
    // =============================================================================
    class BaseListProcessor {
        constructor() {
            this.cardQueue = new Map();
            this.cache = new CacheManager();
            this.processQueueDebounced = Utils.debounce(() => this.processQueue(), Config.DEBOUNCE_DELAY);
        }
        init() {
            const targetNode = document.querySelector(this.getContainerSelector());
            if (!targetNode) return;
            PreviewManager.init(targetNode, `.${Config.CLASSES.processedCard}`);
            this.scanForCards(targetNode);
            new MutationObserver(mutations => {
                for (const m of mutations) for (const n of m.addedNodes) {
                    if (n.nodeType === 1) {
                        if (n.matches(this.getCardSelector())) this.processCard(n);
                        n.querySelectorAll(this.getCardSelector()).forEach(c => this.processCard(c));
                    }
                }
            }).observe(targetNode, { childList: true, subtree: true });
        }
        scanForCards(root = document) { root.querySelectorAll(this.getCardSelector()).forEach(c => this.processCard(c)); }
        async processQueue() {
            if (this.cardQueue.size === 0) return;
            const queue = new Map(this.cardQueue); this.cardQueue.clear();
            const toFetch = [];
            for (const [id, container] of queue.entries()) {
                const cached = this.cache.get(id);
                if (cached) this.updateCardUI(container, cached);
                else { toFetch.push(id); UIBuilder.toggleLoading(container, true); }
            }
            if (toFetch.length === 0) return;
            for (const chunk of Utils.chunk(toFetch, Config.NETWORK.CHUNK_SIZE)) {
                const results = await NetworkManager.fetchMagnetLinks(chunk);
                for (const id of chunk) {
                    const url = results.get(id) ?? null;
                    this.cache.set(id, url);
                    if (queue.has(id)) this.updateCardUI(queue.get(id), url);
                }
            }
            this.cache.save();
        }
        updateCardUI(container, magnetUrl) {
            UIBuilder.toggleLoading(container, false);
            if (magnetUrl) UIBuilder.addMagnetButton(container, magnetUrl);
            const card = container.closest(`.${Config.CLASSES.cardRebuilt}`);
            UIBuilder.applyCardVisibility(card, !!magnetUrl);
        }
        getContainerSelector() { throw new Error("Not implemented"); }
        getCardSelector() { throw new Error("Not implemented"); }
        processCard() { throw new Error("Not implemented"); }
    }
    class BaseDetailProcessor {
         constructor() { this.cache = new CacheManager(); }
        async addExtraPreviews() {
            if (!SettingsManager.get('loadExtraPreviews')) return;
            const fc2Id = Utils.extractFC2Id(location.pathname); if (!fc2Id) return;
            const anchor = document.querySelector(this.getPreviewAnchorSelector()); if (!anchor) return;
            const previewsData = await NetworkManager.fetchExtraPreviews(fc2Id);
            const previewsGrid = UIBuilder.createExtraPreviewsGrid(previewsData);
            if (previewsGrid) anchor.after(previewsGrid);
        }
        getPreviewAnchorSelector() { throw new Error("Not implemented"); }
    }

    // =============================================================================
    // 第三部分:针对特定网站的处理器
    // =============================================================================
    class FD2PPV_ListPageProcessor extends BaseListProcessor {
        getContainerSelector() { return 'body'; }
        getCardSelector() { return '.artist-card:not(.card-rebuilt):not(.other-work-item)'; }
        _extractCardData(card) {
            const link = card.querySelector('h3 a'); const img = card.querySelector('.work-photos img');
            const p = card.querySelector('p'); const mainLink = Array.from(card.querySelectorAll('a[href*="/articles/"]')).find(a => a.querySelector('img'));
            if (!link || !img || !mainLink) return null;
            const fc2Id = link.textContent.trim(); if (!/^\d{6,8}$/.test(fc2Id)) return null;
            return { fc2Id, title: p?.textContent.trim() ?? null, imageUrl: img.src, articleUrl: mainLink.href, };
        }
        processCard(card) {
            const data = this._extractCardData(card); if (!data) return;
            const icons = Array.from(card.querySelectorAll('.float[class*="free"]'));
            icons.sort((a, b) => Utils.getIconSortScore(a) - Utils.getIconSortScore(b));
            const preservedIconsHTML = icons.map(node => { const c = node.cloneNode(true); c.classList.remove('float', 'float-right', 'float-left'); return c.outerHTML; }).join('');
            const { finalElement, linksContainer, newCard } = UIBuilder.createEnhancedCard({ ...data, preservedIconsHTML });
            card.classList.add(Config.CLASSES.cardRebuilt); card.innerHTML = ''; card.appendChild(finalElement);
            if (newCard.classList.contains(Config.CLASSES.isCensored)) card.classList.add(Config.CLASSES.isCensored);
            if (newCard.classList.contains(Config.CLASSES.isViewed)) card.classList.add(Config.CLASSES.isViewed);
            UIBuilder.applyCardVisibility(card, false); UIBuilder.applyCensoredFilter(card); UIBuilder.applyHistoryVisibility(card);
            this.cardQueue.set(data.fc2Id, linksContainer); this.processQueueDebounced();
        }
    }
    class FD2PPV_ActressPageProcessor extends FD2PPV_ListPageProcessor {
        getContainerSelector() { return '.other-works-grid'; }
        getCardSelector() { return '.other-work-item.artist-card:not(.card-rebuilt)'; }
        _extractCardData(card) {
            const link = card.querySelector('.other-work-title a'); const img = card.querySelector('.work-photos img');
            if (!link || !img) return null; const fc2Id = link.textContent.trim(); if (!/^\d{6,8}$/.test(fc2Id)) return null;
            return { fc2Id, title: null, imageUrl: img.src, articleUrl: link.href };
        }
    }
    class FD2PPV_DetailPageProcessor extends BaseDetailProcessor {
        init() { this.processMainImage(); this.addExtraPreviews(); new FD2PPV_ActressPageProcessor().init(); }
        getPreviewAnchorSelector() { return '.artist-info-card'; }
        async processMainImage() {
            const mainCont = document.querySelector('.work-image-large.work-photos'); const titleEl = document.querySelector('h1.work-title');
            if (!mainCont || mainCont.classList.contains(Config.CLASSES.cardRebuilt) || !titleEl) return;
            const fc2Id = titleEl.firstChild?.textContent.trim(); const img = mainCont.querySelector('img');
            if (!fc2Id || !/^\d{6,8}$/.test(fc2Id) || !img) return;
            const { finalElement, linksContainer, newCard } = UIBuilder.createEnhancedCard({ fc2Id, title: null, imageUrl: img.src, articleUrl: null, preservedIconsHTML: null });
            const previewContainer = finalElement.querySelector(`.${Config.CLASSES.videoPreviewContainer}`);
            if (previewContainer && SettingsManager.get('previewMode') === 'autoplay') {
                 const video = PreviewManager._createVideoElement(`https://fourhoi.com/fc2-ppv-${fc2Id}/preview.mp4`, newCard);
                 previewContainer.appendChild(video); video.classList.remove(Config.CLASSES.hidden); img.classList.add(Config.CLASSES.hidden); video.play().catch(() => {});
            }
            mainCont.classList.add(Config.CLASSES.cardRebuilt); mainCont.innerHTML = ''; mainCont.appendChild(finalElement);
            PreviewManager.init(mainCont, `.${Config.CLASSES.processedCard}`);
            const cached = this.cache.get(fc2Id);
            if (cached) { if(cached) UIBuilder.addMagnetButton(linksContainer, cached); }
            else {
                UIBuilder.toggleLoading(linksContainer, true);
                const res = await NetworkManager.fetchMagnetLinks([fc2Id]);
                const url = res.get(fc2Id) ?? null; this.cache.set(fc2Id, url); this.cache.save();
                UIBuilder.toggleLoading(linksContainer, false);
                if (url) UIBuilder.addMagnetButton(linksContainer, url);
            }
        }
    }
    class FC2PPVDB_ListPageProcessor extends BaseListProcessor {
        getContainerSelector() { return '.container'; }
        getCardSelector() { return 'div.p-4:not(.card-rebuilt), div[class*="p-4"]:not(.card-rebuilt)'; }
        processCard(card) {
            if (!card.querySelector('a[href^="/articles/"]')) return;
            const link = card.querySelector('a[href^="/articles/"]'); const titleLink = card.querySelector('div.mt-1 a.text-white');
            const idSpan = card.querySelector('span.absolute.top-0.left-0'); const fc2Id = idSpan?.textContent.trim() ?? Utils.extractFC2Id(link.href);
            if (!fc2Id) return;
            let imageUrl = card.querySelector('img#ArticleImage')?.src;
            if (!imageUrl || imageUrl.includes('no-image')) imageUrl = `https://wumaobi.com/fc2daily/data/FC2-PPV-${fc2Id}/cover.jpg`;
            const title = titleLink?.textContent.trim() ?? `FC2-PPV-${fc2Id}`;
            const { finalElement, linksContainer, newCard } = UIBuilder.createEnhancedCard({ fc2Id, title, imageUrl, articleUrl: link.href, preservedIconsHTML: null });
            card.innerHTML = ''; card.appendChild(finalElement); card.classList.add(Config.CLASSES.cardRebuilt);
            if (newCard.classList.contains(Config.CLASSES.isViewed)) card.classList.add(Config.CLASSES.isViewed);
            UIBuilder.applyCardVisibility(card, false); UIBuilder.applyHistoryVisibility(card);
            this.cardQueue.set(fc2Id, linksContainer); this.processQueueDebounced();
        }
    }
    class FC2PPVDB_DetailPageProcessor extends BaseDetailProcessor {
        init() { this.waitForElementAndProcess(); this.addExtraPreviews(); this.observeConflict(); }
        getPreviewAnchorSelector() { return '.container'; }
        waitForElementAndProcess(retries = 10, interval = 500) {
            if (retries <= 0) return;
            const container = document.querySelector('div.lg\\:w-2\\/5') ?? document.getElementById('ArticleImage')?.closest('div') ?? document.getElementById('NoImage')?.closest('div');
            if (container && !container.classList.contains(Config.CLASSES.cardRebuilt)) this.processMainImage(container);
            else if (!container) setTimeout(() => this.waitForElementAndProcess(retries - 1, interval), interval);
        }
        async processMainImage(mainContainer) {
            const fc2Id = Utils.extractFC2Id(location.href); if (!fc2Id) return;
            let imageUrl = document.getElementById('ArticleImage')?.src;
            if (!imageUrl || imageUrl.includes('no-image')) imageUrl = `https://wumaobi.com/fc2daily/data/FC2-PPV-${fc2Id}/cover.jpg`;
            const { finalElement, linksContainer, newCard } = UIBuilder.createEnhancedCard({ fc2Id, title: null, imageUrl, articleUrl: null });
            const previewContainer = finalElement.querySelector(`.${Config.CLASSES.videoPreviewContainer}`);
            const img = previewContainer?.querySelector('img');
            if (previewContainer && img && SettingsManager.get('previewMode') === 'autoplay') {
                 const video = PreviewManager._createVideoElement(`https://fourhoi.com/fc2-ppv-${fc2Id}/preview.mp4`, newCard);
                 previewContainer.appendChild(video); video.classList.remove(Config.CLASSES.hidden); img.classList.add(Config.CLASSES.hidden); video.play().catch(() => {});
            }
            mainContainer.classList.add(Config.CLASSES.cardRebuilt); mainContainer.innerHTML = ''; mainContainer.appendChild(finalElement);
            PreviewManager.init(mainContainer, `.${Config.CLASSES.processedCard}`);
            const cached = this.cache.get(fc2Id);
            if (cached) { if (cached) UIBuilder.addMagnetButton(linksContainer, cached); }
            else {
                UIBuilder.toggleLoading(linksContainer, true);
                const res = await NetworkManager.fetchMagnetLinks([fc2Id]);
                const url = res.get(fc2Id) ?? null; this.cache.set(fc2Id, url); this.cache.save();
                UIBuilder.toggleLoading(linksContainer, false);
                if (url) UIBuilder.addMagnetButton(linksContainer, url);
            }
        }
        observeConflict() {
            new MutationObserver((_, obs) => {
                const img1 = document.getElementById('ArticleImage'); const img2 = document.getElementById('NoImage');
                if (img1 && img2) { img1.classList.remove('hidden'); img2.remove(); obs.disconnect(); }
            }).observe(document.body, { childList: true, subtree: true });
        }
    }

    // =============================================================================
    // 第四部分:启动器、菜单、设置面板和路由
    // =============================================================================

    class SettingsPanel {
        static panel = null; static backdrop = null; static statsRendered = false;
        static show() {
            if (!this.panel) this._createPanel();
            this.backdrop.style.display = 'block'; this.panel.style.display = 'flex';
            this._renderSettingsForm();
        }
        static hide() {
            if (this.panel) { this.backdrop.style.display = 'none'; this.panel.style.display = 'none'; }
        }
        static _createPanel() {
            this.backdrop = UIBuilder.createElement('div', { className: 'fc2-enh-settings-backdrop' });
            this.panel = UIBuilder.createElement('div', { className: 'fc2-enh-settings-panel' });
            this.panel.innerHTML = `
                <div class="fc2-enh-settings-header">
                    <h2>${t('settingsTitle')}</h2>
                    <button class="close-btn">&times;</button>
                </div>
                <div class="fc2-enh-settings-tabs">
                    <button class="fc2-enh-tab-btn active" data-tab="settings">${t('tabSettings')}</button>
                    <button class="fc2-enh-tab-btn" data-tab="statistics">${t('tabStatistics')}</button>
                </div>
                <div class="fc2-enh-settings-content">
                    <div id="tab-content-settings" class="fc2-enh-tab-content active"></div>
                    <div id="tab-content-statistics" class="fc2-enh-tab-content"></div>
                </div>
                <div id="settings-footer" class="fc2-enh-settings-footer">
                    <button class="fc2-enh-btn primary" id="fc2-enh-save-btn">${t('btnSaveAndApply')}</button>
                </div>
            `;
            document.body.append(this.backdrop, this.panel);
            this._addEventListeners();
        }
        static _renderSettingsForm() {
            const content = this.panel.querySelector('#tab-content-settings');
            content.innerHTML = `
                <div class="fc2-enh-settings-group">
                    <h3>${t('groupFilters')}</h3>
                    <div class="fc2-enh-form-row"><label for="setting-hideNoMagnet"><input type="checkbox" id="setting-hideNoMagnet"> ${t('optionHideNoMagnet')}</label></div>
                    ${location.hostname === 'fd2ppv.cc' ? `<div class="fc2-enh-form-row"><label for="setting-hideCensored"><input type="checkbox" id="setting-hideCensored"> ${t('optionHideCensored')}</label></div>` : ''}
                    <div class="fc2-enh-form-row"><label for="setting-hideViewed"><input type="checkbox" id="setting-hideViewed"> ${t('optionHideViewed')}</label></div>
                </div>
                <div class="fc2-enh-settings-group">
                    <h3>${t('groupAppearance')}</h3>
                    <div class="fc2-enh-form-row">
                        <label for="setting-previewMode">${t('labelPreviewMode')}</label>
                        <select id="setting-previewMode">
                            <option value="static">${t('previewModeStatic')}</option>
                            <option value="hover">${t('previewModeHover')}</option>
                            <option value="autoplay" hidden>Auto Play</option>
                        </select>
                    </div>
                    <div class="fc2-enh-form-row">
                        <label for="setting-cardLayoutMode">${t('labelCardLayout')}</label>
                        <select id="setting-cardLayoutMode"><option value="default">${t('layoutDefault')}</option><option value="compact">${t('layoutCompact')}</option></select>
                    </div>
                    <div class="fc2-enh-form-row">
                        <label for="setting-buttonStyle">${t('labelButtonStyle')}</label>
                        <select id="setting-buttonStyle"><option value="icon">${t('buttonStyleIcon')}</option><option value="text">${t('buttonStyleText')}</option></select>
                    </div>
                </div>
                <div class="fc2-enh-settings-group">
                    <h3>${t('groupDataHistory')}</h3>
                    <div class="fc2-enh-form-row"><label for="setting-enableHistory"><input type="checkbox" id="setting-enableHistory"> ${t('optionEnableHistory')}</label></div>
                    <div class="fc2-enh-form-row"><label for="setting-loadExtraPreviews"><input type="checkbox" id="setting-loadExtraPreviews"> ${t('optionLoadExtraPreviews')}</label></div>
                    <div class="fc2-enh-form-row"><label>${t('labelCacheManagement')}</label><button class="fc2-enh-btn" id="fc2-enh-clear-cache-btn">${t('btnClearCache')}</button></div>
                    <div class="fc2-enh-form-row"><label>${t('labelHistoryManagement')}</label><button class="fc2-enh-btn" id="fc2-enh-clear-history-btn">${t('btnClearHistory')}</button></div>
                </div>
            `;
            this.panel.querySelector('#setting-previewMode').value = SettingsManager.get('previewMode');
            this.panel.querySelector('#setting-hideNoMagnet').checked = SettingsManager.get('hideNoMagnet');
            if (location.hostname === 'fd2ppv.cc') this.panel.querySelector('#setting-hideCensored').checked = SettingsManager.get('hideCensored');
            this.panel.querySelector('#setting-hideViewed').checked = SettingsManager.get('hideViewed');
            this.panel.querySelector('#setting-cardLayoutMode').value = SettingsManager.get('cardLayoutMode');
            this.panel.querySelector('#setting-buttonStyle').value = SettingsManager.get('buttonStyle');
            this.panel.querySelector('#setting-loadExtraPreviews').checked = SettingsManager.get('loadExtraPreviews');
            this.panel.querySelector('#setting-enableHistory').checked = SettingsManager.get('enableHistory');
        }
        static _loadScript(url) {
            return new Promise((resolve, reject) => {
                if (document.querySelector(`script[src="${url}"]`)) return resolve();
                const script = document.createElement('script'); script.src = url;
                script.onload = () => resolve(); script.onerror = () => reject(new Error(`Failed to load script: ${url}`));
                document.head.appendChild(script);
            });
        }
        static async _renderStatistics() {
            if (this.statsRendered) return;
            const content = this.panel.querySelector('#tab-content-statistics');
            content.innerHTML = `
                <div class="fc2-enh-stats-overview">
                    <div class="fc2-enh-stat-card"><div id="stat-history-total" class="fc2-enh-stat-card-value">0</div><div class="fc2-enh-stat-card-label">${t('statTotalViews')}</div></div>
                    <div class="fc2-enh-stat-card"><div id="stat-cache-total" class="fc2-enh-stat-card-value">0</div><div class="fc2-enh-stat-card-label">${t('statCachedMagnets')}</div></div>
                    <div class="fc2-enh-stat-card"><div id="stat-cache-hits" class="fc2-enh-stat-card-value">0</div><div class="fc2-enh-stat-card-label">${t('statCacheHits')}</div></div>
                </div>
                <div class="fc2-enh-chart-container" id="activity-chart-wrapper">${t('chartLoading')}</div>
                <div class="fc2-enh-chart-container" id="cache-chart-wrapper">${t('chartLoading')}</div>
                <div id="achievements-placeholder"></div>
            `;
            const historyData = HistoryManager.getRawData();
            const cache = new CacheManager(); const cacheSize = cache.getSize(); const maxCacheSize = Config.CACHE_MAX_SIZE;
            const cacheStats = { hits: StatsTracker.get('cacheHits', 0) };
            AchievementManager.checkAll({ historyData, cacheStats });
            content.querySelector('#stat-history-total').textContent = historyData.length;
            content.querySelector('#stat-cache-total').textContent = cacheSize;
            content.querySelector('#stat-cache-hits').textContent = cacheStats.hits;
            this._renderAchievements(content.querySelector('#achievements-placeholder'), AchievementManager.getAll(), AchievementManager.getUnlockedIds());
            try {
                if (typeof Chart === 'undefined') await this._loadScript('https://cdn.jsdelivr.net/npm/chart.js');
                const activityWrapper = content.querySelector('#activity-chart-wrapper'); const cacheWrapper = content.querySelector('#cache-chart-wrapper');
                activityWrapper.innerHTML = '<canvas id="activityChart"></canvas>'; cacheWrapper.innerHTML = '<canvas id="cacheChart"></canvas>';
                Chart.defaults.color = '#a6adc8'; Chart.defaults.borderColor = 'rgba(205, 214, 244, 0.1)';
                this._renderActivityChart(content.querySelector('#activityChart'), historyData);
                this._renderCacheChart(content.querySelector('#cacheChart'), cacheSize, maxCacheSize);
            } catch (error) {
                content.querySelector('#activity-chart-wrapper').textContent = 'Chart loading failed.';
                content.querySelector('#cache-chart-wrapper').textContent = 'Chart loading failed.';
            }
            this.statsRendered = true;
        }
        static _renderAchievements(container, allAchievements, unlockedIds) {
            let gridHTML = `<div class="fc2-enh-achievements-container"><h3>${t('achievementsTitle')}</h3><div class="fc2-enh-achievements-grid">`;
            allAchievements.forEach(ach => {
                const isUnlocked = unlockedIds.has(ach.id);
                const statusClass = isUnlocked ? 'unlocked' : 'locked';
                gridHTML += `
                    <div class="fc2-enh-achievement-badge ${statusClass}" title="${t(ach.descriptionKey)}">
                        <div class="icon"><i class="fa-solid ${ach.icon}"></i></div>
                        <div class="details">
                            <div class="title">${t(ach.titleKey)}</div>
                            <div class="description">${isUnlocked ? t('statusUnlocked') : t('statusLocked')}</div>
                        </div>
                    </div>`;
            });
            gridHTML += `</div></div>`;
            container.innerHTML = gridHTML;
        }
        static _renderActivityChart(canvas, historyData) {
            if (!canvas) return;
            const activityData = new Map();
            for (let i = 29; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate() - i); activityData.set(d.toISOString().slice(0, 10), 0); }
            const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
            historyData.filter(item => item.timestamp >= thirtyDaysAgo.getTime()).forEach(item => {
                const dateStr = new Date(item.timestamp).toISOString().slice(0, 10);
                if (activityData.has(dateStr)) activityData.set(dateStr, activityData.get(dateStr) + 1);
            });
            new Chart(canvas.getContext('2d'), { type: 'line', data: { labels: [...activityData.keys()].map(d => d.slice(5)), datasets: [{ label: t('chartActivityLabel'), data: [...activityData.values()], borderColor: '#89b4fa', backgroundColor: 'rgba(137, 180, 250, 0.2)', fill: true, tension: 0.4 }] }, options: { responsive: true, plugins: { legend: { display: false }, title: { display: true, text: t('chartActivityTitle'), color: '#cdd6f4' }}} });
        }
        static _renderCacheChart(canvas, cacheSize, maxCacheSize) {
            if (!canvas) return;
            new Chart(canvas.getContext('2d'), { type: 'doughnut', data: { labels: [t('chartCacheUsed'), t('chartCacheFree')], datasets: [{ data: [cacheSize, Math.max(0, maxCacheSize - cacheSize)], backgroundColor: ['#89b4fa', 'rgba(0,0,0,0.3)'], borderColor: 'rgba(30, 30, 46, 0.8)', borderWidth: 4 }] }, options: { responsive: true, cutout: '70%', plugins: { legend: { position: 'bottom', labels: { color: '#cdd6f4' } }, title: { display: true, text: t('chartCacheTitle'), color: '#cdd6f4' }}} });
        }
        static _save() {
            const newSettings = {
                previewMode: this.panel.querySelector('#setting-previewMode').value,
                hideNoMagnet: this.panel.querySelector('#setting-hideNoMagnet').checked,
                hideViewed: this.panel.querySelector('#setting-hideViewed').checked,
                cardLayoutMode: this.panel.querySelector('#setting-cardLayoutMode').value,
                buttonStyle: this.panel.querySelector('#setting-buttonStyle').value,
                loadExtraPreviews: this.panel.querySelector('#setting-loadExtraPreviews').checked,
                enableHistory: this.panel.querySelector('#setting-enableHistory').checked,
            };
            if (location.hostname === 'fd2ppv.cc') newSettings.hideCensored = this.panel.querySelector('#setting-hideCensored').checked;
            Object.entries(newSettings).forEach(([key, value]) => SettingsManager.set(key, value));
            alert(t('alertSettingsSaved'));
            this.hide();
        }
        static _addEventListeners() {
            this.panel.querySelector('.close-btn').addEventListener('click', () => this.hide());
            this.backdrop.addEventListener('click', () => this.hide());
            this.panel.querySelector('#fc2-enh-save-btn').addEventListener('click', () => this._save());
            this.panel.querySelector('#tab-content-settings').addEventListener('click', e => {
                if (e.target.id === 'fc2-enh-clear-cache-btn') { new CacheManager().clear(); alert(t('alertCacheCleared')); this.statsRendered = false; }
                if (e.target.id === 'fc2-enh-clear-history-btn') { HistoryManager.clear(); alert(t('alertHistoryCleared')); this.statsRendered = false; }
            });
            this.panel.querySelectorAll('.fc2-enh-tab-btn').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    const tabName = e.target.dataset.tab;
                    this.panel.querySelectorAll('.fc2-enh-tab-btn, .fc2-enh-tab-content').forEach(el => el.classList.remove('active'));
                    e.target.classList.add('active');
                    this.panel.querySelector(`#tab-content-${tabName}`).classList.add('active');
                    this.panel.querySelector('#settings-footer').style.display = (tabName === 'settings') ? 'flex' : 'none';
                    if (tabName === 'statistics') this._renderStatistics();
                });
            });
        }
    }

    class MenuManager {
        static menuIds = [];
        static register() {
            this.menuIds.forEach(GM_unregisterMenuCommand); this.menuIds = [];
            this.menuIds.push(GM_registerMenuCommand(t('menuOpenSettings'), () => SettingsPanel.show()));
        }
    }

    class ProcessorFactory {
        static create(name) {
            const P = { FD2PPV_ListPageProcessor, FD2PPV_ActressPageProcessor, FD2PPV_DetailPageProcessor, FC2PPVDB_ListPageProcessor, FC2PPVDB_DetailPageProcessor };
            if (P[name]) return new P[name]();
            throw new Error(`Processor ${name} not found.`);
        }
    }

    function main() {
        Localization.init();
        StatsTracker.load();
        SettingsManager.load();
        HistoryManager.load();
        AchievementManager.load();
        StyleManager.inject();
        MenuManager.register();
        DynamicStyleApplier.init();
        document.body.classList.add(`layout-${SettingsManager.get('cardLayoutMode')}`);
        document.body.classList.add(`buttons-${SettingsManager.get('buttonStyle')}`);
        const siteConfig = Config.SITES[location.hostname]; if (!siteConfig) return;
        const route = siteConfig.routes.find(r => r.path.test(location.pathname));
        if (route) {
            try { ProcessorFactory.create(route.processor).init(); }
            catch (error) { console.error('Script execution error:', error); }
        }
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', main);
    else main();

})();