// ==UserScript== // @name Linux.do Credit Display // @namespace http://tampermonkey.net/ // @version 2.3 // @description 显示 linux.do 今日积分变化,点击刷新,每半小时自动刷新 // @author You // @match https://linux.do/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect credit.linux.do // @downloadURL none // ==/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 PAGE_SIZE = 20; const IFRAME_POLL_INTERVAL = 100; const IFRAME_MAX_ATTEMPTS = 50; const IFRAME_TIMEOUT = 10000; 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); `; // 获取今天的时间范围(使用本地时区) 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; 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; const userEl = iframeDoc.querySelector('.user.-self .user__score'); if (userEl) { const score = parseNumberText(userEl.textContent); if (score !== null) { finish(true, score); } else { finish(false, new Error('积分解析失败')); } } 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 = doFetch; // 初始显示 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(); })();