Greasy Fork

Greasy Fork is available in English.

Linux.do Credit Display

显示 linux.do 今日积分变化,点击刷新,每半小时自动刷新

当前为 2026-02-09 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Linux.do Credit Display
// @namespace    http://tampermonkey.net/
// @version      2.4
// @description  显示 linux.do 今日积分变化,点击刷新,每半小时自动刷新
// @author       You
// @match        https://linux.do/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      credit.linux.do
// ==/UserScript==

(function() {
    'use strict';

    // API 和页面配置
    const TRANSACTIONS_API = 'https://credit.linux.do/api/v1/order/transactions';
    const LEADERBOARD_URL = 'https://linux.do/leaderboard';

    // 缓存 key
    const STORAGE_KEY = 'linux_do_credit_cache';
    const CURRENT_SCORE_KEY = 'linux_do_current_score_cache';
    const POSITION_KEY = 'linux_do_credit_position';

    // 请求配置
    const PAGE_SIZE = 20;
    const IFRAME_POLL_INTERVAL = 100;
    const IFRAME_MAX_ATTEMPTS = 50;
    const IFRAME_TIMEOUT = 15000;
    const ELEMENT_WAIT_TIMEOUT = 5000;
    const AUTO_REFRESH_INTERVAL = 30 * 60 * 1000; // 30分钟
    const MIN_VALID_SCORE = 100; // 积分低于此值视为异常

    // 通用容器样式
    const CONTAINER_STYLE = `
        position: fixed;
        bottom: 20px;
        right: 20px;
        background: #fff;
        color: #333;
        padding: 10px 14px;
        border-radius: 8px;
        font-size: 14px;
        z-index: 9999;
        box-shadow: 0 2px 12px rgba(0,0,0,0.15);
        white-space: nowrap;
    `;

    // 获取今天的时间范围(使用本地时区)
    function getTodayRange() {
        const now = new Date();
        const y = now.getFullYear();
        const m = String(now.getMonth() + 1).padStart(2, '0');
        const d = String(now.getDate()).padStart(2, '0');

        // 获取本地时区偏移,格式化为 +HH:MM 或 -HH:MM
        const offset = -now.getTimezoneOffset();
        const sign = offset >= 0 ? '+' : '-';
        const absOffset = Math.abs(offset);
        const hours = String(Math.floor(absOffset / 60)).padStart(2, '0');
        const minutes = String(absOffset % 60).padStart(2, '0');
        const tz = `${sign}${hours}:${minutes}`;

        return {
            startTime: `${y}-${m}-${d}T00:00:00${tz}`,
            endTime: `${y}-${m}-${d}T23:59:59${tz}`
        };
    }

    // 从 remark 解析积分: "社区积分从 1877 更新到 1938,变化 61" -> 1938
    function parseScore(remark) {
        const match = remark.match(/更新到\s*(\d+)/);
        if (!match) return null;
        const score = parseInt(match[1], 10);
        return isNaN(score) ? null : score;
    }

    // 安全解析数字文本(移除逗号,如 "2,003" -> 2003)
    function parseNumberText(text) {
        if (!text) return null;
        const num = parseInt(text.trim().replace(/,/g, ''), 10);
        return isNaN(num) ? null : num;
    }

    // 通用缓存读取(当天有效)
    function getCache(key) {
        const cache = GM_getValue(key, null);
        if (cache && cache.date === new Date().toDateString()) {
            return cache.score;
        }
        return null;
    }

    // 通用缓存写入
    function setCache(key, score) {
        GM_setValue(key, { score, date: new Date().toDateString() });
    }

    // 计算积分变化的显示信息
    function getDiffDisplay(currentScore, baseScore) {
        const diff = currentScore - baseScore;
        const sign = diff >= 0 ? '+' : '';
        const color = diff >= 0 ? '#22c55e' : '#ef4444';
        return { diff, sign, color };
    }

    // 获取今日基础积分
    function fetchBaseScore() {
        return new Promise((resolve, reject) => {
            const { startTime, endTime } = getTodayRange();

            GM_xmlhttpRequest({
                method: 'POST',
                url: TRANSACTIONS_API,
                headers: { 'Content-Type': 'application/json' },
                data: JSON.stringify({ page: 1, page_size: PAGE_SIZE, startTime, endTime }),
                onload: (res) => {
                    try {
                        const data = JSON.parse(res.responseText);
                        const orders = data.data?.orders || [];
                        for (const item of orders) {
                            if (item.remark?.includes('社区积分')) {
                                const score = parseScore(item.remark);
                                if (score !== null) {
                                    resolve(score);
                                    return;
                                }
                            }
                        }
                        reject(new Error('未找到积分记录'));
                    } catch (e) {
                        reject(e);
                    }
                },
                onerror: reject
            });
        });
    }

    // 等待元素出现
    function waitForElement(selector, timeout = ELEMENT_WAIT_TIMEOUT) {
        return new Promise((resolve) => {
            const el = document.querySelector(selector);
            if (el) {
                resolve(el);
                return;
            }

            let resolved = false;
            const observer = new MutationObserver(() => {
                if (resolved) return;
                const el = document.querySelector(selector);
                if (el) {
                    resolved = true;
                    observer.disconnect();
                    resolve(el);
                }
            });

            observer.observe(document.body, { childList: true, subtree: true });

            setTimeout(() => {
                if (!resolved) {
                    resolved = true;
                    observer.disconnect();
                    resolve(null);
                }
            }, timeout);
        });
    }

    // 从排行榜页面 DOM 获取当前积分
    function parseScoreFromElement(userEl) {
        if (!userEl) return null;
        return parseNumberText(userEl.textContent);
    }

    // 判断是否在排行榜页面
    function isLeaderboardPage() {
        return location.pathname === '/leaderboard' || location.pathname === '/leaderboard/';
    }

    // 创建基础容器
    function createContainer() {
        const container = document.createElement('div');
        container.id = 'linux-do-credit';
        container.style.cssText = CONTAINER_STYLE;

        // 恢复保存的位置
        const savedPos = GM_getValue(POSITION_KEY, null);
        if (savedPos) {
            container.style.right = 'auto';
            container.style.bottom = 'auto';
            container.style.left = savedPos.x + 'px';
            container.style.top = savedPos.y + 'px';
        }

        // 添加拖拽支持
        let isDragging = false;
        let hasMoved = false;
        let startX, startY, initialX, initialY;

        const onStart = (e) => {
            const touch = e.touches ? e.touches[0] : e;
            isDragging = true;
            hasMoved = false;
            startX = touch.clientX;
            startY = touch.clientY;
            const rect = container.getBoundingClientRect();
            initialX = rect.left;
            initialY = rect.top;
            container.style.transition = 'none';
        };

        const onMove = (e) => {
            if (!isDragging) return;
            const touch = e.touches ? e.touches[0] : e;
            const dx = touch.clientX - startX;
            const dy = touch.clientY - startY;
            if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
                hasMoved = true;
            }
            if (hasMoved) {
                e.preventDefault();
                const rect = container.getBoundingClientRect();
                const maxX = window.innerWidth - rect.width;
                const maxY = window.innerHeight - rect.height;
                const newX = Math.max(0, Math.min(maxX, initialX + dx));
                const newY = Math.max(0, Math.min(maxY, initialY + dy));
                container.style.right = 'auto';
                container.style.bottom = 'auto';
                container.style.left = newX + 'px';
                container.style.top = newY + 'px';
            }
        };

        const onEnd = () => {
            if (!isDragging) return;
            isDragging = false;
            container.style.transition = '';
            if (hasMoved) {
                const rect = container.getBoundingClientRect();
                GM_setValue(POSITION_KEY, { x: rect.left, y: rect.top });
            }
        };

        container.addEventListener('mousedown', onStart);
        container.addEventListener('touchstart', onStart, { passive: false });
        document.addEventListener('mousemove', onMove);
        document.addEventListener('touchmove', onMove, { passive: false });
        document.addEventListener('mouseup', onEnd);
        document.addEventListener('touchend', onEnd);

        // 标记是否拖拽过,用于区分点击和拖拽
        container._hasMoved = () => hasMoved;

        return container;
    }

    // 创建积分变化显示元素
    function createDiffElement(currentScore, baseScore) {
        const { sign, diff, color } = getDiffDisplay(currentScore, baseScore);
        const span = document.createElement('span');
        span.style.cssText = `font-weight: bold; color: ${color};`;
        span.textContent = `${sign}${diff}`;
        return span;
    }

    // 创建 UI(排行榜页面专用)
    async function createLeaderboardUI(baseScore) {
        const container = createContainer();
        container.textContent = '计算中...';
        document.body.appendChild(container);

        const userEl = await waitForElement('.user.-self .user__score');
        const currentScore = parseScoreFromElement(userEl);

        container.textContent = '';
        if (currentScore !== null) {
            const textNode = document.createTextNode('今日变化: ');
            container.appendChild(textNode);
            container.appendChild(createDiffElement(currentScore, baseScore));
        } else {
            container.style.color = '#f59e0b';
            container.textContent = '无法获取积分';
        }
    }

    // 通过隐藏 iframe 获取排行榜积分
    function fetchScoreViaIframe() {
        return new Promise((resolve, reject) => {
            const iframe = document.createElement('iframe');
            iframe.style.cssText = 'position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none;';
            iframe.src = LEADERBOARD_URL;

            let resolved = false;

            const cleanup = () => {
                if (iframe.parentNode) {
                    document.body.removeChild(iframe);
                }
            };

            const finish = (success, value) => {
                if (resolved) return;
                resolved = true;
                cleanup();
                success ? resolve(value) : reject(value);
            };

            iframe.onload = () => {
                let iframeDoc;
                try {
                    iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
                } catch (e) {
                    finish(false, new Error('无法访问 iframe 内容(跨域限制)'));
                    return;
                }

                let attempts = 0;
                const checkElement = () => {
                    if (resolved) return;

                    // 优先获取 title 属性(移动端显示 2.1k,但 title 有完整数值)
                    const userEl = iframeDoc.querySelector('.user.-self .user__score');
                    let score = null;

                    if (userEl) {
                        const numberSpan = userEl.querySelector('.number[title]');
                        if (numberSpan) {
                            score = parseNumberText(numberSpan.getAttribute('title'));
                        }
                        if (score === null) {
                            score = parseNumberText(userEl.textContent);
                        }
                    }

                    if (score !== null && score >= MIN_VALID_SCORE) {
                        finish(true, score);
                    } else if (attempts < IFRAME_MAX_ATTEMPTS) {
                        attempts++;
                        setTimeout(checkElement, IFRAME_POLL_INTERVAL);
                    } else {
                        finish(false, new Error('无法找到积分元素'));
                    }
                };

                setTimeout(checkElement, 500);
            };

            iframe.onerror = () => {
                finish(false, new Error('iframe 加载失败'));
            };

            setTimeout(() => {
                finish(false, new Error('获取积分超时'));
            }, IFRAME_TIMEOUT);

            document.body.appendChild(iframe);
        });
    }

    // 创建 UI(普通页面)
    function createUI(baseScore) {
        const container = createContainer();
        container.style.cssText += 'cursor: pointer;';

        let isLoading = false;

        // 更新显示
        const updateDisplay = (currentScore) => {
            container.textContent = '';
            container.appendChild(document.createTextNode('今日: '));
            container.appendChild(createDiffElement(currentScore, baseScore));
        };

        // 显示加载状态
        const showLoading = () => {
            container.textContent = '获取中...';
            container.style.color = '#999';
        };

        // 显示错误
        const showError = (msg) => {
            container.textContent = msg;
            container.style.color = '#ef4444';
        };

        // 恢复正常样式
        const resetStyle = () => {
            container.style.color = '#333';
        };

        // 获取积分
        const doFetch = async () => {
            if (isLoading) return;
            isLoading = true;
            showLoading();

            try {
                const currentScore = await fetchScoreViaIframe();
                setCache(CURRENT_SCORE_KEY, currentScore);
                resetStyle();
                updateDisplay(currentScore);
            } catch (e) {
                console.error('获取积分失败:', e);
                showErrorWithLogin(container);
            }

            isLoading = false;
        };

        document.body.appendChild(container);

        // 点击刷新(排除拖拽)
        container.onclick = (e) => {
            if (container._hasMoved()) return;
            if (e.target.tagName === 'A') return;
            doFetch();
        };

        // 双击清理缓存
        container.ondblclick = () => {
            GM_setValue(STORAGE_KEY, null);
            GM_setValue(CURRENT_SCORE_KEY, null);
            GM_setValue(POSITION_KEY, null);
            location.reload();
        };

        // 初始显示
        const cachedCurrentScore = getCache(CURRENT_SCORE_KEY);
        if (cachedCurrentScore !== null) {
            updateDisplay(cachedCurrentScore);
        } else {
            container.textContent = '点击获取今日积分';
        }

        // 定时自动刷新
        setInterval(doFetch, AUTO_REFRESH_INTERVAL);
    }

    // 显示错误状态(带登录链接)
    function showErrorWithLogin(container) {
        container.innerHTML = '';
        container.style.color = '#333';
        container.appendChild(document.createTextNode('请登录 '));
        const link = document.createElement('a');
        link.href = 'https://credit.linux.do/';
        link.target = '_blank';
        link.style.cssText = 'color: #3b82f6; text-decoration: underline;';
        link.textContent = 'LINUX DO Credit';
        container.appendChild(link);
    }

    // 显示错误状态
    function showError() {
        const container = createContainer();
        showErrorWithLogin(container);
        document.body.appendChild(container);
    }

    // 初始化
    async function init() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
            return;
        }

        // 获取基础积分(优先缓存)
        let baseScore = getCache(STORAGE_KEY);

        if (baseScore === null) {
            try {
                baseScore = await fetchBaseScore();
                setCache(STORAGE_KEY, baseScore);
            } catch (e) {
                console.error('获取积分失败:', e);
                showError();
                return;
            }
        }

        // 根据页面类型显示不同 UI
        if (isLeaderboardPage()) {
            createLeaderboardUI(baseScore);
        } else {
            createUI(baseScore);
        }
    }

    init();
})();