LeetCode题单助手

LeetCode中英文站点增强工具,智能转换链接并显示做题状态

当前为 2025-06-02 提交的版本,查看 最新版本

// ==UserScript==
// @name         LeetCode题单助手
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  LeetCode中英文站点增强工具,智能转换链接并显示做题状态
// @author       You
// @match        https://leetcode.cn/*
// @match        https://leetcode.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// @connect      leetcode.com
// @connect      leetcode.cn
// @license MIT
// ==/UserScript==

(function() {
    'use strict';
    
    const CONFIG = {
        STORAGE_KEY: 'leetcode_progress_data',
        LAST_SYNC_KEY: 'leetcode_last_sync',
        SETTINGS_KEY: 'leetcode_settings',
        CACHE_DURATION: 2 * 60 * 60 * 1000,
        VERSION: '3.0'
    };
    
    const DEFAULT_SETTINGS = {
        autoSync: true,
        showDifficulty: true,
        showProgress: true,
        compactMode: false,
        hideCompleted: false,
        customCSS: ''
    };
    
    const DIFFICULTY_MAP = {
        1: { text: 'Easy', color: '#00b8a3' },
        2: { text: 'Med.', color: '#ffc01e' },
        3: { text: 'Hard', color: '#ff375f' }
    };
    
    const STATUS_MAP = {
        'ac': { emoji: '✅', text: 'Solved', color: '#52c41a', bgColor: '#f6ffed' },
        'notac': { emoji: '❌', text: 'Attempted', color: '#ff4d4f', bgColor: '#fff2f0' },
        null: { emoji: '⭕', text: 'Not Attempted', color: '#8c8c8c', bgColor: '#fafafa' }
    };
    
    console.log('🚀 LeetCode题单助手启动,版本:', CONFIG.VERSION);
    
    GM_addStyle(`
        .lc-converted {
            padding-left: 8px !important;
            transition: all 0.3s ease;
        }
        
        .lc-converted:hover {
            background-color: #f0f8ff !important;
            transform: translateX(2px);
        }
        
        .lc-status-container {
            display: inline-flex;
            align-items: center;
            gap: 4px;
            margin-right: 8px;
        }
        
        .lc-status-badge {
            display: inline-flex;
            align-items: center;
            padding: 2px 6px;
            border-radius: 12px;
            font-size: 11px;
            font-weight: 500;
            border: 1px solid #d9d9d9;
            transition: transform 0.2s ease;
        }
        
        .lc-status-badge:hover {
            transform: scale(1.1);
        }
        
        .lc-difficulty-badge {
            display: inline-flex;
            align-items: center;
            padding: 2px 6px;
            border-radius: 4px;
            font-size: 11px;
            font-weight: bold;
            color: white;
            min-width: 32px;
            justify-content: center;
        }
        
        .lc-control-panel {
            position: fixed;
            top: 80px;
            right: 20px;
            z-index: 9999;
            background: #ffffff;
            color: #333333;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            overflow: hidden;
            min-width: 200px;
            transition: all 0.3s ease;
        }
        
        [data-theme="dark"] .lc-control-panel,
        .dark .lc-control-panel,
        body[class*="dark"] .lc-control-panel {
            background: #1f1f1f !important;
            color: #ffffff !important;
            border: 1px solid #333;
        }
        
        .lc-panel-header {
            background: linear-gradient(135deg, #1890ff, #40a9ff);
            color: white;
            padding: 12px 16px;
            font-weight: bold;
            font-size: 14px;
            display: flex;
            align-items: center;
            justify-content: space-between;
            cursor: pointer;
            user-select: none;
        }
        
        .lc-panel-toggle {
            font-size: 12px;
            opacity: 0.8;
            transition: transform 0.3s ease;
        }
        
        .lc-panel-toggle.collapsed {
            transform: rotate(180deg);
        }
        
        .lc-panel-content {
            padding: 12px;
            transition: all 0.3s ease;
            overflow: hidden;
        }
        
        .lc-control-panel.collapsed .lc-panel-content {
            max-height: 0;
            padding: 0 12px;
            opacity: 0;
        }
        
        .lc-control-panel:not(.collapsed) .lc-panel-content {
            max-height: 500px;
            opacity: 1;
        }
        
        .lc-button {
            width: 100%;
            padding: 8px 12px;
            margin-bottom: 8px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            transition: all 0.2s ease;
            display: flex;
            align-items: center;
            gap: 6px;
        }
        
        .lc-button-primary {
            background: #1890ff;
            color: white;
        }
        
        .lc-button-primary:hover {
            background: #40a9ff;
        }
        
        .lc-button-secondary {
            background: #f0f0f0;
            color: #333;
        }
        
        .lc-button-secondary:hover {
            background: #e0e0e0;
        }
        
        [data-theme="dark"] .lc-button-secondary,
        .dark .lc-button-secondary,
        body[class*="dark"] .lc-button-secondary {
            background: #333 !important;
            color: #fff !important;
        }
        
        [data-theme="dark"] .lc-button-secondary:hover,
        .dark .lc-button-secondary:hover,
        body[class*="dark"] .lc-button-secondary:hover {
            background: #444 !important;
        }
        
        .lc-stats {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 6px;
            margin: 8px 0;
            font-size: 10px;
        }
        
        .lc-stat-item {
            text-align: center;
            padding: 4px;
            border-radius: 4px;
            background: #f9f9f9;
            color: #333;
        }
        
        [data-theme="dark"] .lc-stat-item,
        .dark .lc-stat-item,
        body[class*="dark"] .lc-stat-item {
            background: #333 !important;
            color: #fff !important;
        }
        
        .lc-progress-bar {
            width: 100%;
            height: 6px;
            background: #f0f0f0;
            border-radius: 3px;
            overflow: hidden;
            margin: 8px 0;
        }
        
        .lc-progress-fill {
            height: 100%;
            background: linear-gradient(90deg, #52c41a, #73d13d);
            transition: width 0.3s ease;
        }
        
        .lc-settings {
            border-top: 1px solid #f0f0f0;
            margin-top: 8px;
            padding-top: 8px;
        }
        
        [data-theme="dark"] .lc-settings,
        .dark .lc-settings,
        body[class*="dark"] .lc-settings {
            border-top-color: #444 !important;
        }
        
        .lc-setting-item {
            display: flex;
            align-items: center;
            justify-content: space-between;
            margin-bottom: 6px;
            font-size: 11px;
            color: inherit;
        }
        
        .lc-switch {
            position: relative;
            width: 32px;
            height: 18px;
            background: #ccc;
            border-radius: 9px;
            cursor: pointer;
            transition: background 0.2s;
        }
        
        .lc-switch.active {
            background: #1890ff;
        }
        
        .lc-switch::after {
            content: '';
            position: absolute;
            top: 2px;
            left: 2px;
            width: 14px;
            height: 14px;
            background: white;
            border-radius: 50%;
            transition: transform 0.2s;
        }
        
        .lc-switch.active::after {
            transform: translateX(14px);
        }
        
        .lc-notification {
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 10000;
            background: white;
            border-left: 4px solid #52c41a;
            padding: 16px 20px;
            border-radius: 4px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            max-width: 300px;
            animation: slideIn 0.3s ease;
        }
        
        @keyframes slideIn {
            from { transform: translateX(100%); opacity: 0; }
            to { transform: translateX(0); opacity: 1; }
        }
        
        .lc-hidden {
            opacity: 0.3;
            filter: grayscale(50%);
        }
        
        .lc-jump-button {
            display: inline-flex;
            align-items: center;
            gap: 4px;
            padding: 4px 8px;
            margin-left: 12px;
            background: linear-gradient(135deg, #1890ff, #40a9ff);
            color: white;
            border: none;
            border-radius: 6px;
            font-size: 12px;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.2s ease;
            text-decoration: none;
            box-shadow: 0 2px 4px rgba(24, 144, 255, 0.3);
        }
        
        .lc-jump-button:hover {
            background: linear-gradient(135deg, #40a9ff, #69c0ff);
            transform: translateY(-1px);
            box-shadow: 0 4px 8px rgba(24, 144, 255, 0.4);
            color: white;
            text-decoration: none;
        }
        
        .lc-jump-button:active {
            transform: translateY(0);
        }
    `);
    
    class LeetCodeUltimate {
        constructor() {
            this.settings = { ...DEFAULT_SETTINGS, ...GM_getValue(CONFIG.SETTINGS_KEY, {}) };
            this.progressData = GM_getValue(CONFIG.STORAGE_KEY, {});
            this.isInternational = window.location.hostname === 'leetcode.com';
            this.isChinese = window.location.hostname === 'leetcode.cn';
            this.stats = { total: 0, solved: 0, attempted: 0 };
            
            this.init();
        }
        
        init() {
            if (this.isInternational) {
                this.handleInternationalSite();
            } else if (this.isChinese) {
                if (this.isProblemsDetailPage()) {
                    this.addJumpButton();
                    return;
                }
                this.handleChineseSite();
            }
            
            if (this.settings.customCSS) {
                GM_addStyle(this.settings.customCSS);
            }
        }
        
        isProblemsDetailPage() {
            const path = window.location.pathname;
            return /^\/problems\/[^\/]+\/(description|solutions|discuss|submissions|editorial)/.test(path) ||
                   /^\/problems\/[^\/]+\/$/.test(path);
        }
        
        addJumpButton() {
            setTimeout(() => {
                this.insertJumpButton();
            }, 1500);
            
            const observer = new MutationObserver(() => {
                if (!document.querySelector('.lc-jump-button')) {
                    setTimeout(() => this.insertJumpButton(), 500);
                }
            });
            
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }
        
        insertJumpButton() {
            if (document.querySelector('.lc-jump-button')) {
                return;
            }
            
            const titleSelectors = [
                'h1[data-cy="question-title"]',
                '.question-title h1',
                '.question-content h1',
                '.css-v3d350',
                '[class*="title"]'
            ];
            
            let titleElement = null;
            for (const selector of titleSelectors) {
                try {
                    titleElement = document.querySelector(selector);
                    if (titleElement) break;
                } catch (e) {
                    continue;
                }
            }
            
            if (!titleElement) {
                const h1Elements = document.querySelectorAll('h1');
                for (const h1 of h1Elements) {
                    const text = h1.textContent.trim();
                    if (/^\d+\./.test(text) && text.length > 5) {
                        titleElement = h1;
                        break;
                    }
                }
            }
            
            if (!titleElement) {
                return;
            }
            
            const currentUrl = window.location.href;
            const targetUrl = this.getTargetUrl(currentUrl);
            
            if (!targetUrl) {
                return;
            }
            
            const jumpButton = document.createElement('a');
            jumpButton.className = 'lc-jump-button';
            jumpButton.href = targetUrl;
            jumpButton.target = '_blank';
            jumpButton.rel = 'noopener noreferrer';
            
            if (this.isChinese) {
                jumpButton.innerHTML = `
                    <span>🌍</span>
                    <span>English</span>
                `;
                jumpButton.title = '在LeetCode国际站打开 / Open on LeetCode.com';
            } else {
                jumpButton.innerHTML = `
                    <span>🇨🇳</span>
                    <span>中文</span>
                `;
                jumpButton.title = '在LeetCode中文站打开 / Open on LeetCode.cn';
            }
            
            titleElement.appendChild(jumpButton);
        }
        
        getTargetUrl(currentUrl) {
            try {
                const url = new URL(currentUrl);
                const pathname = url.pathname;
                const search = url.search;
                const hash = url.hash;
                
                if (this.isChinese) {
                    return `https://leetcode.com${pathname}${search}${hash}`;
                } else if (this.isInternational) {
                    return `https://leetcode.cn${pathname}${search}${hash}`;
                }
                
                return null;
            } catch (e) {
                return null;
            }
        }
        
        handleInternationalSite() {
            if (this.isProblemsDetailPage()) {
                this.addJumpButton();
                return;
            }
            
            setTimeout(() => {
                this.createControlPanel();
                if (this.settings.autoSync) {
                    this.checkAndAutoSync();
                }
            }, 2000);
        }
        
        createControlPanel() {
            const panel = document.createElement('div');
            panel.className = 'lc-control-panel';
            panel.innerHTML = `
                <div class="lc-panel-header">
                    <div style="display: flex; align-items: center; gap: 8px;">
                        <span>📚</span>
                        <span>LeetCode Supporter / 题单助手</span>
                    </div>
                    <span class="lc-panel-toggle">▼</span>
                </div>
                <div class="lc-panel-content">
                    <button class="lc-button lc-button-primary" id="lc-sync-btn">
                        <span>📊</span>
                        <span>同步到中文站 / Sync to CN</span>
                    </button>
                    <button class="lc-button lc-button-secondary" id="lc-clear-btn">
                        <span>🗑️</span>
                        <span>清除缓存 / Clear Cache</span>
                    </button>
                    <div class="lc-stats" id="lc-stats">
                        <div class="lc-stat-item">
                            <div>📈 已同步 / Synced</div>
                            <div id="lc-sync-count">0</div>
                        </div>
                        <div class="lc-stat-item">
                            <div>⏰ 最后同步 / Last Sync</div>
                            <div id="lc-last-sync">未同步 / Not Synced</div>
                        </div>
                    </div>
                    <div class="lc-settings">
                        <div class="lc-setting-item">
                            <span>自动同步 / Auto Sync</span>
                            <div class="lc-switch ${this.settings.autoSync ? 'active' : ''}" data-setting="autoSync"></div>
                        </div>
                    </div>
                </div>
            `;
            
            document.body.appendChild(panel);
            this.bindInternationalEvents(panel);
            this.updateSyncStats();
            
            this.addPanelToggle(panel);
        }
        
        bindInternationalEvents(panel) {
            panel.querySelector('#lc-sync-btn').onclick = () => this.syncProgress();
            panel.querySelector('#lc-clear-btn').onclick = () => this.clearCache();
            
            panel.querySelectorAll('.lc-switch').forEach(sw => {
                sw.onclick = () => this.toggleSetting(sw.dataset.setting, sw);
            });
        }
        
        async checkAndAutoSync() {
            const lastSync = GM_getValue(CONFIG.LAST_SYNC_KEY, 0);
            if (Date.now() - lastSync > CONFIG.CACHE_DURATION) {
                setTimeout(() => this.syncProgress(), 3000);
            }
        }
        
        async syncProgress() {
            const button = document.querySelector('#lc-sync-btn');
            const originalHTML = button.innerHTML;
            button.innerHTML = '<span>⏳</span><span>同步中...</span>';
            button.disabled = true;
            
            try {
                let data = await this.fetchFromAPI();
                if (!data) {
                    data = await this.parseFromPage();
                }
                if (!data) {
                    data = await this.extractFromLocalStorage();
                }
                
                if (data && Object.keys(data).length > 0) {
                    this.saveProgressData(data);
                    button.innerHTML = '<span>✅</span><span>同步成功</span>';
                    this.showNotification('同步成功! / Sync Success!', `已同步 ${Object.keys(data).length} 个题目的状态 / Synced ${Object.keys(data).length} problems`, 'success');
                } else {
                    throw new Error('无法获取有效数据 / Cannot get valid data');
                }
                
            } catch (error) {
                console.error('❌ 同步失败:', error);
                button.innerHTML = '<span>❌</span><span>同步失败</span>';
                this.showNotification('同步失败 / Sync Failed', error.message || '同步过程中出现错误 / Error during sync', 'error');
            }
            
            setTimeout(() => {
                button.innerHTML = originalHTML;
                button.disabled = false;
                this.updateSyncStats();
            }, 2000);
        }
        
        async fetchFromAPI() {
            return new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: 'https://leetcode.com/api/problems/all/',
                    headers: {
                        'User-Agent': navigator.userAgent,
                        'Referer': 'https://leetcode.com/problemset/all/'
                    },
                    timeout: 10000,
                    onload: (response) => {
                        try {
                            if (response.status === 200) {
                                const apiData = JSON.parse(response.responseText);
                                if (apiData.stat_status_pairs) {
                                    const data = {};
                                    apiData.stat_status_pairs.forEach(item => {
                                        const slug = item.stat.question__title_slug;
                                        data[slug] = {
                                            titleSlug: slug,
                                            status: item.status,
                                            frontendId: item.stat.frontend_question_id,
                                            difficulty: item.difficulty?.level || null,
                                            title: item.stat.question__title,
                                            _raw_difficulty: item.difficulty
                                        };
                                    });
                                    
                                    resolve(data);
                                    return;
                                }
                            }
                            resolve(null);
                        } catch (e) {
                            resolve(null);
                        }
                    },
                    onerror: () => {
                        resolve(null);
                    }
                });
            });
        }
        
        async parseFromPage() {
            const data = {};
            
            await new Promise(resolve => setTimeout(resolve, 2000));
            
            const selectors = [
                'div[role="row"]',
                'tr[data-cy="question-row"]',
                '.question-list-table tbody tr'
            ];
            
            for (const selector of selectors) {
                const rows = document.querySelectorAll(selector);
                if (rows.length > 0) {
                    rows.forEach((row) => {
                        try {
                            const link = row.querySelector('a[href*="/problems/"]');
                            if (!link) return;
                            
                            const slug = link.href.match(/\/problems\/([^\/\?]+)/)?.[1];
                            if (!slug) return;
                            
                            let status = null;
                            const statusElements = row.querySelectorAll('[data-cy="status"], .text-green-s, .text-yellow, .status, svg');
                            statusElements.forEach(el => {
                                const text = el.textContent?.toLowerCase() || '';
                                const classList = el.classList?.toString() || '';
                                
                                if (text.includes('✓') || classList.includes('text-green') || classList.includes('solved')) {
                                    status = 'ac';
                                } else if (text.includes('?') || classList.includes('text-yellow') || classList.includes('attempted')) {
                                    status = 'notac';
                                }
                            });
                            
                            let difficulty = null;
                            const difficultySelectors = [
                                '[data-cy="difficulty"]',
                                '.difficulty',
                                '[class*="difficulty"]',
                                'span[class*="text-"]',
                                '.text-green-s', '.text-yellow', '.text-red'
                            ];
                            
                            difficultySelectors.forEach(selector => {
                                const diffEl = row.querySelector(selector);
                                if (diffEl && !difficulty) {
                                    const diffText = diffEl.textContent?.toLowerCase() || '';
                                    const classList = diffEl.classList?.toString().toLowerCase() || '';
                                    
                                    if (diffText.includes('easy') || classList.includes('green')) {
                                        difficulty = 1;
                                    } else if (diffText.includes('medium') || classList.includes('yellow')) {
                                        difficulty = 2;
                                    } else if (diffText.includes('hard') || classList.includes('red')) {
                                        difficulty = 3;
                                    }
                                }
                            });
                            
                            data[slug] = {
                                titleSlug: slug,
                                status: status,
                                difficulty: difficulty,
                                title: link.textContent.trim(),
                                _source: 'page_parse'
                            };
                            
                        } catch (e) {
                        }
                    });
                    
                    if (Object.keys(data).length > 0) {
                        return data;
                    }
                }
            }
            
            return null;
        }
        
        async extractFromLocalStorage() {
            try {
                const keys = Object.keys(localStorage);
                for (const key of keys) {
                    if (key.includes('leetcode') || key.includes('question')) {
                        try {
                            const value = JSON.parse(localStorage.getItem(key));
                            if (value && typeof value === 'object') {
                                if (value.questionData || value.questions || value.stat_status_pairs) {
                                }
                            }
                        } catch (e) {
                        }
                    }
                }
            } catch (e) {
            }
            return null;
        }
        
        saveProgressData(data) {
            GM_setValue(CONFIG.STORAGE_KEY, data);
            GM_setValue(CONFIG.LAST_SYNC_KEY, Date.now());
            console.log(`💾 保存了 ${Object.keys(data).length} 个题目的数据`);
        }
        
        clearCache() {
            GM_setValue(CONFIG.STORAGE_KEY, {});
            GM_setValue(CONFIG.LAST_SYNC_KEY, 0);
            this.updateSyncStats();
            this.showNotification('缓存已清除 / Cache Cleared', '所有同步数据已删除 / All sync data deleted', 'info');
        }
        
        updateSyncStats() {
            const data = GM_getValue(CONFIG.STORAGE_KEY, {});
            const lastSync = GM_getValue(CONFIG.LAST_SYNC_KEY, 0);
            
            const countEl = document.querySelector('#lc-sync-count');
            const timeEl = document.querySelector('#lc-last-sync');
            
            if (countEl) countEl.textContent = Object.keys(data).length;
            if (timeEl) {
                timeEl.textContent = lastSync ? 
                    new Date(lastSync).toLocaleTimeString() : '未同步 / Not Synced';
            }
        }
        
        handleChineseSite() {
            setTimeout(() => {
                this.createChinesePanel();
                this.processPage();
                this.observeChanges();
            }, 1500);
        }
        
        createChinesePanel() {
            const data = GM_getValue(CONFIG.STORAGE_KEY, {});
            const panel = document.createElement('div');
            panel.className = 'lc-control-panel';
            panel.innerHTML = `
                <div class="lc-panel-header">
                    <div style="display: flex; align-items: center; gap: 8px;">
                        <span>📚</span>
                        <span>LeetCode Supporter / 题单助手</span>
                    </div>
                    <span class="lc-panel-toggle">▼</span>
                </div>
                <div class="lc-panel-content">
                    <div class="lc-stats">
                        <div class="lc-stat-item">
                            <div>✅ 已解决 / Solved</div>
                            <div id="lc-solved">0</div>
                        </div>
                        <div class="lc-stat-item">
                            <div>❌ 尝试中 / Attempted</div>
                            <div id="lc-attempted">0</div>
                        </div>
                        <div class="lc-stat-item">
                            <div>⭕ 未尝试 / Not Attempted</div>
                            <div id="lc-not-attempted">0</div>
                        </div>
                        <div class="lc-stat-item">
                            <div>📊 总题数 / Total</div>
                            <div id="lc-total">0</div>
                        </div>
                    </div>
                    <div class="lc-progress-bar">
                        <div class="lc-progress-fill" id="lc-progress"></div>
                    </div>
                    <button class="lc-button lc-button-secondary" id="lc-refresh-btn">
                        <span>🔄</span>
                        <span>刷新状态 / Refresh</span>
                    </button>
                    <div class="lc-settings">
                        <div class="lc-setting-item">
                            <span>显示难度 / Show Difficulty</span>
                            <div class="lc-switch ${this.settings.showDifficulty ? 'active' : ''}" data-setting="showDifficulty"></div>
                        </div>
                        <div class="lc-setting-item">
                            <span>隐藏已完成 / Hide Solved</span>
                            <div class="lc-switch ${this.settings.hideCompleted ? 'active' : ''}" data-setting="hideCompleted"></div>
                        </div>
                    </div>
                    ${Object.keys(data).length === 0 ? `
                        <div style="text-align: center; margin: 12px 0; font-size: 11px; color: #999;">
                            <div>⚠️ 未找到同步数据 / No sync data found</div>
                            <a href="https://leetcode.com/problemset/all/" target="_blank" style="color: #1890ff;">
                                前往国际站同步 / Go to LeetCode.com to sync
                            </a>
                        </div>
                    ` : ''}
                </div>
            `;
            
            document.body.appendChild(panel);
            this.bindChineseEvents(panel);
            
            this.addPanelToggle(panel);
        }
        
        bindChineseEvents(panel) {
            panel.querySelector('#lc-refresh-btn')?.addEventListener('click', () => {
                this.processPage();
                this.showNotification('已刷新 / Refreshed', '状态显示已更新 / Status display updated', 'info');
            });
            
            panel.querySelectorAll('.lc-switch').forEach(sw => {
                sw.addEventListener('click', (e) => {
                    e.stopPropagation();
                    this.toggleSetting(sw.dataset.setting, sw);
                });
            });
        }
        
        processPage() {
            const data = GM_getValue(CONFIG.STORAGE_KEY, {});
            
            if (Object.keys(data).length === 0) {
                return;
            }
            
            document.querySelectorAll('.lc-status-container').forEach(el => el.remove());
            
            document.querySelectorAll('.lc-hidden').forEach(el => {
                el.classList.remove('lc-hidden');
            });
            
            this.processLinks(data);
            this.updateStats();
        }
        
        processLinks(data) {
            const links = Array.from(document.querySelectorAll('a[href*="/problems/"]'));
            
            if (links.length === 0) {
                return;
            }
            
            let processed = 0;
            
            links.forEach(link => {
                if (this.processLink(link, data)) {
                    processed++;
                }
            });
            
            if (processed > 0) {
                this.updateStats();
            }
        }
        
        processLink(linkElement, data) {
            const originalHref = linkElement.href;
            
            if (!originalHref.includes('/problems/')) {
                return false;
            }
            
            const slug = this.extractProblemSlug(originalHref);
            if (!slug) return false;
            
            if (originalHref.includes('leetcode.cn')) {
                linkElement.href = originalHref.replace('leetcode.cn', 'leetcode.com');
                linkElement.target = '_blank';
            }
            linkElement.classList.add('lc-converted');
            
            const problemData = data[slug];
            const status = problemData?.status;
            
            this.addEnhancedBadges(linkElement, problemData);
            
            if (this.settings.hideCompleted && status === 'ac') {
                const parentElement = linkElement.closest('tr, div[role="row"], li, .question-item, .problem-item');
                if (parentElement) {
                    parentElement.classList.add('lc-hidden');
                }
            }
            
            return true;
        }
        
        addEnhancedBadges(linkElement, problemData) {
            const existingContainer = linkElement.parentNode.querySelector('.lc-status-container');
            if (existingContainer) {
                existingContainer.remove();
            }
            
            const container = document.createElement('div');
            container.className = 'lc-status-container';
            
            if (this.settings.showDifficulty) {
                const difficulty = problemData?.difficulty;
                
                if (difficulty && DIFFICULTY_MAP[difficulty]) {
                    const diffInfo = DIFFICULTY_MAP[difficulty];
                    const diffBadge = document.createElement('span');
                    diffBadge.className = 'lc-difficulty-badge';
                    diffBadge.textContent = diffInfo.text;
                    diffBadge.title = `难度: ${diffInfo.text}`;
                    diffBadge.style.backgroundColor = diffInfo.color;
                    diffBadge.style.color = 'white';
                    
                    container.appendChild(diffBadge);
                }
            }
            
            const status = problemData?.status;
            const statusInfo = STATUS_MAP[status] || STATUS_MAP[null];
            
            const statusBadge = document.createElement('span');
            statusBadge.className = 'lc-status-badge';
            statusBadge.textContent = statusInfo.text;
            statusBadge.title = `状态: ${statusInfo.text} - ${problemData?.titleSlug || 'unknown'}`;
            statusBadge.style.backgroundColor = statusInfo.bgColor;
            statusBadge.style.color = statusInfo.color;
            statusBadge.style.borderColor = statusInfo.color;
            
            container.appendChild(statusBadge);
            
            linkElement.parentNode.insertBefore(container, linkElement);
        }
        
        updateStats() {
            const data = GM_getValue(CONFIG.STORAGE_KEY, {});
            const links = document.querySelectorAll('.lc-converted');
            
            this.stats = { total: 0, solved: 0, attempted: 0, notAttempted: 0 };
            
            links.forEach(link => {
                const slug = this.extractProblemSlug(link.href);
                const problemData = data[slug];
                if (problemData) {
                    this.stats.total++;
                    if (problemData.status === 'ac') {
                        this.stats.solved++;
                    } else if (problemData.status === 'notac') {
                        this.stats.attempted++;
                    } else {
                        this.stats.notAttempted++;
                    }
                }
            });
            
            const solvedEl = document.querySelector('#lc-solved');
            const attemptedEl = document.querySelector('#lc-attempted');
            const notAttemptedEl = document.querySelector('#lc-not-attempted');
            const totalEl = document.querySelector('#lc-total');
            const progressEl = document.querySelector('#lc-progress');
            
            if (solvedEl) solvedEl.textContent = this.stats.solved;
            if (attemptedEl) attemptedEl.textContent = this.stats.attempted;
            if (notAttemptedEl) notAttemptedEl.textContent = this.stats.notAttempted;
            if (totalEl) totalEl.textContent = this.stats.total;
            if (progressEl && this.stats.total > 0) {
                const percentage = (this.stats.solved / this.stats.total) * 100;
                progressEl.style.width = percentage + '%';
            }
        }
        
        observeChanges() {
            let isProcessing = false;
            
            const observer = new MutationObserver((mutations) => {
                if (isProcessing) return;
                
                let shouldProcess = false;
                
                mutations.forEach(mutation => {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === 1) {
                            if (node.classList?.contains('lc-status-container') ||
                                node.classList?.contains('lc-control-panel') ||
                                node.classList?.contains('lc-notification') ||
                                node.closest?.('.lc-status-container, .lc-control-panel, .lc-notification')) {
                                return;
                            }
                            
                            if (node.querySelector?.('a[href*="/problems/"]:not(.lc-converted)') ||
                                (node.matches?.('a[href*="/problems/"]') && !node.classList.contains('lc-converted'))) {
                                shouldProcess = true;
                            }
                        }
                    });
                });
                
                if (shouldProcess) {
                    isProcessing = true;
                    setTimeout(() => {
                        this.processPage();
                        isProcessing = false;
                    }, 1000);
                }
            });
            
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }
        
        extractProblemSlug(url) {
            const match = url.match(/\/problems\/([^\/\?]+)/);
            return match ? match[1] : null;
        }
        
        toggleSetting(key, switchEl) {
            this.settings[key] = !this.settings[key];
            switchEl.classList.toggle('active', this.settings[key]);
            GM_setValue(CONFIG.SETTINGS_KEY, this.settings);
            
            if (key === 'showDifficulty' || key === 'hideCompleted') {
                this.processPage();
            }
            
            const settingMessages = {
                'showDifficulty': {
                    on: '显示难度 / Show Difficulty',
                    off: '隐藏难度 / Hide Difficulty'
                },
                'hideCompleted': {
                    on: '隐藏已完成 / Hide Solved',
                    off: '显示已完成 / Show Solved'
                },
                'autoSync': {
                    on: '自动同步 / Auto Sync',
                    off: '手动同步 / Manual Sync'
                }
            };
            
            const message = settingMessages[key];
            if (message) {
                const statusText = this.settings[key] ? message.on : message.off;
                this.showNotification('设置已更新 / Settings Updated', statusText, 'info');
            }
        }
        
        showNotification(title, message, type = 'info') {
            const colors = {
                success: '#52c41a',
                error: '#ff4d4f',
                info: '#1890ff',
                warning: '#faad14'
            };
            
            const notification = document.createElement('div');
            notification.className = 'lc-notification';
            notification.style.borderLeftColor = colors[type];
            notification.innerHTML = `
                <div style="font-weight: bold; margin-bottom: 4px;">${title}</div>
                <div style="font-size: 12px; color: #666;">${message}</div>
            `;
            
            document.body.appendChild(notification);
            
            setTimeout(() => {
                notification.style.animation = 'slideIn 0.3s ease reverse';
                setTimeout(() => notification.remove(), 300);
            }, 3000);
        }
        
        addPanelToggle(panel) {
            const header = panel.querySelector('.lc-panel-header');
            const toggle = panel.querySelector('.lc-panel-toggle');
            
            const isCollapsed = GM_getValue('panel_collapsed', false);
            if (isCollapsed) {
                panel.classList.add('collapsed');
                toggle.classList.add('collapsed');
            }
            
            header.addEventListener('click', () => {
                const collapsed = panel.classList.toggle('collapsed');
                toggle.classList.toggle('collapsed', collapsed);
                
                GM_setValue('panel_collapsed', collapsed);
            });
        }
        
        initHotkeys() {
            document.addEventListener('keydown', (e) => {
                if (e.ctrlKey && e.shiftKey && e.key === 'L') {
                    e.preventDefault();
                    if (this.isInternational) {
                        this.syncProgress();
                    } else {
                        this.processPage();
                    }
                }
                
                if (e.ctrlKey && e.shiftKey && e.key === 'H') {
                    e.preventDefault();
                    if (this.isChinese) {
                        const switchEl = document.querySelector('[data-setting="hideCompleted"]');
                        if (switchEl) {
                            this.toggleSetting('hideCompleted', switchEl);
                        }
                    }
                }
            });
        }
    }
    
    function initialize() {
        const app = new LeetCodeUltimate();
        
        app.initHotkeys();
        
        const lastVersion = GM_getValue('last_version', '');
        if (lastVersion !== CONFIG.VERSION) {
            GM_setValue('last_version', CONFIG.VERSION);
            if (lastVersion) {
                app.showNotification(
                    '更新完成!', 
                    `LeetCode题单助手已更新到 v${CONFIG.VERSION}`, 
                    'success'
                );
            }
        }
    }
    
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        setTimeout(initialize, 500);
    }
    
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址