Greasy Fork is available in English.
显示 linux.do 今日积分变化,每半小时自动刷新,双击清除缓存
当前为
// ==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();
})();