// ==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 // @downloadURL https://update.greasyfork.icu/scripts/560312/LINUX%20DO%20Credit%20%E9%A2%84%E4%BC%B0.user.js // @updateURL https://update.greasyfork.icu/scripts/560312/LINUX%20DO%20Credit%20%E9%A2%84%E4%BC%B0.meta.js // ==/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(); } })();