Greasy Fork is available in English.
LINUX DO Credit 实时收入
当前为
// ==UserScript==
// @name LINUX DO Credit 积分
// @namespace http://tampermonkey.net/
// @version 1.1.2
// @description LINUX DO Credit 实时收入
// @author @Chenyme
// @license MIT
// @match https://linux.do/*
// @match https://credit.linux.do/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @connect credit.linux.do
// @connect linux.do
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
GM_addStyle(`
#ldc-mini {
position: fixed;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 2px 4px rgb(0 0 0 / 0.04);
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 10px 14px;
font-variant-numeric: tabular-nums;
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
width: fit-content;
min-width: 36px;
max-width: 200px;
white-space: nowrap;
overflow: hidden;
transition: all 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
cursor: move;
user-select: none;
}
.dark #ldc-mini {
background: #1f2937;
border-color: rgba(255, 255, 255, 0.1);
}
#ldc-mini:hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
transform: translateY(-1px);
}
#ldc-mini:active {
transform: scale(0.98);
}
#ldc-mini.loading {
min-width: 36px;
max-width: 36px;
padding: 10px 0;
color: #6b7280;
cursor: wait;
border-color: transparent;
background: rgba(255, 255, 255, 0.8);
}
.dark #ldc-mini.loading {
background: rgba(31, 41, 55, 0.8);
}
#ldc-tooltip {
position: fixed;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
line-height: 1.5;
z-index: 10001;
pointer-events: auto;
white-space: pre;
opacity: 0;
transition: opacity 0.15s ease;
backdrop-filter: blur(4px);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-variant-numeric: tabular-nums;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.dark #ldc-tooltip {
background: rgba(255, 255, 255, 0.9);
color: black;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
#ldc-theme-buttons {
display: flex;
gap: 4px;
margin-top: 6px;
padding-top: 4px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.dark #ldc-theme-buttons {
border-top-color: rgba(0, 0, 0, 0.15);
}
.ldc-theme-btn {
flex: 1;
padding: 3px 6px;
background: transparent;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 10px;
transition: all 0.15s ease;
user-select: none;
opacity: 0.5;
color: inherit;
text-align: center;
}
.ldc-theme-btn:hover {
opacity: 0.8;
background: rgba(255, 255, 255, 0.1);
}
.dark .ldc-theme-btn:hover {
background: rgba(0, 0, 0, 0.1);
}
.ldc-theme-btn.active {
opacity: 1;
background: rgba(255, 255, 255, 0.15);
font-weight: 500;
}
.dark .ldc-theme-btn.active {
background: rgba(0, 0, 0, 0.2);
}
#ldc-mini.positive { color: #10b981; }
#ldc-mini.negative { color: #ef4444; }
#ldc-mini.neutral { color: #6b7280; }
.dark #ldc-mini.positive { color: #34d399; }
.dark #ldc-mini.negative { color: #f87171; }
.dark #ldc-mini.neutral { color: #9ca3af; }
`);
let communityBalance = null;
let gamificationScore = null;
let username = null;
let isDragging = false;
let tooltipContent = '加载中...';
let tooltipHovered = false;
let currentTheme = GM_getValue('ldc_theme', 'auto');
function createWidget() {
const widget = document.createElement('div');
widget.id = 'ldc-mini';
widget.className = 'loading';
widget.textContent = '···';
const tooltip = document.createElement('div');
tooltip.id = 'ldc-tooltip';
document.body.appendChild(tooltip);
const savedPos = GM_getValue('ldc_pos', { bottom: '20px', right: '20px' });
Object.assign(widget.style, savedPos);
document.body.appendChild(widget);
applyTheme();
widget.addEventListener('mouseenter', () => {
if (isDragging) return;
tooltipHovered = false;
const rect = widget.getBoundingClientRect();
updateTooltipContent();
const tooltipHeight = 120;
if (rect.top > tooltipHeight + 10) {
tooltip.style.top = 'auto';
tooltip.style.bottom = (window.innerHeight - rect.top + 8) + 'px';
} else {
tooltip.style.bottom = 'auto';
tooltip.style.top = (rect.bottom + 8) + 'px';
}
const toolRect = tooltip.getBoundingClientRect();
tooltip.style.left = 'auto';
tooltip.style.right = (window.innerWidth - rect.right) + 'px';
tooltip.style.opacity = '1';
});
widget.addEventListener('mouseleave', () => {
setTimeout(() => {
if (!tooltipHovered) {
tooltip.style.opacity = '0';
}
}, 100);
});
tooltip.addEventListener('mouseenter', () => {
tooltipHovered = true;
});
tooltip.addEventListener('mouseleave', () => {
tooltipHovered = false;
tooltip.style.opacity = '0';
});
let startX, startY;
let startRight, startBottom;
widget.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
isDragging = false;
startX = e.clientX;
startY = e.clientY;
const rect = widget.getBoundingClientRect();
startRight = window.innerWidth - rect.right;
startBottom = window.innerHeight - rect.bottom;
e.preventDefault();
tooltip.style.opacity = '0';
const onMouseMove = (moveEvent) => {
isDragging = true;
const deltaX = startX - moveEvent.clientX;
const deltaY = startY - moveEvent.clientY;
widget.style.right = `${Math.max(0, Math.min(window.innerWidth - rect.width, startRight + deltaX))}px`;
widget.style.bottom = `${Math.max(0, Math.min(window.innerHeight - rect.height, startBottom + deltaY))}px`;
widget.style.top = 'auto';
widget.style.left = 'auto';
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
if (isDragging) {
GM_setValue('ldc_pos', {
right: widget.style.right,
bottom: widget.style.bottom
});
setTimeout(() => isDragging = false, 50);
}
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
widget.addEventListener('click', (e) => {
if (!isDragging) {
// console.log('LDC: Manual refresh triggered');
widget.className = 'loading';
widget.textContent = '···';
tooltipContent = '刷新中...';
const tooltip = document.getElementById('ldc-tooltip');
if (tooltip.style.opacity === '1') {
tooltip.textContent = tooltipContent;
}
fetchData();
}
});
}
function applyTheme() {
const htmlElement = document.documentElement;
const bodyElement = document.body;
if (currentTheme === 'dark') {
htmlElement.classList.add('dark');
bodyElement.classList.add('dark');
} else if (currentTheme === 'light') {
htmlElement.classList.remove('dark');
bodyElement.classList.remove('dark');
} else if (currentTheme === 'auto') {
detectAndApplySystemTheme();
}
}
function detectAndApplySystemTheme() {
const htmlElement = document.documentElement;
const bodyElement = document.body;
const colorScheme = getComputedStyle(document.documentElement).colorScheme;
const schemeType = getComputedStyle(document.documentElement).getPropertyValue('--scheme-type').trim();
const isDark = colorScheme === 'dark' || schemeType === 'dark';
if (isDark) {
htmlElement.classList.add('dark');
bodyElement.classList.add('dark');
} else {
htmlElement.classList.remove('dark');
bodyElement.classList.remove('dark');
}
}
function startThemeObserver() {
const observer = new MutationObserver(() => {
if (currentTheme === 'auto') {
detectAndApplySystemTheme();
}
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['style', 'class']
});
const styleObserver = setInterval(() => {
if (currentTheme === 'auto') {
detectAndApplySystemTheme();
}
}, 1000);
return () => {
observer.disconnect();
clearInterval(styleObserver);
};
}
function setTheme(theme) {
currentTheme = theme;
GM_setValue('ldc_theme', currentTheme);
applyTheme();
updateTooltipContent();
}
function updateTooltipContent() {
const tooltip = document.getElementById('ldc-tooltip');
if (!tooltip) return;
tooltip.innerHTML = '';
const textNode = document.createElement('div');
textNode.textContent = tooltipContent;
tooltip.appendChild(textNode);
const buttonsContainer = document.createElement('div');
buttonsContainer.id = 'ldc-theme-buttons';
const themes = [
{ id: 'auto', label: '自动' },
{ id: 'light', label: '浅色' },
{ id: 'dark', label: '深色' }
];
themes.forEach(theme => {
const btn = document.createElement('button');
btn.className = 'ldc-theme-btn';
if (currentTheme === theme.id) {
btn.classList.add('active');
}
btn.textContent = theme.label;
btn.addEventListener('click', (e) => {
e.stopPropagation();
setTheme(theme.id);
});
buttonsContainer.appendChild(btn);
});
tooltip.appendChild(buttonsContainer);
}
function updateDisplay() {
const widget = document.getElementById('ldc-mini');
const tooltip = document.getElementById('ldc-tooltip');
if (!widget) return;
if (gamificationScore !== null && communityBalance !== null) {
const diff = gamificationScore - communityBalance;
let displayText;
if (diff > 0) {
displayText = `+${diff.toFixed(2)}`;
} else if (diff < 0) {
displayText = `- ${Math.abs(diff).toFixed(2)}`; // 负数显示为 "- xxx" 格式
} else {
displayText = '0.00';
}
widget.textContent = displayText;
tooltipContent = `仅供参考,可能有误差!\n当前分: ${gamificationScore.toFixed(2)}\n基准值: ${communityBalance.toFixed(2)}`;
if (tooltip && tooltip.style.opacity === '1') {
updateTooltipContent();
}
widget.className = diff > 0 ? 'positive' : diff < 0 ? 'negative' : 'neutral';
if (widget.style.cursor === 'wait') {
widget.style.removeProperty('cursor');
}
} else if (communityBalance !== null) {
widget.textContent = '···';
widget.className = 'loading';
tooltipContent = '仅供参考,可能有误差!\n正在获取实时积分...';
}
}
async function request(url) {
const isSameOrigin = url.startsWith(window.location.origin);
if (isSameOrigin) {
try {
const res = await fetch(url, { credentials: 'include' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (e) {
// console.error('LDC: Native fetch failed, falling back to GM', e);
// Fallback to GM if native fails (unlikely for same-origin unless CSP issues)
}
}
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
withCredentials: true,
headers: {
'Accept': 'application/json',
'Referer': 'https://credit.linux.do/home'
},
timeout: 10000,
onload: (res) => {
if (res.status === 200) {
try {
resolve(JSON.parse(res.responseText));
} catch (e) {
reject(e);
}
} else {
reject(new Error(`HTTP ${res.status}`));
}
},
ontimeout: () => reject(new Error('Timeout')),
onerror: (err) => reject(err)
});
});
}
async function fetchData() {
try {
// 1. 获取 Credit 余额
const creditData = await request('https://credit.linux.do/api/v1/oauth/user-info');
if (creditData?.data) {
communityBalance = parseFloat(creditData.data['community-balance'] || creditData.data.community_balance || 0);
username = creditData.data.username || creditData.data.nickname;
updateDisplay();
// 2. 获取 Gamification 分数
if (username) {
await fetchGamificationByUsername();
}
}
} catch (e) {
console.error('LDC: Fetch balance error', e);
handleError('Credit API 异常');
}
}
function handleError(msg) {
const widget = document.getElementById('ldc-mini');
if (widget) {
widget.textContent = '!';
tooltipContent = `出错啦!\n${msg}\n(请检查是否已登录相关站点)`;
widget.classList.add('negative');
if (widget.classList.contains('loading')) widget.classList.remove('loading');
}
}
async function fetchGamificationByUsername() {
if (!username) return;
try {
const data = await request(`https://linux.do/u/${username}.json`);
if (data?.user?.gamification_score !== undefined) {
gamificationScore = parseFloat(data.user.gamification_score);
updateDisplay();
}
} catch (e) {
console.error('LDC: Fetch gamification error', e);
}
}
function init() {
createWidget();
startThemeObserver(); // 启动主题监听
setTimeout(fetchData, 500);
setInterval(fetchData, 60000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();