Greasy Fork is available in English.
预估 LINUX DO Credit
// ==UserScript==
// @name LINUX DO Credit 预估
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @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: oklch(1 0 0);
border: 1px solid oklch(0.92 0.004 286.32);
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;
min-width: 65px; /* 数字显示状态的最小宽度 */
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: oklch(0.21 0.006 285.885);
border-color: oklch(1 0 0 / 10%);
}
#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; /* 减少 padding 以保持圆点居中 */
color: oklch(0.552 0.016 285.938);
cursor: wait;
border-color: transparent; /* 加载时淡化边框 */
background: oklch(1 0 0 / 0.8);
}
.dark #ldc-mini.loading {
background: oklch(0.21 0.006 285.885 / 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: none;
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-mini.positive { color: oklch(0.696 0.17 162.48); }
#ldc-mini.negative { color: oklch(0.704 0.191 22.216); }
#ldc-mini.neutral { color: oklch(0.552 0.016 285.938); }
.dark #ldc-mini.neutral { color: oklch(0.705 0.015 286.067); }
`);
let communityBalance = null;
let gamificationScore = null;
let username = null;
let isDragging = false;
let tooltipContent = '加载中...';
function createWidget() {
const widget = document.createElement('div');
widget.id = 'ldc-mini';
widget.className = 'loading';
widget.textContent = '·'; // 使用一个小点代替 ... 以配合圆形加载态
// 创建 tooltip 元素
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);
// 悬浮显示 Tooltip
widget.addEventListener('mouseenter', () => {
if (isDragging) return;
const rect = widget.getBoundingClientRect();
tooltip.textContent = tooltipContent;
const tooltipHeight = 80;
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';
}
// 计算 tooltip 水平位置,居中对齐 widget 但不超出屏幕
const toolRect = tooltip.getBoundingClientRect(); // 获取预估宽度,如果不准确可以设固定值或 delayed
// 这里简单处理:右对齐
tooltip.style.left = 'auto';
tooltip.style.right = (window.innerWidth - rect.right) + 'px';
tooltip.style.opacity = '1';
});
widget.addEventListener('mouseleave', () => {
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 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;
const sign = diff >= 0 ? '+' : '';
widget.textContent = `${sign}${diff.toFixed(2)}`;
tooltipContent = `仅供参考,可能有误差!\n当前分: ${gamificationScore.toFixed(2)}\n基准值: ${communityBalance.toFixed(2)}`;
if (tooltip && tooltip.style.opacity === '1') {
tooltip.textContent = tooltipContent;
}
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正在获取实时积分...';
}
}
function fetchData() {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://credit.linux.do/api/v1/oauth/user-info',
withCredentials: true,
headers: {
'Accept': 'application/json',
'Referer': 'https://credit.linux.do/home'
},
timeout: 10000,
onload: function (response) {
if (response.status === 200) {
try {
const json = JSON.parse(response.responseText);
if (json?.data) {
communityBalance = parseFloat(json.data['community-balance'] || json.data.community_balance || 0);
username = json.data.username || json.data.nickname;
updateDisplay();
if (username) {
fetchGamificationByUsername();
}
}
} catch (e) {
console.error('LDC: Parse balance error', e);
}
}
},
ontimeout: () => {
const widget = document.getElementById('ldc-mini');
if (widget) {
widget.textContent = '!';
tooltipContent = '仅供参考,可能有误差!\nCredit API 超时,点击重试';
widget.classList.add('negative');
}
},
onerror: () => console.error('LDC: Network error (balance)')
});
}
function fetchGamificationByUsername() {
if (!username) return;
GM_xmlhttpRequest({
method: 'GET',
url: `https://linux.do/u/${username}.json`,
withCredentials: true,
headers: { 'Accept': 'application/json' },
timeout: 10000,
onload: function (response) {
if (response.status === 200) {
try {
const json = JSON.parse(response.responseText);
if (json?.user?.gamification_score !== undefined) {
gamificationScore = parseFloat(json.user.gamification_score);
updateDisplay();
}
} catch (e) {
console.error('LDC: Parse gamification error', e);
}
}
},
ontimeout: () => {
const widget = document.getElementById('ldc-mini');
if (widget) {
widget.textContent = '!';
tooltipContent = '仅供参考,可能有误差!\nLinux.do API 超时,点击重试';
widget.classList.add('negative');
}
},
onerror: () => console.error('LDC: Network error (gamification)')
});
}
function init() {
createWidget();
setTimeout(fetchData, 500);
setInterval(fetchData, 60000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();