LDStatus 手机版

在 Linux.do 页面显示信任级别进度 (面板优化, 字体统一为12px, 版本号移位)

// ==UserScript==
// @name         LDStatus 手机版
// @namespace    http://tampermonkey.net/
// @version      3.6.1
// @description  在 Linux.do 页面显示信任级别进度 (面板优化, 字体统一为12px, 版本号移位)
// @author       1e0n (modified by AI)
// @match        https://linux.do/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @license MIT
// @grant        GM_info
// @connect      connect.linux.do
// @connect      github.com
// @connect      raw.githubusercontent.com
// @updateURL
// @downloadURL
// ==/UserScript==

(function() {
    'use strict';
    console.log("LDStatus (v3.6.1) script started!");

    // 创建样式
    const style = document.createElement('style');
    style.textContent = `
        /* 目标宽度: 126px */
        /* 目标基础字体: 12px */

        /* 深色主题 */
        #ld-trust-level-panel.ld-dark-theme {
            background-color: #2d3748;
            color: #e2e8f0;
            box-shadow: 0 0 6px rgba(0, 0, 0, 0.4);
        }
        #ld-trust-level-panel.ld-dark-theme #ld-trust-level-header {
            background-color: #1a202c;
            color: white;
        }
        #ld-trust-level-panel.ld-dark-theme .ld-toggle-btn,
        #ld-trust-level-panel.ld-dark-theme .ld-refresh-btn,
        #ld-trust-level-panel.ld-dark-theme .ld-update-btn,
        #ld-trust-level-panel.ld-dark-theme .ld-theme-btn {
            color: white;
        }
        #ld-trust-level-panel.ld-dark-theme .ld-version {
            color: #a0aec0;
        }
        #ld-trust-level-panel.ld-dark-theme .ld-trust-level-item.ld-success .ld-value { color: #68d391; }
        #ld-trust-level-panel.ld-dark-theme .ld-trust-level-item.ld-fail .ld-value { color: #fc8181; }
        #ld-trust-level-panel.ld-dark-theme .ld-loading { color: #a0aec0; }

        /* 亮色主题 */
        #ld-trust-level-panel.ld-light-theme {
            background-color: #ffffff;
            color: #1a202c;
            box-shadow: 0 0 6px rgba(0, 0, 0, 0.15);
            border: 1px solid #e2e8f0;
        }
        #ld-trust-level-panel.ld-light-theme #ld-trust-level-header {
            background-color: #3182ce;
            color: #ffffff;
            border-bottom: 1px solid #2c5282;
        }
        #ld-trust-level-panel.ld-light-theme .ld-toggle-btn,
        #ld-trust-level-panel.ld-light-theme .ld-refresh-btn,
        #ld-trust-level-panel.ld-light-theme .ld-update-btn,
        #ld-trust-level-panel.ld-light-theme .ld-theme-btn {
            color: white;
            text-shadow: 0 0 1px rgba(0,0,0,0.3);
        }
        #ld-trust-level-panel.ld-light-theme .ld-version { color: #4a5568; }
        #ld-trust-level-panel.ld-light-theme .ld-trust-level-item.ld-success .ld-value { color: #276749; font-weight: bold; }
        #ld-trust-level-panel.ld-light-theme .ld-trust-level-item.ld-fail .ld-value { color: #c53030; font-weight: bold; }
        #ld-trust-level-panel.ld-light-theme .ld-name { color: #2d3748; }
        #ld-trust-level-panel.ld-light-theme .ld-loading { color: #4a5568; }

        /* 共用样式 */
        #ld-trust-level-panel {
            position: fixed;
            left: 10px;
            top: 100px;
            width: 126px;
            border-radius: 6px;
            z-index: 9999;
            font-family: Arial, sans-serif;
            transition: all 0.3s ease;
            overflow: hidden;
            font-size: 12px; /* Base font size */
        }

        #ld-trust-level-header {
            padding: 2px 6px;
            cursor: move;
            user-select: none;
        }

        .ld-header-top-line {

            display: flex;
            justify-content: space-between;
            align-items: center;
            width: 100%;
        }

        .ld-header-title {
            font-weight: bold;
            white-space: nowrap;
            margin-right: 5px;
        }

        .ld-header-second-line {
            display: flex;
            justify-content: space-between;
            align-items: center;
            width: 100%;
            margin-top: 1px;
        }

        .ld-header-button-bar {
            display: flex;
            align-items: center;
        }

        #ld-trust-level-content {
            padding: 3px 6px;
            max-height: none;
            overflow-y: visible;
        }

        .ld-trust-level-item { margin-bottom: 4px; }
        .ld-trust-level-item .ld-name {
            display: block;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            max-width: 100%;
            margin-bottom: 2px;
        }
        .ld-trust-level-item .ld-value {
            display: block;
            font-weight: bold;
            text-align: left;
        }

        .ld-toggle-btn, .ld-refresh-btn, .ld-update-btn, .ld-theme-btn {
            background: none;
            border: none;
            cursor: pointer;
            font-size: 12px;
            padding: 2px;
        }
        .ld-toggle-btn { margin-left: 3px; }
        .ld-header-button-bar button { margin-left: 3px; }
        .ld-header-button-bar button:first-child { margin-left: 0; }


        .ld-version {
            font-size: 12px;
            font-weight: normal;
        }

        /* Collapsed state styles */
        .ld-collapsed {
            width: 24px !important; height: 24px !important;
            min-width: 24px !important; max-width: 24px !important;
            border-radius: 6px; overflow: hidden; transform: none !important;
        }
        .ld-collapsed #ld-trust-level-header {
            justify-content: center; width: 24px !important; height: 24px !important;
            min-width: 24px !important; max-width: 24px !important;
            padding: 0; display: flex; align-items: center;
        }
        .ld-collapsed #ld-trust-level-content { display: none !important; }
        .ld-collapsed .ld-header-title,
        .ld-collapsed .ld-header-second-line {
            display: none !important;
        }
        .ld-collapsed .ld-toggle-btn {
            margin: 0; font-size: 12px;
            display: flex;
            justify-content: center; align-items: center;
            width: 100%; height: 100%;
        }
        /* End Collapsed state styles */

        .ld-loading { text-align: center; padding: 6px; }
        .ld-dark-theme .ld-increase { color: #ffd700; }
        .ld-dark-theme .ld-decrease { color: #4299e1; }
        .ld-light-theme .ld-increase { color: #d69e2e; font-weight: bold; }
        .ld-light-theme .ld-decrease { color: #2b6cb0; font-weight: bold; }
    `;
    if (document.head) {
        document.head.appendChild(style);
    } else {
        document.addEventListener('DOMContentLoaded', () => document.head.appendChild(style));
    }

    const STORAGE_KEY_PREFIX = 'ld_panel_v3_';
    const STORAGE_KEY_POSITION = STORAGE_KEY_PREFIX + 'position';
    const STORAGE_KEY_COLLAPSED = STORAGE_KEY_PREFIX + 'collapsed';
    const STORAGE_KEY_THEME = STORAGE_KEY_PREFIX + 'theme';
    const STORAGE_KEY_PREVIOUS_REQ = STORAGE_KEY_PREFIX + 'previous_requirements';

    const panel = document.createElement('div');
    panel.id = 'ld-trust-level-panel';

    const currentTheme = GM_getValue(STORAGE_KEY_THEME, 'dark');
    panel.classList.add(currentTheme === 'dark' ? 'ld-dark-theme' : 'ld-light-theme');

    let scriptVersion = "N/A";
    if (typeof GM_info !== 'undefined' && GM_info.script) {
        scriptVersion = GM_info.script.version || "N/A";
    }

    const header = document.createElement('div');
    header.id = 'ld-trust-level-header';
    header.innerHTML = `
        <div class="ld-header-top-line">
            <span class="ld-header-title">Status</span>
            <button class="ld-toggle-btn" title="展开/收起">◀</button>
        </div>
        <div class="ld-header-second-line">
            <span class="ld-version">v${scriptVersion}</span>
            <div class="ld-header-button-bar">
                <button class="ld-update-btn" title="检查更新">🔎</button>
                <button class="ld-refresh-btn" title="刷新数据">🔄</button>
                <button class="ld-theme-btn" title="切换主题">🌙</button>
            </div>
        </div>
    `;

    const content = document.createElement('div');
    content.id = 'ld-trust-level-content';
    content.innerHTML = '<div class="ld-loading">加载中...</div>';

    panel.appendChild(header);
    panel.appendChild(content);

    if (document.body) {
        document.body.appendChild(panel);
    } else {
        document.addEventListener('DOMContentLoaded', () => document.body.appendChild(panel));
    }

    let toggleBtn, refreshBtn, updateBtn, themeBtn, versionSpan;

    function queryHeaderElements() {
        toggleBtn = header ? header.querySelector('.ld-toggle-btn') : null;
        versionSpan = header ? header.querySelector('.ld-version') : null;
        const buttonBar = header ? header.querySelector('.ld-header-button-bar') : null;
        if (buttonBar) {
            updateBtn = buttonBar.querySelector('.ld-update-btn');
            refreshBtn = buttonBar.querySelector('.ld-refresh-btn');
            themeBtn = buttonBar.querySelector('.ld-theme-btn');
        }
    }
    queryHeaderElements();


    function savePanelPosition() {
        try {
            const transform = window.getComputedStyle(panel).transform;
            if (transform && transform !== 'none') {
                const matrix = new DOMMatrixReadOnly(transform);
                GM_setValue(STORAGE_KEY_POSITION, { x: matrix.e, y: matrix.f });
            }
        } catch (e) { console.error("Error saving panel position:", e); }
    }

    function savePanelCollapsedState() {
        try {
            GM_setValue(STORAGE_KEY_COLLAPSED, panel.classList.contains('ld-collapsed'));
        } catch (e) { console.error("Error saving panel collapsed state:", e); }
    }

    function restorePanelState() {
        try {
            const isCollapsed = GM_getValue(STORAGE_KEY_COLLAPSED, false);
            if (isCollapsed) {
                panel.classList.add('ld-collapsed');
                if (toggleBtn) toggleBtn.textContent = '▶';
            } else {
                panel.classList.remove('ld-collapsed');
                if (toggleBtn) toggleBtn.textContent = '◀';
            }

            const position = GM_getValue(STORAGE_KEY_POSITION, null);
            if (position && typeof position.x === 'number' && typeof position.y === 'number') {
                panel.style.transform = `translate(${position.x}px, ${position.y}px)`;
            } else {
                panel.style.left = '10px';
                panel.style.top = '100px';
                panel.style.transform = '';
            }
        } catch (e) {
            console.error("Error restoring panel state:", e);
            panel.classList.remove('ld-collapsed');
            if (toggleBtn) toggleBtn.textContent = '◀';
            panel.style.left = '10px';
            panel.style.top = '100px';
            panel.style.transform = '';
        }
    }

    let isDragging = false;
    let lastX, lastY;

    if (header) {
        header.addEventListener('mousedown', (e) => {
            if (panel.classList.contains('ld-collapsed') || e.target.closest('button')) {
                return;
            }
            isDragging = true;
            const currentTransform = window.getComputedStyle(panel).transform;
            const matrix = new DOMMatrixReadOnly(currentTransform === 'none' ? '' : currentTransform);
            lastX = e.clientX - matrix.e;
            lastY = e.clientY - matrix.f;
            panel.style.transition = 'none';
            document.body.style.userSelect = 'none';
        });
    }

    document.addEventListener('mousemove', (e) => {
        if (!isDragging) return;
        const newX = e.clientX - lastX;
        const newY = e.clientY - lastY;
        panel.style.transform = `translate(${newX}px, ${newY}px)`;
    });

    document.addEventListener('mouseup', () => {
        if (!isDragging) return;
        isDragging = false;
        panel.style.transition = '';
        document.body.style.userSelect = '';
        savePanelPosition();
    });

    if (toggleBtn) {
        toggleBtn.addEventListener('click', () => {
            panel.classList.toggle('ld-collapsed');
            toggleBtn.textContent = panel.classList.contains('ld-collapsed') ? '▶' : '◀';
            savePanelCollapsedState();
        });
    }

    if (refreshBtn) refreshBtn.addEventListener('click', fetchTrustLevelData);
    if (updateBtn) updateBtn.addEventListener('click', checkForUpdates);
    if (themeBtn) themeBtn.addEventListener('click', toggleTheme);

    function toggleTheme() {
        const isDarkTheme = panel.classList.contains('ld-dark-theme');
        panel.classList.remove(isDarkTheme ? 'ld-dark-theme' : 'ld-light-theme');
        panel.classList.add(isDarkTheme ? 'ld-light-theme' : 'ld-dark-theme');
        GM_setValue(STORAGE_KEY_THEME, isDarkTheme ? 'light' : 'dark');
        updateThemeButtonIcon();
    }

    function updateThemeButtonIcon() {
        if (!themeBtn) return;
        const isCurrentlyDark = panel.classList.contains('ld-dark-theme');
        themeBtn.textContent = isCurrentlyDark ? '🌙' : '☀️';
        themeBtn.title = isCurrentlyDark ? '切换为亮色主题' : '切换为深色主题';
    }


    function checkForUpdates() {
        if (!updateBtn) return;
        const currentScriptVersion = (typeof GM_info !== 'undefined' && GM_info.script) ? GM_info.script.version : '0';
        const updateMetaURL = (typeof GM_info !== 'undefined' && GM_info.script) ? GM_info.script.updateURL || GM_info.script.downloadURL : null;
        const downloadURL = (typeof GM_info !== 'undefined' && GM_info.script) ? GM_info.script.downloadURL || GM_info.script.updateURL : 'https://raw.githubusercontent.com/1e0n/LinuxDoStatus/master/LDStatus.user.js';

        if (!updateMetaURL) {
            updateBtn.textContent = '⚠️';
            updateBtn.title = '无法获取更新信息URL';
            return;
        }
        updateBtn.textContent = '⌛';
        updateBtn.title = '正在检查更新...';
        GM_xmlhttpRequest({
            method: 'GET',
            url: updateMetaURL,
            onload: function(response) {
                if (response.status === 200) {
                    const versionMatch = response.responseText.match(/@version\s+([\d\.]+)/);
                    if (versionMatch && versionMatch[1]) {
                        const remoteVersion = versionMatch[1];
                        if (remoteVersion > currentScriptVersion) {
                            updateBtn.textContent = '⚠️';
                            updateBtn.title = `发现新版本 v${remoteVersion},点击前往更新页面`;
                            updateBtn.style.color = '#ffd700';
                            updateBtn.onclick = function() { window.open(downloadURL, '_blank'); };
                        } else {
                            updateBtn.textContent = '✔';
                            updateBtn.title = '已是最新版本';
                            updateBtn.style.color = '#68d391';
                            setTimeout(() => {
                                updateBtn.textContent = '🔎';
                                updateBtn.title = '检查更新';
                                updateBtn.style.color = '';
                                updateBtn.onclick = checkForUpdates;
                            }, 3000);
                        }
                    } else { handleUpdateError("无法解析远程版本号"); }
                } else { handleUpdateError(`请求失败: ${response.status}`); }
            },
            onerror: function(error) { handleUpdateError(`网络错误: ${error.error}`);}
        });
        function handleUpdateError(message = '检查更新失败') {
            updateBtn.textContent = '❌';
            updateBtn.title = message;
            updateBtn.style.color = '#fc8181';
            setTimeout(() => {
                updateBtn.textContent = '🔎';
                updateBtn.title = '检查更新';
                updateBtn.style.color = '';
            }, 3000);
        }
    }

    let previousRequirements = [];
    try {
        const storedReqs = GM_getValue(STORAGE_KEY_PREVIOUS_REQ, null);
        if (storedReqs) previousRequirements = JSON.parse(storedReqs);
    } catch (e) {
        console.error("Error parsing stored previousRequirements:", e);
        GM_setValue(STORAGE_KEY_PREVIOUS_REQ, null);
    }

    function fetchTrustLevelData() {
        if (!content) {
            console.error("Content element not found for fetchTrustLevelData");
            return;
        }
        content.innerHTML = '<div class="ld-loading">加载中...</div>';
        GM_xmlhttpRequest({
            method: 'GET',
            url: 'https://connect.linux.do',
            timeout: 15000,
            onload: function(response) {
                if (response.status === 200) {
                    parseTrustLevelData(response.responseText);
                } else {
                    content.innerHTML = `<div class="ld-loading">获取数据失败 (${response.status})</div>`;
                }
            },
            onerror: function(error) {
                content.innerHTML = `<div class="ld-loading">获取数据失败 (网络错误: ${error.error || 'Unknown'})</div>`;
            },
            ontimeout: function() {
                content.innerHTML = '<div class="ld-loading">获取数据超时</div>';
            }
        });
    }

    function parseTrustLevelData(html) {
        try {
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');
            const trustLevelSection = Array.from(doc.querySelectorAll('.bg-white.p-6.rounded-lg')).find(div => {
                const heading = div.querySelector('h2');
                return heading && heading.textContent.includes('信任级别');
            });

            if (!trustLevelSection) {
                content.innerHTML = '<div class="ld-loading">未找到信任级别数据 (请登录 connect.linux.do)</div>';
                return;
            }

            const heading = trustLevelSection.querySelector('h2').textContent.trim();
            const match = heading.match(/(.*) - 信任级别 (\d+) 的要求/);
            const username = match ? match[1] : '未知用户';
            const targetLevel = match ? match[2] : '未知';

            const tableRows = trustLevelSection.querySelectorAll('table tr');
            const currentRequirements = [];

            for (let i = 1; i < tableRows.length; i++) {
                const row = tableRows[i];
                const cells = row.querySelectorAll('td');
                if (cells.length >= 3) {
                    const name = cells[0].textContent.trim();
                    const current = cells[1].textContent.trim();
                    const required = cells[2].textContent.trim();
                    const isSuccess = cells[1].classList.contains('text-green-500');
                    const currentMatch = current.match(/(\d+)/);
                    const currentValue = currentMatch ? parseInt(currentMatch[1], 10) : 0;

                    let changeValue = 0;
                    let hasChanged = false;
                    if (previousRequirements && previousRequirements.length > 0) {
                        const prevReq = previousRequirements.find(pr => pr.name === name);
                        if (prevReq) {
                            if (currentValue !== prevReq.currentValue) {
                                changeValue = currentValue - prevReq.currentValue;
                                hasChanged = true;
                            } else if (prevReq.hasChanged && typeof prevReq.changeValue === 'number') {
                                changeValue = prevReq.changeValue;
                                hasChanged = true;
                            }
                        }
                    }
                    currentRequirements.push({ name, current, required, isSuccess, currentValue, changeValue, hasChanged });
                }
            }

            const resultText = trustLevelSection.querySelector('p.text-red-500, p.text-green-500');
            const isMeetingRequirements = resultText ? !resultText.classList.contains('text-red-500') : false;

            renderTrustLevelData(username, targetLevel, currentRequirements, isMeetingRequirements);

            previousRequirements = currentRequirements.map(r => ({ name: r.name, currentValue: r.currentValue, changeValue: r.changeValue, hasChanged: r.hasChanged }));
            GM_setValue(STORAGE_KEY_PREVIOUS_REQ, JSON.stringify(previousRequirements));
        } catch (e) {
            console.error("Error parsing trust level data:", e);
            content.innerHTML = '<div class="ld-loading">解析数据时出错</div>';
        }
    }

    function renderTrustLevelData(username, targetLevel, requirements, isMeetingRequirements) {
        let html = `
            <div style="margin-bottom: 3px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${username} - 信任级别 ${targetLevel}">
                ${username} - TL${targetLevel}
            </div>
            <div style="margin-bottom: 1px; ${isMeetingRequirements ? 'color: #68d391' : 'color: #fc8181'}; font-size: 12px;">
                ${isMeetingRequirements ? '已符合' : '未符合'}要求
            </div>
        `; // MODIFIED: Removed erroneous JS comment from HTML string

        requirements.forEach(req => {
            let name = req.name;
            name = name.replace('已读帖子(所有时间)', '已读(总)');
            name = name.replace('浏览的话题(所有时间)', '浏览(总)');
            name = name.replace('获赞:点赞用户数量', '点赞用户');
            name = name.replace('获赞:单日最高数量', '总获赞天');
            name = name.replace('被禁言(过去 6 个月)', '被禁言');
            name = name.replace('被封禁(过去 6 个月)', '被封禁');

            let current = req.current.match(/(\d+)/) ? req.current.match(/(\d+)/)[1] : req.current;
            let required = req.required.match(/(\d+)/) ? req.required.match(/(\d+)/)[1] : req.required;

            let changeIndicator = '';
            if (req.hasChanged && typeof req.changeValue === 'number' && req.changeValue !== 0) {
                const diff = req.changeValue;
                if (diff > 0) {
                    changeIndicator = `<span class="ld-increase"> ▲${diff}</span>`;
                } else {
                    changeIndicator = `<span class="ld-decrease"> ▼${Math.abs(diff)}</span>`;
                }
            }

            html += `
                <div class="ld-trust-level-item ${req.isSuccess ? 'ld-success' : 'ld-fail'}" title="${req.name}: ${req.current} / ${req.required}">
                    <span class="ld-name">${name}</span>
                    <span class="ld-value">${current}${changeIndicator} / ${required}</span>
                </div>
            `;
        });

        if (content) content.innerHTML = html;
    }

    function initializePanel() {
        if (!panel || !header || !content || !document.body || !document.head) {
            console.warn("LDStatus: Panel elements not fully ready, retrying init.");
            setTimeout(initializePanel, 50);
            return;
        }
        console.log("LDStatus: Initializing panel fully.");
        queryHeaderElements();
        restorePanelState();
        updateThemeButtonIcon();
        fetchTrustLevelData();
        setInterval(fetchTrustLevelData, 300000);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializePanel);
    } else {
        initializePanel();
    }

})();