Greasy Fork

Greasy Fork is available in English.

LINUX DO Credit 积分

LINUX DO Credit 实时收入

当前为 2025-12-28 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         LINUX DO Credit 积分
// @namespace    http://tampermonkey.net/
// @version      1.1.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: #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; /* 减少 padding 以保持圆点居中 */
            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: 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: #10b981; }
        #ldc-mini.negative { color: #ef4444; }
        #ldc-mini.neutral { color: #6b7280; }
        .dark #ldc-mini.neutral { color: #9ca3af; }
    `);

  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 = '···';

    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);

    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';
      }

      const toolRect = tooltip.getBoundingClientRect();
      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'); // Removed

        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正在获取实时积分...';
    }
  }

  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();
    setTimeout(fetchData, 500);
    setInterval(fetchData, 60000);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();