Hexo.io 插件页面增强:star数、更新时间

增强 Hexo 插件页面,获取并显示 GitHub 仓库的 star 数和最后更新时间,支持排序和缓存

// ==UserScript==
// @name            Hexo.io 插件页面增强:star数、更新时间
// @name:en         Hexo.io plugin page enhancements: star count, update time
// @namespace       http://tampermonkey.net/
// @version         0.1
// @description     增强 Hexo 插件页面,获取并显示 GitHub 仓库的 star 数和最后更新时间,支持排序和缓存
// @description:en  Enhance the Hexo plugin page to obtain and display the number of stars and last update time of the GitHub repository, and support sorting and caching
// @author          二次蓝
// @match           https://hexo.io/plugins/
// @grant           GM_xmlhttpRequest
// @grant           GM_setValue
// @grant           GM_getValue
// @connect         api.github.com
// ==/UserScript==

(function () {
  'use strict';

  const PLUGIN_SELECTOR = '.plugin';
  const GITHUB_API = 'https://api.github.com/repos/';
  const plugins = [];
  const CACHE_KEY = 'hexo_plugin_info_cache';
  const TOKEN_KEY = 'github_token';
  const CACHE_DURATION = 1000 * 60 * 60 * 24;
  const THEME_COLOR = '#5AC8FA';

  const SVG_ICONS = {
    settings: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 15.5A3.5 3.5 0 0 1 8.5 12A3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5a3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97c0-.33-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1c0 .33.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66Z"/></svg>`,
    close: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12L19 6.41Z"/></svg>`,
    rocket: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg>`,
    trash: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M9 3V4H4V6H5V19C5 20.1 5.9 21 7 21H17C18.1 21 19 20.1 19 19V6H20V4H15V3H9M7 6H17V19H7V6M9 8V17H11V8H9M13 8V17H15V8H13Z"/></svg>`,
    key: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M7 14C5.9 14 5 13.1 5 12S5.9 10 7 10 9 10.9 9 12 8.1 14 7 14M12.65 10C11.83 7.67 9.61 6 7 6C3.69 6 1 8.69 1 12S3.69 18 7 18C9.61 18 11.83 16.33 12.65 14H17V18H21V14H23V10H12.65Z"/></svg>`,
    sort: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M18 21L14 17H17V7H14L18 3L22 7H19V17H22M2 19V17H12V19M2 13V11H9V13M2 7V5H6V7H2Z"/></svg>`,
    star: `<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 17.27L18.18 21L16.54 13.97L22 9.24L14.81 8.62L12 2L9.19 8.62L2 9.24L7.46 13.97L5.82 21L12 17.27Z"/></svg>`,
    calendar: `<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H18V1H16V3H8V1H6V3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3M19 19H5V8H19V19M7 10H12V15H7"/></svg>`,
    warning: `<svg width="16" height="16" viewBox="0 0 24 24" fill="#ff6b6b"><path d="M13 14H11V9H13M13 18H11V16H13M1 21H23L12 2L1 21Z"/></svg>`
  };

  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

  const extractRepo = (url) => {
    const match = url.match(/github\.com\/([\w-]+\/[\w.-]+)/);
    return match ? match[1] : null;
  };

  function checkCacheExpiry() {
    try {
      const raw = GM_getValue(CACHE_KEY, null);
      if (!raw) return { hasExpired: false, total: 0, expired: 0 };
      
      const parsed = JSON.parse(raw);
      const now = Date.now();
      let total = 0;
      let expired = 0;
      
      Object.entries(parsed).forEach(([, v]) => {
        total++;
        if (now - v.timestamp >= CACHE_DURATION) {
          expired++;
        }
      });
      
      return { hasExpired: expired > 0, total, expired };
    } catch {
      return { hasExpired: false, total: 0, expired: 0 };
    }
  }

  function displayCachedData() {
    const pluginElements = Array.from(document.querySelectorAll(PLUGIN_SELECTOR));
    const cache = loadCache();
    let displayed = 0;
    
    pluginElements.forEach(el => {
      const a = el.querySelector('a[href*="github.com"]');
      const repo = a ? extractRepo(a.href) : null;
      if (!repo || !cache[repo]) return;
      
      const data = cache[repo].data;
      const isExpired = Date.now() - cache[repo].timestamp >= CACHE_DURATION;
      
      let extra = el.querySelector('.github-info');
      if (!extra) {
        extra = document.createElement('div');
        extra.className = 'github-info';
        extra.style.cssText = `
          font-size: 12px;
          color: #666;
          margin-top: 5px;
          padding: 4px 8px;
          background: ${isExpired ? '#fff3cd' : '#f8f9fa'};
          border-radius: 4px;
          border-left: 3px solid ${isExpired ? '#ffc107' : THEME_COLOR};
        `;
        el.appendChild(extra);
      }
      
      const updatedDate = data.updated ? new Date(data.updated).toLocaleDateString('zh-CN') : '未知';
      extra.innerHTML = `
        <span style="color: #ffc107; vertical-align: middle;">${SVG_ICONS.star}</span> ${data.stars} | 
        <span style="color: #28a745; vertical-align: middle;">${SVG_ICONS.calendar}</span> ${updatedDate}
        ${isExpired ? `<span style="color: #ff6b6b; margin-left: 8px; vertical-align: middle;">${SVG_ICONS.warning} 数据已过期</span>` : ''}
      `;
      
      plugins.push({
        element: el,
        repo: repo,
        stars: data.stars,
        updated: data.updated
      });
      
      displayed++;
    });
    
    return displayed;
  }

  function loadCache() {
    try {
      const raw = GM_getValue(CACHE_KEY, null);
      if (!raw) return {};
      const parsed = JSON.parse(raw);
      const now = Date.now();
      return Object.fromEntries(
        Object.entries(parsed).filter(([, v]) => now - v.timestamp < CACHE_DURATION)
      );
    } catch (e) {
      console.warn('缓存加载失败:', e);
      return {};
    }
  }

  function saveCache(cache) {
    try {
      GM_setValue(CACHE_KEY, JSON.stringify(cache));
    } catch (e) {
      console.warn('缓存保存失败:', e);
    }
  }

  const fetchGitHubInfo = (repo, cache) => {
    return new Promise((resolve) => {
      if (cache[repo]) {
        return resolve(cache[repo].data);
      }

      const token = GM_getValue(TOKEN_KEY, '');
      const headers = {
        'Accept': 'application/vnd.github.v3+json',
        'User-Agent': 'Hexo-Plugin-Enhancer/0.1'
      };
      
      if (token) {
        headers['Authorization'] = `token ${token}`;
      }

      GM_xmlhttpRequest({
        method: 'GET',
        url: GITHUB_API + repo,
        headers: headers,
        timeout: 15000,
        onload: function (response) {
          if (response.status === 200) {
            try {
              const data = JSON.parse(response.responseText);
              const result = {
                stars: data.stargazers_count || 0,
                updated: data.pushed_at || data.updated_at || ''
              };

              cache[repo] = { timestamp: Date.now(), data: result };
              resolve(result);
            } catch (e) {
              console.warn(`解析 ${repo} 数据失败:`, e);
              resolve(null);
            }
          } else if (response.status === 403) {
            console.warn(`API限制达到上限 ${repo}: ${response.status}`);
            resolve(null);
          } else {
            console.warn(`获取 ${repo} 信息失败: ${response.status}`);
            resolve(null);
          }
        },
        onerror: (error) => {
          console.warn(`请求 ${repo} 失败:`, error);
          resolve(null);
        },
        ontimeout: () => {
          console.warn(`请求 ${repo} 超时`);
          resolve(null);
        }
      });
    });
  };

  let loadingBox = null;

  function showLoadingMessage(total = 0, current = 0) {
    if (!loadingBox) {
      loadingBox = document.createElement('div');
      loadingBox.id = 'hexo-plugin-loading-info';
      loadingBox.style.cssText = `
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        background: linear-gradient(90deg, #667eea, #764ba2);
        color: #fff;
        padding: 12px;
        font-size: 14px;
        z-index: 9999;
        text-align: center;
        box-shadow: 0 2px 10px rgba(0,0,0,0.3);
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      `;
      document.body.appendChild(loadingBox);
    }
    const percentage = total > 0 ? Math.round((current / total) * 100) : 0;
    const token = GM_getValue(TOKEN_KEY, '');
    const mode = token ? '高速模式(并发50个)' : '标准模式(并发20个)';
    loadingBox.innerHTML = `
      <div>${SVG_ICONS.rocket} 正在抓取 GitHub 信息...(${current} / ${total})- ${percentage}% | ${mode}</div>
      <div style="background: rgba(255,255,255,0.3); height: 4px; margin-top: 8px; border-radius: 2px;">
        <div style="background: #fff; height: 100%; width: ${percentage}%; border-radius: 2px; transition: width 0.3s ease;"></div>
      </div>
    `;
  }

  function showFinishedMessage(total, cached = 0) {
    if (loadingBox) {
      loadingBox.style.background = 'linear-gradient(90deg, #56ab2f, #a8e6cf)';
      loadingBox.innerHTML = `${SVG_ICONS.rocket} 抓取完成(共 ${total} 项,缓存命中 ${cached} 项)`;
      setTimeout(() => {
        if (loadingBox && loadingBox.parentNode) {
          loadingBox.remove();
          loadingBox = null;
        }
      }, 3000);
    }
  }

  async function enhance() {
    const pluginElements = Array.from(document.querySelectorAll(PLUGIN_SELECTOR));
    if (!pluginElements.length) {
      console.warn('未找到插件元素');
      return;
    }

    const total = pluginElements.length;
    let current = 0;
    let cached = 0;

    const cache = loadCache();
    showLoadingMessage(total, current);

    const token = GM_getValue(TOKEN_KEY, '');
    const concurrency = token ? 50 : 20;

    const pluginInfos = [];
    for (const el of pluginElements) {
      const a = el.querySelector('a[href*="github.com"]');
      const repo = a ? extractRepo(a.href) : null;
      if (repo) {
        pluginInfos.push({
          element: el,
          repo: repo,
          stars: 0,
          updated: ''
        });
      }
    }

    const processBatch = async (batch) => {
      const promises = batch.map(async (info) => {
        const wasCached = !!cache[info.repo];
        const data = await fetchGitHubInfo(info.repo, cache);
        
        if (wasCached) cached++;
        
        if (data) {
          info.stars = data.stars;
          info.updated = data.updated;

          let extra = info.element.querySelector('.github-info');
          if (!extra) {
            extra = document.createElement('div');
            extra.className = 'github-info';
            extra.style.cssText = `
              font-size: 12px;
              color: #666;
              margin-top: 5px;
              padding: 4px 8px;
              background: #f8f9fa;
              border-radius: 4px;
              border-left: 3px solid #007bff;
            `;
            info.element.appendChild(extra);
          }
          
          const updatedDate = data.updated ? new Date(data.updated).toLocaleDateString('zh-CN') : '未知';
          extra.innerHTML = `
            <span style="color: #ffc107; vertical-align: middle;">${SVG_ICONS.star}</span> ${data.stars} | 
            <span style="color: #28a745; vertical-align: middle;">${SVG_ICONS.calendar}</span> ${updatedDate}
          `;
        }

        plugins.push(info);
        current++;
        showLoadingMessage(total, current);
        return info;
      });

      return Promise.all(promises);
    };

    for (let i = 0; i < pluginInfos.length; i += concurrency) {
      const batch = pluginInfos.slice(i, i + concurrency);
      await processBatch(batch);
      
      // 每批之间稍微延迟,避免请求过于密集
      if (i + concurrency < pluginInfos.length) {
        await sleep(token ? 100 : 500);
      }
      
      saveCache(cache);
    }

    showFinishedMessage(total, cached);
    injectSortControls();
  }

  function injectControls() {
    const cacheStatus = checkCacheExpiry();
    
    const floatingBall = document.createElement('div');
    floatingBall.id = 'hexo-plugin-floating-ball';
    
    const ballContent = document.createElement('div');
    ballContent.style.cssText = `
      display: flex;
      align-items: center;
      justify-content: center;
      width: 100%;
      height: 100%;
      position: relative;
    `;
    
    const mainIcon = document.createElement('div');
    mainIcon.innerHTML = SVG_ICONS.settings;
    mainIcon.style.cssText = `
      color: white;
      display: flex;
      align-items: center;
      justify-content: center;
    `;
    ballContent.appendChild(mainIcon);
    
    if (cacheStatus.hasExpired) {
      const badge = document.createElement('div');
      badge.innerHTML = cacheStatus.expired;
      badge.style.cssText = `
        position: absolute;
        top: -5px;
        right: -5px;
        background: #ff6b6b;
        color: white;
        border-radius: 50%;
        width: 18px;
        height: 18px;
        font-size: 10px;
        font-weight: bold;
        display: flex;
        align-items: center;
        justify-content: center;
        border: 2px solid white;
      `;
      ballContent.appendChild(badge);
    }
    
    floatingBall.appendChild(ballContent);
    floatingBall.title = cacheStatus.hasExpired ? 
      `Hexo 插件增强设置 (${cacheStatus.expired}项缓存已过期)` : 
      'Hexo 插件增强设置';
    
    floatingBall.style.cssText = `
      position: fixed;
      top: 50%;
      right: 20px;
      width: 50px;
      height: 50px;
      background: linear-gradient(135deg, ${THEME_COLOR}, #4A9EFF);
      border-radius: 50%;
      cursor: pointer;
      z-index: 10000;
      box-shadow: 0 4px 12px rgba(90, 200, 250, 0.3);
      transition: all 0.3s ease;
      user-select: none;
    `;
    
    // 拖拽功能
    let isDragging = false;
    let hasDragged = false;
    let startX, startY, startLeft, startTop;
    let mouseDownTime = 0;
    const DRAG_THRESHOLD = 5;
    const CLICK_TIME_THRESHOLD = 200;
    
    floatingBall.addEventListener('mousedown', (e) => {
      mouseDownTime = Date.now();
      isDragging = false;
      hasDragged = false;
      startX = e.clientX;
      startY = e.clientY;
      const rect = floatingBall.getBoundingClientRect();
      startLeft = rect.left;
      startTop = rect.top;
      e.preventDefault();
    });
    
    document.addEventListener('mousemove', (e) => {
      if (mouseDownTime === 0) return;
      
      const deltaX = e.clientX - startX;
      const deltaY = e.clientY - startY;
      const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
      
      // 只有移动距离超过阈值才开始拖拽
      if (distance > DRAG_THRESHOLD && !isDragging) {
        isDragging = true;
        hasDragged = true;
        floatingBall.style.cursor = 'grabbing';
      }
      
      if (isDragging) {
        let newLeft = startLeft + deltaX;
        let newTop = startTop + deltaY;
        
        // 边界限制
        const maxLeft = window.innerWidth - 50;
        const maxTop = window.innerHeight - 50;
        
        newLeft = Math.max(0, Math.min(newLeft, maxLeft));
        newTop = Math.max(0, Math.min(newTop, maxTop));
        
        floatingBall.style.left = newLeft + 'px';
        floatingBall.style.top = newTop + 'px';
        floatingBall.style.right = 'auto';
        floatingBall.style.transform = 'none';
      }
    });
    
    document.addEventListener('mouseup', () => {
      if (mouseDownTime > 0) {
        const clickDuration = Date.now() - mouseDownTime;
        
        // 重置状态
        mouseDownTime = 0;
        if (isDragging) {
          isDragging = false;
          floatingBall.style.cursor = 'pointer';
        }
        
        if (!hasDragged && clickDuration < CLICK_TIME_THRESHOLD) {
          // 延迟触发点击,确保拖拽状态已重置
          setTimeout(() => {
            floatingBall.dispatchEvent(new Event('ballclick'));
          }, 10);
        }
      }
    });
    
    floatingBall.addEventListener('mouseenter', () => {
      if (!isDragging) {
        const currentTransform = floatingBall.style.transform;
        if (currentTransform.includes('translateY(-50%)')) {
          floatingBall.style.transform = 'translateY(-50%) scale(1.1)';
        } else {
          floatingBall.style.transform = 'scale(1.1)';
        }
        floatingBall.style.boxShadow = '0 6px 20px rgba(90, 200, 250, 0.5)';
      }
    });
    
    floatingBall.addEventListener('mouseleave', () => {
      if (!isDragging) {
        const currentTransform = floatingBall.style.transform;
        if (currentTransform.includes('translateY(-50%)')) {
          floatingBall.style.transform = 'translateY(-50%)';
        } else {
          floatingBall.style.transform = 'none';
        }
        floatingBall.style.boxShadow = '0 4px 12px rgba(90, 200, 250, 0.3)';
      }
    });
    
    document.body.appendChild(floatingBall);
    
    // 立即显示缓存数据
    const displayedCount = displayCachedData();
    if (displayedCount > 0) {
      console.log(`已显示 ${displayedCount} 项缓存数据`);
      // 延迟启用排序控制,确保DOM已完全渲染
      setTimeout(() => {
        injectSortControls();
      }, 100);
    }

    const container = document.createElement('div');
    container.id = 'hexo-plugin-controls';
    container.style.cssText = `
      position: fixed;
      top: 50%;
      right: 80px;
      width: 400px;
      max-height: 80vh;
      overflow-y: auto;
      padding: 20px;
      background: white;
      border-radius: 12px;
      border: 1px solid #dee2e6;
      box-shadow: 0 8px 32px rgba(90, 200, 250, 0.15);
      z-index: 9999;
      transform: translateY(-50%) scale(0);
      opacity: 0;
      transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
      transform-origin: right center;
    `;
    
    const title = document.createElement('div');
    title.innerHTML = 'Hexo 插件增强设置';
    title.style.cssText = `
      font-size: 18px;
      font-weight: 600;
      color: #333;
      margin-bottom: 20px;
      text-align: center;
      border-bottom: 2px solid ${THEME_COLOR};
      padding-bottom: 10px;
    `;
    container.appendChild(title);
    
    document.body.appendChild(container);
    
    let isOpen = false;
    floatingBall.addEventListener('ballclick', () => {
      isOpen = !isOpen;
      if (isOpen) {
        container.style.transform = 'translateY(-50%) scale(1)';
        container.style.opacity = '1';
        mainIcon.innerHTML = SVG_ICONS.close;
        floatingBall.title = '点击关闭设置面板';
      } else {
        container.style.transform = 'translateY(-50%) scale(0)';
        container.style.opacity = '0';
        mainIcon.innerHTML = SVG_ICONS.settings;
        floatingBall.title = cacheStatus.hasExpired ? 
          `Hexo 插件增强设置 (${cacheStatus.expired}项缓存已过期)` : 
          'Hexo 插件增强设置';
      }
    });
    
    document.addEventListener('click', (e) => {
      if (isOpen && !container.contains(e.target) && !floatingBall.contains(e.target)) {
        isOpen = false;
        container.style.transform = 'translateY(-50%) scale(0)';
        container.style.opacity = '0';
        mainIcon.innerHTML = SVG_ICONS.settings;
        floatingBall.title = cacheStatus.hasExpired ? 
          `Hexo 插件增强设置 (${cacheStatus.expired}项缓存已过期)` : 
          'Hexo 插件增强设置';
      }
    });
    
    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape' && isOpen) {
        isOpen = false;
        container.style.transform = 'translateY(-50%) scale(0)';
        container.style.opacity = '0';
        mainIcon.innerHTML = SVG_ICONS.settings;
        floatingBall.title = cacheStatus.hasExpired ? 
          `Hexo 插件增强设置 (${cacheStatus.expired}项缓存已过期)` : 
          'Hexo 插件增强设置';
      }
    });

    const tokenSection = document.createElement('div');
    tokenSection.style.cssText = `
      margin-bottom: 20px;
      padding: 15px;
      background: #f8f9fa;
      border-radius: 8px;
      border-left: 4px solid ${THEME_COLOR};
    `;
    
    const tokenLabel = document.createElement('label');
    tokenLabel.innerHTML = `${SVG_ICONS.key} GitHub Token`;
    tokenLabel.style.cssText = `
      display: flex;
      align-items: center;
      gap: 8px;
      font-weight: 600;
      margin-bottom: 8px;
      color: #495057;
      font-size: 14px;
    `;
    
    const tokenDesc = document.createElement('div');
    tokenDesc.textContent = '不设置每小时每IP只能请求60次api,设置后可以提高到5,000次';
    tokenDesc.style.cssText = `
      font-size: 12px;
      color: #6c757d;
      margin-bottom: 10px;
    `;
    tokenSection.appendChild(tokenLabel);
    tokenSection.appendChild(tokenDesc);
    
    const tokenInput = document.createElement('input');
    tokenInput.type = 'password';
    tokenInput.placeholder = '输入GitHub Personal Access Token';
    tokenInput.value = GM_getValue(TOKEN_KEY, '');
    tokenInput.style.cssText = `
      width: 100%;
      padding: 8px 12px;
      border: 1px solid #ced4da;
      border-radius: 6px;
      margin-bottom: 10px;
      font-size: 14px;
      box-sizing: border-box;
      transition: border-color 0.3s;
    `;
    tokenInput.addEventListener('focus', () => {
      tokenInput.style.borderColor = THEME_COLOR;
    });
    tokenInput.addEventListener('blur', () => {
      tokenInput.style.borderColor = '#ced4da';
    });
    tokenSection.appendChild(tokenInput);
    
    const saveTokenBtn = document.createElement('button');
    saveTokenBtn.textContent = '保存Token';
    saveTokenBtn.style.cssText = `
      padding: 8px 16px;
      background: linear-gradient(135deg, ${THEME_COLOR}, #4A9FE7);
      color: white;
      border: none;
      border-radius: 6px;
      cursor: pointer;
      font-size: 14px;
      margin-right: 10px;
      transition: all 0.3s;
    `;
    
    saveTokenBtn.addEventListener('mouseenter', () => {
      saveTokenBtn.style.transform = 'translateY(-1px)';
      saveTokenBtn.style.boxShadow = `0 4px 12px rgba(90, 200, 250, 0.3)`;
    });
    
    saveTokenBtn.addEventListener('mouseleave', () => {
      saveTokenBtn.style.transform = 'none';
      saveTokenBtn.style.boxShadow = 'none';
    });
    saveTokenBtn.addEventListener('click', () => {
      GM_setValue(TOKEN_KEY, tokenInput.value.trim());
      alert('Token已保存!');
    });
    tokenSection.appendChild(saveTokenBtn);
    
    const tokenHelp = document.createElement('div');
    tokenHelp.innerHTML = `
      <small style="color: #6c757d; line-height: 1.4;">
        💡 在 <a href="https://github.com/settings/tokens" target="_blank" style="color: #007bff;">GitHub Settings</a> 创建Token,无需任何权限,填写好名称描述,即可提高API限制
      </small>
    `;
    tokenSection.appendChild(tokenHelp);
    
    container.appendChild(tokenSection);

    const controlSection = document.createElement('div');
    controlSection.style.cssText = `
      margin-bottom: 20px;
      padding: 15px;
      background: #f8f9fa;
      border-radius: 8px;
      border-left: 4px solid ${THEME_COLOR};
    `;
    
    const startBtn = document.createElement('button');
    startBtn.innerHTML = `${SVG_ICONS.rocket} 开始获取插件信息`;
    startBtn.style.cssText = `
      width: 100%;
      padding: 12px 16px;
      background: linear-gradient(135deg, ${THEME_COLOR}, #4A9FE7);
      color: white;
      border: none;
      border-radius: 6px;
      cursor: pointer;
      font-size: 14px;
      margin-bottom: 10px;
      transition: all 0.3s;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
    `;
    
    startBtn.addEventListener('mouseenter', () => {
      startBtn.style.transform = 'translateY(-2px)';
      startBtn.style.boxShadow = '0 4px 16px rgba(90, 200, 250, 0.4)';
    });
    
    startBtn.addEventListener('mouseleave', () => {
      startBtn.style.transform = 'none';
      startBtn.style.boxShadow = 'none';
    });
    startBtn.addEventListener('click', enhance);
    controlSection.appendChild(startBtn);
    
    const clearCacheBtn = document.createElement('button');
    clearCacheBtn.innerHTML = `${SVG_ICONS.trash} 清除缓存`;
    clearCacheBtn.style.cssText = `
      width: 100%;
      padding: 12px 16px;
      background: linear-gradient(135deg, #FF6B6B, #FF5252);
      color: white;
      border: none;
      border-radius: 6px;
      cursor: pointer;
      font-size: 14px;
      transition: all 0.3s;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
    `;
    
    clearCacheBtn.addEventListener('mouseenter', () => {
      clearCacheBtn.style.transform = 'translateY(-2px)';
      clearCacheBtn.style.boxShadow = '0 4px 16px rgba(255, 107, 107, 0.4)';
    });
    
    clearCacheBtn.addEventListener('mouseleave', () => {
      clearCacheBtn.style.transform = 'none';
      clearCacheBtn.style.boxShadow = 'none';
    });
    clearCacheBtn.addEventListener('click', () => {
      GM_setValue(CACHE_KEY, '{}');
      alert('缓存已清除!');
    });
    controlSection.appendChild(clearCacheBtn);
    
    container.appendChild(controlSection);

    const sortSection = document.createElement('div');
    sortSection.className = 'sort-section';
    sortSection.style.cssText = `
      padding: 15px;
      background: #f8f9fa;
      border-radius: 8px;
      border-left: 4px solid ${THEME_COLOR};
    `;
    
    const label = document.createElement('label');
    label.innerHTML = `${SVG_ICONS.sort} 排序方式`;
    label.style.cssText = `
      display: flex;
      align-items: center;
      gap: 8px;
      font-weight: 600;
      margin-bottom: 10px;
      color: #495057;
      font-size: 14px;
    `;
    sortSection.appendChild(label);

    const select = document.createElement('select');
    select.style.cssText = `
      width: 100%;
      padding: 8px 12px;
      border: 1px solid #ced4da;
      border-radius: 6px;
      background: white;
      font-size: 14px;
      cursor: pointer;
      box-sizing: border-box;
      transition: border-color 0.3s;
    `;
    select.addEventListener('focus', () => {
      select.style.borderColor = THEME_COLOR;
    });
    select.addEventListener('blur', () => {
      select.style.borderColor = '#ced4da';
    });
    select.innerHTML = `
      <option value="default">默认顺序</option>
      <option value="stars">⭐ Star 数(高到低)</option>
      <option value="updated">📅 更新时间(新到旧)</option>
    `;
    select.disabled = true;
    select.style.opacity = '0.5';
    
    select.addEventListener('change', () => {
      const val = select.value;
      
      const pluginElements = Array.from(document.querySelectorAll(PLUGIN_SELECTOR));
      if (pluginElements.length === 0) {
        console.warn('未找到插件元素,无法排序');
        return;
      }
      
      const pluginsWithData = pluginElements.map(el => {
        const a = el.querySelector('a[href*="github.com"]');
        const repo = a ? extractRepo(a.href) : null;
        const githubInfo = el.querySelector('.github-info');
        
        let stars = 0;
        let updated = '';
        
        if (githubInfo && repo) {
          // 从缓存或全局plugins数组中获取数据
          const cachedPlugin = plugins.find(p => p.repo === repo);
          if (cachedPlugin) {
            stars = cachedPlugin.stars || 0;
            updated = cachedPlugin.updated || '';
          } else {
            // 从缓存中获取数据
            const cache = loadCache();
            if (cache[repo]) {
              stars = cache[repo].data.stars || 0;
              updated = cache[repo].data.updated || '';
            }
          }
        }
        
        return {
          element: el,
          repo: repo,
          stars: stars,
          updated: updated
        };
      });
      
      // 根据选择的排序方式进行排序
      let sorted = [...pluginsWithData];
      if (val === 'stars') {
        sorted.sort((a, b) => b.stars - a.stars);
      } else if (val === 'updated') {
        sorted.sort((a, b) => {
          const dateA = new Date(a.updated || 0);
          const dateB = new Date(b.updated || 0);
          return dateB - dateA;
        });
      }
      
      const firstPlugin = pluginElements[0];
      const parent = firstPlugin.parentNode;
      
      if (parent) {
        sorted.forEach(info => {
          parent.appendChild(info.element);
        });
        console.log(`已按${val === 'stars' ? 'Star数' : val === 'updated' ? '更新时间' : '默认顺序'}排序`);
      } else {
        console.warn('未找到插件父容器,无法排序');
      }
    });

    sortSection.appendChild(select);
    container.appendChild(sortSection);

    const authorSection = document.createElement('div');
    authorSection.style.cssText = `
      padding: 12px 15px 0;
      text-align: center;
      margin-top: 10px;
    `;
    
    const authorInfo = document.createElement('div');
    authorInfo.innerHTML = 'Made with ❤️ by 二次蓝 <a href="https://blog.ercilan.cn" target="_blank" style="color: #007bff; text-decoration: none; font-weight: 500;">https://blog.ercilan.cn</a>';
    authorInfo.style.cssText = `
      font-size: 12px;
      color: #6c757d;
      line-height: 1.4;
    `;
    
    authorSection.appendChild(authorInfo);
    container.appendChild(authorSection);
  }

  function injectSortControls() {
    const container = document.querySelector('#hexo-plugin-controls');
    if (!container) return;
    
    const sortSection = container.querySelector('.sort-section');
    if (!sortSection) return;
    
    const select = sortSection.querySelector('select');
    if (!select) return;
    
    if (plugins.length > 0) {
      select.disabled = false;
      select.style.opacity = '1';
      console.log('排序控制已启用,可排序插件数量:', plugins.length);
    } else {
      const cache = loadCache();
      const cacheCount = Object.keys(cache).length;
      if (cacheCount > 0) {
        select.disabled = false;
        select.style.opacity = '1';
        console.log('基于缓存数据启用排序控制,缓存项数量:', cacheCount);
      }
    }
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => {
      setTimeout(injectControls, 500);
    });
  } else {
    setTimeout(injectControls, 500);
  }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址