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