Greasy Fork

Greasy Fork is available in English.

LINUX DO Credit 预估

预估 LINUX DO Credit

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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();
  }
})();