Greasy Fork

Greasy Fork is available in English.

Linux.do Credit Display

显示 linux.do 今日积分变化,每半小时自动刷新,双击清除缓存

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Linux.do Credit Display
// @namespace    http://tampermonkey.net/
// @version      2.8
// @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 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 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}`;

        // 一周前
        const start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
        const sy = start.getFullYear();
        const sm = String(start.getMonth() + 1).padStart(2, '0');
        const sd = String(start.getDate()).padStart(2, '0');

        // 两天后
        const end = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000);
        const ey = end.getFullYear();
        const em = String(end.getMonth() + 1).padStart(2, '0');
        const ed = String(end.getDate()).padStart(2, '0');

        return {
            startTime: `${sy}-${sm}-${sd}T00:00:00${tz}`,
            endTime: `${ey}-${em}-${ed}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;
    }

    // 解析紧凑积分文本(如 "2.1k" -> 2100, "2k" -> 2000)
    function parseCompactNumberText(text) {
        if (!text) return null;
        const t = text.replace(/\u00A0/g, ' ').trim();

        // 1) 精确匹配:纯数字 / 带 k/m
        let match = t.replace(/,/g, '').match(/^(\d+(?:\.\d+)?)\s*([kKmM])?$/);
        if (match) {
            const value = parseFloat(match[1]);
            if (!isFinite(value)) return null;
            const unit = match[2]?.toLowerCase();
            const multiplier = unit === 'k' ? 1000 : unit === 'm' ? 1000000 : 1;
            const result = Math.round(value * multiplier);
            return isNaN(result) ? null : result;
        }

        // 2) 文本中提取:如 "2.1k" 混在其它字符里
        match = t.match(/(\d+(?:\.\d+)?)\s*([kKmM])\b/);
        if (match) {
            const value = parseFloat(match[1]);
            if (!isFinite(value)) return null;
            const unit = match[2].toLowerCase();
            const multiplier = unit === 'k' ? 1000 : unit === 'm' ? 1000000 : 1;
            const result = Math.round(value * multiplier);
            return isNaN(result) ? null : result;
        }

        // 3) 回退:提取第一个整数(支持逗号)
        match = t.match(/(\d[\d,]*)/);
        return match ? parseNumberText(match[1]) : null;
    }

    // 通用缓存读取(当天有效)
    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, types: ["community"] }),
                withCredentials: true,
                anonymous: false,
                onload: (res) => {
                    try {
                        if (res.status === 401 || res.status === 403) {
                            reject(new Error('未登录或无权限'));
                            return;
                        }
                        if (res.status < 200 || res.status >= 300) {
                            reject(new Error(`请求失败: ${res.status}`));
                            return;
                        }
                        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;
        const numberSpan = userEl.querySelector?.('.number[title]');
        if (numberSpan) {
            const fromTitle = parseNumberText(numberSpan.getAttribute('title'));
            if (fromTitle !== null) return fromTitle;
        }
        return parseCompactNumberText(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 });
            }
        };

        // 拖拽开始时才绑定 document 事件,结束时解绑,避免内存泄漏
        const onStartWrapped = (e) => {
            onStart(e);
            document.addEventListener('mousemove', onMove);
            document.addEventListener('touchmove', onMove, { passive: false });
            document.addEventListener('mouseup', onEndWrapped);
            document.addEventListener('touchend', onEndWrapped);
        };

        const onEndWrapped = () => {
            onEnd();
            document.removeEventListener('mousemove', onMove);
            document.removeEventListener('touchmove', onMove);
            document.removeEventListener('mouseup', onEndWrapped);
            document.removeEventListener('touchend', onEndWrapped);
        };

        container.addEventListener('mousedown', onStartWrapped);
        container.addEventListener('touchstart', onStartWrapped, { passive: false });

        // 标记是否拖拽过,用于区分点击和拖拽
        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(排行榜页面专用)
    function createLeaderboardUI(baseScore) {
        const container = createContainer();
        container.style.cssText += 'cursor: pointer;';
        document.body.appendChild(container);

        let isLoading = false;
        let currentBaseScore = baseScore;

        // 更新显示(与普通页面一致)
        const updateDisplay = (currentScore) => {
            container.textContent = '';
            container.style.color = '#333';
            container.appendChild(document.createTextNode('今日: '));
            container.appendChild(createDiffElement(currentScore, currentBaseScore));
        };

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

        // 获取积分(从当前页面 DOM 读取,无需 iframe)
        const doFetch = async () => {
            if (isLoading) return;
            isLoading = true;
            showLoading();

            try {
                // 跨天后刷新基础积分
                const cachedBase = getCache(STORAGE_KEY);
                if (cachedBase === null) {
                    currentBaseScore = await fetchBaseScore();
                    setCache(STORAGE_KEY, currentBaseScore);
                    GM_setValue(CURRENT_SCORE_KEY, null);
                } else {
                    currentBaseScore = cachedBase;
                }

                // 直接从排行榜页面 DOM 读取积分
                const userEl = await waitForElement('.user.-self .user__score');
                const currentScore = parseScoreFromElement(userEl);

                if (currentScore !== null) {
                    setCache(CURRENT_SCORE_KEY, currentScore);
                    updateDisplay(currentScore);
                } else {
                    container.textContent = '无法获取积分';
                    container.style.color = '#f59e0b';
                }
            } catch (e) {
                console.error('获取积分失败:', e);
                showErrorWithLogin(container);
            }

            isLoading = false;
        };

        let clickTimeout = null;

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

            if (clickTimeout) clearTimeout(clickTimeout);
            clickTimeout = setTimeout(() => {
                doFetch();
                clickTimeout = null;
            }, 250);
        };

        // 双击清理缓存
        container.ondblclick = () => {
            if (clickTimeout) clearTimeout(clickTimeout);
            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);
        }
        // 排行榜页面可直接从 DOM 读取,无需 iframe,进入时自动更新一次
        doFetch();

        // 定时自动刷新
        const intervalId = setInterval(doFetch, AUTO_REFRESH_INTERVAL);

        // 返回清理函数,供 SPA 导航时调用
        return () => {
            if (clickTimeout) clearTimeout(clickTimeout);
            clearInterval(intervalId);
            if (container.parentNode) container.parentNode.removeChild(container);
        };
    }

    // 通过隐藏 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 = parseCompactNumberText(userEl.textContent);
                        }
                    }

                    if (score !== null) {
                        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;
        let currentBaseScore = baseScore;

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

        // 显示加载状态
        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 cachedBase = getCache(STORAGE_KEY);
                if (cachedBase === null) {
                    currentBaseScore = await fetchBaseScore();
                    setCache(STORAGE_KEY, currentBaseScore);
                    GM_setValue(CURRENT_SCORE_KEY, null);
                } else {
                    currentBaseScore = cachedBase;
                }

                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);

        let clickTimeout = null;

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

            if (clickTimeout) clearTimeout(clickTimeout);
            clickTimeout = setTimeout(() => {
                doFetch();
                clickTimeout = null;
            }, 250);
        };

        // 双击清理缓存
        container.ondblclick = () => {
            if (clickTimeout) clearTimeout(clickTimeout);
            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 {
            // 如果没有缓存,说明是首次访问或双击清理了缓存,自动获取一次积分
            doFetch();
        }

        // 定时自动刷新
        const intervalId = setInterval(doFetch, AUTO_REFRESH_INTERVAL);

        // 返回清理函数,供 SPA 导航时调用
        return () => {
            if (clickTimeout) clearTimeout(clickTimeout);
            clearInterval(intervalId);
            if (container.parentNode) container.parentNode.removeChild(container);
        };
    }

    // 显示错误状态(带登录链接)
    function showErrorWithLogin(container) {
        container.textContent = '';
        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);
        return () => {
            if (container.parentNode) container.parentNode.removeChild(container);
        };
    }

    // 当前实例的清理函数
    let currentCleanup = null;
    let initCounter = 0;

    // 初始化(可重复调用,自动清理上一次实例)
    async function init() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
            return;
        }

        const currentInitId = ++initCounter;

        // 清理上一次的 UI 和定时器
        if (currentCleanup) {
            currentCleanup();
            currentCleanup = null;
        }

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

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

        if (currentInitId !== initCounter) return;

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

    // 监听 SPA 导航(linux.do 是 Ember.js 应用,URL 变化不刷新页面)
    let lastPath = location.pathname;
    const onNavigate = () => {
        if (location.pathname !== lastPath) {
            lastPath = location.pathname;
            init();
        }
    };

    // popstate 处理浏览器前进/后退
    window.addEventListener('popstate', onNavigate);

    // 劫持 pushState/replaceState 处理 SPA 路由跳转
    const origPushState = history.pushState;
    const origReplaceState = history.replaceState;
    history.pushState = function(...args) {
        origPushState.apply(this, args);
        onNavigate();
    };
    history.replaceState = function(...args) {
        origReplaceState.apply(this, args);
        onNavigate();
    };

    init();
})();