GitHub Style

智能高亮 GitHub 活跃仓库:集成 CSS 视觉排序、配置导入/导出,彻底解决 React 冲突与时间闪烁。

Från och med 2026-07-20. Se den senaste versionen.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name           GitHub Style
// @namespace      http://tampermonkey.net/
// @version        1.2.0
// @description    智能高亮 GitHub 活跃仓库:集成 CSS 视觉排序、配置导入/导出,彻底解决 React 冲突与时间闪烁。
// @author         XiMenChuiXue
// @license        MIT
// @icon           https://raw.githubusercontent.com/rational-stars/picgo/refs/heads/main/avatar.jpg
// @match          https://github.com/*
// @exclude        https://github.com/settings/*
// @exclude        https://github.com/notifications/*
// @exclude        https://github.com/marketplace/*
// @exclude        https://github.com/orgs/*
// @exclude        https://github.com/topics/*
// @exclude        https://github.com/collections/*
// @exclude        https://github.com/features/*
// @exclude        https://github.com/pricing*
// @exclude        https://github.com/login*
// @exclude        https://github.com/join*
// @noframes
// @grant          GM_registerMenuCommand
// @grant          GM_setValue
// @grant          GM_getValue
// @grant          GM_addStyle
// @grant          GM_getResourceText
// @run-at         document-start
// @require        https://code.jquery.com/jquery-3.6.0.min.js#sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=
// @require        https://cdn.jsdelivr.net/npm/sweetalert2@11
// @require        https://cdn.jsdelivr.net/npm/@simonwep/[email protected]/dist/pickr.min.js
// @require        https://cdn.jsdelivr.net/npm/[email protected]/build/global/luxon.min.js
// @resource       PICKR_CSS https://cdn.jsdelivr.net/npm/@simonwep/[email protected]/dist/themes/classic.min.css
// ==/UserScript==

/* global luxon, Pickr, Swal, $ */
(function () {
    'use strict';

    const DateTime = luxon.DateTime;

    // ==================== 修复 #12: GM_getResourceText 容错 ====================
    try {
        const pickrCss = GM_getResourceText('PICKR_CSS');
        if (pickrCss) GM_addStyle(pickrCss);
    } catch (e) {
        console.warn('GitHub Freshness: Failed to load Pickr CSS resource', e);
    }

    // ==================== Styles ====================
    GM_addStyle(`
      .swal2-popup.swal2-modal.swal2-show {
          color: #FFF; border-radius: 20px; background: #31b96c;
          box-shadow: 8px 8px 16px #217e49, -8px -8px 16px #41f48f;
      }
      .formatted-date-span, .custom-time-format {
          white-space: nowrap !important; display: inline-block !important;
          font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif !important;
          font-size: 14px !important; vertical-align: middle;
      }
      div[role="row"] > div:last-child, td[role="gridcell"]:last-child {
          min-width: 135px !important; text-align: right !important; display: flex !important;
          align-items: center; justify-content: flex-end; padding-right: 10px !important;
      }
      #swal2-title a { display: inline-block; height: 40px; margin-right: 10px; border-radius: 10px; overflow: hidden; color: #fff; }
      #swal2-title { display: flex !important; justify-content: center; align-items: center; }

      .row-box select {
          border: 2px solid rgba(255, 255, 255, 0.3); border-radius: 8px;
          background-color: rgba(0, 0, 0, 0.2) !important; color: #fff !important;
          padding: 8px 12px; font-size: 14px; cursor: pointer; outline: none; min-width: 120px;
          -webkit-appearance: none; -moz-appearance: none; appearance: none;
          background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
          background-repeat: no-repeat; background-position: right 8px center; background-size: 16px; padding-right: 32px;
      }
      .row-box select:hover { border-color: rgba(255, 255, 255, 0.5); background-color: rgba(0, 0, 0, 0.3) !important; }
      .row-box select:focus { border-color: rgba(255, 255, 255, 0.8); background-color: rgba(0, 0, 0, 0.4) !important; box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1); }
      .row-box select option { background-color: #2d3748 !important; color: #fff !important; padding: 8px; }
      .row-box select:-moz-focusring { color: transparent; text-shadow: 0 0 0 #fff; }

      .row-box { display: flex; margin: 20px 25px; align-items: center; justify-content: space-between; }
      .row-box .swal2-input { height: 40px; background-color: rgba(0, 0, 0, 0.2) !important; color: #fff !important; border: 2px solid rgba(255, 255, 255, 0.3); border-radius: 8px; }
      .row-box .swal2-input:focus { border-color: rgba(255, 255, 255, 0.8); box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1); }
      .row-box label { margin-right: 10px; color: #fff; font-weight: 500; }
      .row-box main input { background: rgba(15, 172, 83, 1); width: 70px; border: unset; box-shadow: unset; text-align: right; margin: 0; }
      .row-box main { display: flex; align-items: center; gap: 10px; }
      .freshness-stars { padding: 8px; }
      .freshness-updated { margin-left: 5px; }
      .github-freshness-loading { opacity: 0.6; pointer-events: none; }

      input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; accent-color: #0fac53; }
      input[type="number"], input[type="password"] {
          background-color: rgba(0, 0, 0, 0.2) !important; color: #fff !important;
          border: 2px solid rgba(255, 255, 255, 0.3) !important; border-radius: 8px; padding: 8px; text-align: center;
      }
      input[type="password"] { min-width: 200px; text-align: left; padding: 8px 12px; }
      input[type="number"]:focus, input[type="password"]:focus { border-color: rgba(255, 255, 255, 0.8) !important; outline: none; box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1); }

      #RESET_DEFAULTS { background-color: #e53e3e !important; color: white !important; border: none !important; padding: 10px 24px !important; border-radius: 8px !important; cursor: pointer; font-weight: 500; transition: all 0.2s; }
      #RESET_DEFAULTS:hover { background-color: #c53030 !important; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(229, 62, 62, 0.4); }
      .swal2-confirm { background-color: #0fac53 !important; border-radius: 8px !important; }
      .swal2-cancel { background-color: #718096 !important; border-radius: 8px !important; }

      relative-time.custom-time-replaced { display: none !important; }

      .gh-freshness-flex-sort {
          display: flex !important;
          flex-direction: column !important;
      }
      .gh-freshness-flex-sort > [role="row"],
      .gh-freshness-flex-sort > [data-testid="file-list-row"],
      .gh-freshness-flex-sort > .Box-row {
          width: 100% !important;
          box-sizing: border-box !important;
          margin-left: 0 !important;
          margin-right: 0 !important;
          flex-shrink: 0 !important;
      }
    `);

    // ==================== Panel DOM ====================
    const PanelDom = `
      <div class="row-box">
          <label for="THEME-select">主题设置:</label>
          <main>
              <select tabindex="-1" id="THEME-select" class="swal2-input">
                  <option value="light">☀️ Light</option>
                  <option value="dark">🌙 Dark</option>
              </select>
          </main>
      </div>
      <div class="row-box">
          <label id="TIME_BOUNDARY-label">时间阈值:</label>
          <main>
              <input id="TIME_BOUNDARY-number" type="number" class="swal2-input" value="" min="1" max="999" maxlength="3" pattern="\\d{1,3}">
              <select tabindex="-1" id="TIME_BOUNDARY-select" class="swal2-input">
                  <option value="day">📅 日</option>
                  <option value="week">📆 周</option>
                  <option value="month">🗓️ 月</option>
                  <option value="year">📊 年</option>
              </select>
          </main>
      </div>
      <div class="row-box">
          <div><label id="BGC-label">🎨 背景颜色:</label><input type="checkbox" id="BGC-enabled"></div>
          <main>
              <span id="BGC-highlight-color-value"><div id="BGC-highlight-color-pickr"></div></span>
              <span id="BGC-grey-color-value"><div id="BGC-grey-color-pickr"></div></span>
          </main>
      </div>
      <div class="row-box">
          <div><label id="FONT-label">🔤 字体颜色:</label><input type="checkbox" id="FONT-enabled"></div>
          <main>
              <span id="FONT-highlight-color-value"><div id="FONT-highlight-color-pickr"></div></span>
              <span id="FONT-grey-color-value"><div id="FONT-grey-color-pickr"></div></span>
          </main>
      </div>
      <div class="row-box">
          <div><label id="DIR-label">📁 文件夹颜色:</label><input type="checkbox" id="DIR-enabled"></div>
          <main>
              <span id="DIR-highlight-color-value"><div id="DIR-highlight-color-pickr"></div></span>
              <span id="DIR-grey-color-value"><div id="DIR-grey-color-pickr"></div></span>
          </main>
      </div>
      <div class="row-box">
          <div><label id="TIME_FORMAT-label">🕐 时间格式化:</label><input type="checkbox" id="TIME_FORMAT-enabled"></div>
      </div>
      <div class="row-box">
          <div><label id="SORT-label">🔀 文件排序 (CSS视觉排序):</label><input type="checkbox" id="SORT-enabled"></div>
          <main>
              <select tabindex="-1" id="SORT-select" class="swal2-input">
                  <option value="asc">⬆️ 时间正序</option>
                  <option value="desc">⬇️ 时间倒序</option>
              </select>
          </main>
      </div>
      <div class="row-box">
          <label for="CURRENT_THEME-select">🌓 当前主题:</label>
          <main>
              <select tabindex="-1" id="CURRENT_THEME-select" class="swal2-input">
                  <option value="auto">⚡ Auto</option>
                  <option value="light">☀️ Light</option>
                  <option value="dark">🌙 Dark</option>
              </select>
          </main>
      </div>
      <div class="row-box">
          <div>
              <label id="AWESOME-label"><a target="_blank" href="https://github.com/settings/tokens" style="color: #fff; text-decoration: underline;">🔑 AWESOME token:</a></label>
              <input type="checkbox" id="AWESOME-enabled">
          </div>
          <main>
              <input id="AWESOME_TOKEN" type="password" class="swal2-input" value="" placeholder="ghp_xxx 或 github_pat_xxx">
          </main>
      </div>

      <div class="row-box" style="justify-content: center; gap: 15px; margin-top: 25px; flex-wrap: wrap;">
          <button type="button" id="EXPORT_CONFIG" class="swal2-styled" style="background-color: #3182ce !important; border-radius: 8px !important; margin: 5px; padding: 10px 20px !important;">📤 导出配置</button>
          <button type="button" id="IMPORT_CONFIG" class="swal2-styled" style="background-color: #805ad5 !important; border-radius: 8px !important; margin: 5px; padding: 10px 20px !important;">📥 导入配置</button>
      </div>

      <div class="row-box" style="justify-content: center; margin-top: 15px;">
          <button type="button" id="RESET_DEFAULTS" class="swal2-styled">🔄 恢复默认设置</button>
      </div>
      <p style="font-size: 0.85em; opacity: 0.9; text-align: center; margin-top: 15px; color: rgba(255,255,255,0.8);">
          💡 提示:排序采用 CSS Flex 黑科技,不破坏 React 架构。导入/导出不包含 Token,请单独配置。
      </p>
    `;

    // ==================== Default Config ====================
    const default_THEME = {
        BGC: { highlightColor: 'rgba(15, 172, 83, 1)', greyColor: 'rgba(245, 245, 245, 0.24)', isEnabled: true },
        TIME_BOUNDARY: { number: 30, select: 'day' },
        FONT: { highlightColor: 'rgba(252, 252, 252, 1)', greyColor: 'rgba(0, 0, 0, 1)', isEnabled: true },
        DIR: { highlightColor: 'rgba(15, 172, 83, 1)', greyColor: 'rgba(154, 154, 154, 1)', isEnabled: true },
        SORT: { select: 'desc', isEnabled: true },
        AWESOME: { isEnabled: false },
        TIME_FORMAT: { isEnabled: true },
    };

    // ==================== 修复 #3: 配置解析容错 ====================
    function safeParseConfig() {
        try {
            const raw = GM_getValue('config_JSON', null);
            if (!raw) return null;
            const parsed = JSON.parse(raw);
            if (!parsed || typeof parsed !== 'object') return null;
            if (!parsed.light || !parsed.dark) return null;
            for (const themeKey of ['light', 'dark']) {
                const t = parsed[themeKey];
                if (!t.BGC || !t.TIME_BOUNDARY || !t.FONT || !t.DIR || !t.SORT || !t.AWESOME || !t.TIME_FORMAT) {
                    return null;
                }
            }
            return parsed;
        } catch (e) {
            console.warn('GitHub Freshness: Config parse failed, using defaults', e);
            return null;
        }
    }

    let CURRENT_THEME = GM_getValue('CURRENT_THEME', 'light');
    let AWESOME_TOKEN = GM_getValue('AWESOME_TOKEN', '');
    let THEME_TYPE = getThemeType();
    let config_JSON = safeParseConfig() || {
        light: JSON.parse(JSON.stringify(default_THEME)),
        dark: JSON.parse(JSON.stringify(default_THEME))
    };
    let THEME = config_JSON[THEME_TYPE] || JSON.parse(JSON.stringify(default_THEME));

    // ==================== 修复 #9: API 缓存加上限 (LRU) + 修复 #5: 定时器去重 ====================
    const API_CACHE_MAX = 100;
    const apiCache = new Map();
    const cacheTimers = new Map();
    const apiQueue = [];
    let apiProcessing = false;
    const API_DELAY = 100;

    function apiCacheSet(key, value) {
        if (apiCache.size >= API_CACHE_MAX) {
            const firstKey = apiCache.keys().next().value;
            apiCache.delete(firstKey);
            if (cacheTimers.has(firstKey)) {
                clearTimeout(cacheTimers.get(firstKey));
                cacheTimers.delete(firstKey);
            }
        }
        // 修复 #5: 清除同一 key 的旧定时器
        if (cacheTimers.has(key)) {
            clearTimeout(cacheTimers.get(key));
        }
        apiCache.set(key, value);
        cacheTimers.set(key, setTimeout(() => {
            apiCache.delete(key);
            cacheTimers.delete(key);
        }, 5 * 60 * 1000));
    }

    const configPickr = {
        theme: 'classic',
        components: {
            preview: true, opacity: true, hue: true,
            interaction: { rgba: true, input: true, clear: true, save: true },
        },
    };

    // ==================== 修复 #2: Pickr 实例管理 ====================
    const pickrInstances = {};

    function destroyPickr(key) {
        if (pickrInstances[key]) {
            try {
                pickrInstances[key].destroyAndRemove();
            } catch (e) { /* ignore */ }
            delete pickrInstances[key];
        }
    }

    function destroyAllPickrs() {
        Object.keys(pickrInstances).forEach(destroyPickr);
    }

    // ==================== Utility Functions ====================
    function getThemeType() {
        let themeType = CURRENT_THEME;
        if (CURRENT_THEME === 'auto') {
            themeType = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
        }
        return themeType;
    }

    // ==================== 修复 #10: 使用 Pickr init 事件替代硬编码延时 ====================
    function initPickr(el_default) {
        const key = el_default.el;
        destroyPickr(key);

        const pickr = Pickr.create({ ...configPickr, ...el_default });
        pickrInstances[key] = pickr;

        pickr.on('init', (instance) => {
            const btn = instance.getRoot().button;
            if (btn && el_default.default) {
                btn.setAttribute('data-pickr-color', el_default.default);
            }
        });

        pickr.on('save', (color, instance) => {
            const hex = color.toHEXA().toString();
            instance.getRoot().button.setAttribute('data-pickr-color', hex);
            pickr.hide();
        });

        return pickr;
    }

    function getUpdatedThemeConfig() {
        let updatedTheme = {};
        for (const [themeKey, themeVal] of Object.entries(default_THEME)) {
            updatedTheme[themeKey] = {};
            for (let [key, val] of Object.entries(themeVal)) {
                if (key === 'highlightColor' || key === 'greyColor') {
                    const type = key === 'highlightColor' ? 'highlight' : 'grey';
                    const $btn = $(`#${themeKey}-${type}-color-value .pcr-button`);
                    let colorVal = $btn.attr('data-pickr-color');
                    if (!colorVal) colorVal = $btn.css('--pcr-color');
                    val = colorVal || val;
                } else if (key === 'isEnabled') {
                    val = $(`#${themeKey}-enabled`).prop('checked');
                } else if (key === 'number' || key === 'select') {
                    val = $(`#${themeKey}-${key}`).val();
                    if (key === 'number') val = parseInt(val) || 30;
                }
                updatedTheme[themeKey][key] = val;
            }
        }
        return updatedTheme;
    }

    // ==================== 修复 #6: handleData 同步更新 Pickr 实例颜色 ====================
    function handleData(theme) {
        if (!theme) return;
        for (const [themeKey, themeVal] of Object.entries(theme)) {
            for (const [key, val] of Object.entries(themeVal)) {
                if (key === 'highlightColor' || key === 'greyColor') {
                    const type = key === 'highlightColor' ? 'highlight' : 'grey';
                    const pickrKey = `#${themeKey}-${type}-color-pickr`;
                    const $btn = $(`#${themeKey}-${type}-color-value .pcr-button`);
                    if ($btn.length && val) {
                        $btn.css('--pcr-color', val);
                        $btn.attr('data-pickr-color', val);
                    }
                    // 修复 #6: 同步 Pickr 实例内部颜色
                    if (pickrInstances[pickrKey] && val) {
                        try {
                            pickrInstances[pickrKey].setColor(val, true);
                        } catch (e) { /* ignore */ }
                    }
                } else if (key === 'isEnabled') {
                    $(`#${themeKey}-enabled`).prop('checked', val);
                } else if (key === 'number' || key === 'select') {
                    $(`#${themeKey}-${key}`).val(val);
                }
            }
        }
    }

    function resetToDefaults() {
        handleData(default_THEME);
        initPickr({ el: '#BGC-highlight-color-pickr', default: default_THEME.BGC.highlightColor });
        initPickr({ el: '#BGC-grey-color-pickr', default: default_THEME.BGC.greyColor });
        initPickr({ el: '#FONT-highlight-color-pickr', default: default_THEME.FONT.highlightColor });
        initPickr({ el: '#FONT-grey-color-pickr', default: default_THEME.FONT.greyColor });
        initPickr({ el: '#DIR-highlight-color-pickr', default: default_THEME.DIR.highlightColor });
        initPickr({ el: '#DIR-grey-color-pickr', default: default_THEME.DIR.greyColor });
    }

    // ==================== Settings Panel ====================
    function createSettingsPanel() {
        Swal.fire({
            title: `<div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
                <span>🎨</span>
                <span>GitHub Freshness Settings</span>
            </div>`,
            html: PanelDom,
            focusConfirm: false,
            allowOutsideClick: false,
            background: '#31b96c',
            confirmButtonColor: '#0fac53',
            cancelButtonColor: '#718096',
            preConfirm: () => {
                const updated_THEME = getUpdatedThemeConfig();
                CURRENT_THEME = $('#CURRENT_THEME-select').val();
                AWESOME_TOKEN = $('#AWESOME_TOKEN').val().trim();

                const tokenRegex = /^(ghp_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9_]+)$/;
                if (updated_THEME.AWESOME.isEnabled && AWESOME_TOKEN && !AWESOME_TOKEN.match(tokenRegex)) {
                    Swal.showValidationMessage('❌ Token 格式不正确 (支持 ghp_ 或 github_pat_)');
                    return false;
                }

                config_JSON[$('#THEME-select').val()] = updated_THEME;
                GM_setValue('config_JSON', JSON.stringify(config_JSON));
                GM_setValue('CURRENT_THEME', CURRENT_THEME);
                GM_setValue('AWESOME_TOKEN', AWESOME_TOKEN);

                // ==================== 修复 #1: 保存后使用正确的当前主题 ====================
                THEME_TYPE = getThemeType();
                THEME = config_JSON[THEME_TYPE] || JSON.parse(JSON.stringify(default_THEME));
                GitHub_Freshness(THEME);

                Swal.fire({
                    position: 'top-center',
                    background: '#31b96c',
                    icon: 'success',
                    iconColor: '#fff',
                    title: '<span style="color: #fff;">✅ 保存成功!</span>',
                    showConfirmButton: false,
                    timer: 1000,
                    toast: true
                });
            },
            heightAuto: false,
            showCancelButton: true,
            confirmButtonText: '💾 保存',
            cancelButtonText: '❌ 取消',
            didOpen: () => {
                initSettings(THEME);

                $('#THEME-select').on('change', function () {
                    let selectedTheme = $(this).val();
                    let theme = config_JSON[selectedTheme] || JSON.parse(JSON.stringify(default_THEME));
                    handleData(theme);

                    initPickr({ el: '#BGC-highlight-color-pickr', default: theme.BGC.highlightColor });
                    initPickr({ el: '#BGC-grey-color-pickr', default: theme.BGC.greyColor });
                    initPickr({ el: '#FONT-highlight-color-pickr', default: theme.FONT.highlightColor });
                    initPickr({ el: '#FONT-grey-color-pickr', default: theme.FONT.greyColor });
                    initPickr({ el: '#DIR-highlight-color-pickr', default: theme.DIR.highlightColor });
                    initPickr({ el: '#DIR-grey-color-pickr', default: theme.DIR.greyColor });
                });

                $('#RESET_DEFAULTS').on('click', () => {
                    Swal.fire({
                        title: '🔄 确认恢复默认?',
                        text: '当前主题的设置将被重置为默认值',
                        icon: 'warning',
                        iconColor: '#f6e05e',
                        background: '#31b96c',
                        color: '#fff',
                        showCancelButton: true,
                        confirmButtonColor: '#e53e3e',
                        cancelButtonColor: '#718096',
                        confirmButtonText: '⚠️ 确认重置',
                        cancelButtonText: '✋ 取消'
                    }).then((result) => {
                        if (result.isConfirmed) {
                            resetToDefaults();
                            Swal.fire({
                                position: 'top-center',
                                background: '#31b96c',
                                icon: 'success',
                                iconColor: '#fff',
                                title: '<span style="color: #fff;">✅ 已重置为默认值</span>',
                                showConfirmButton: false,
                                timer: 800,
                                toast: true
                            });
                        }
                    });
                });

                // ==================== 导出配置 (排除 Token) ====================
                $('#EXPORT_CONFIG').on('click', () => {
                    const exportData = {
                        config_JSON: config_JSON,
                        CURRENT_THEME: CURRENT_THEME
                        // 注意:不包含 AWESOME_TOKEN
                    };
                    const b64 = btoa(unescape(encodeURIComponent(JSON.stringify(exportData))));

                    Swal.fire({
                        title: '📤 导出配置',
                        html: `
                            <p style="color: #fff; font-size: 14px; margin-bottom: 10px;">✅ 已排除 Token 等敏感信息,可安全分享。</p>
                            <textarea id="export-text" style="width: 100%; height: 150px; background: rgba(0,0,0,0.3); color: #fff; border: 1px solid rgba(255,255,255,0.3); border-radius: 8px; padding: 10px; font-family: monospace; resize: none;">${b64}</textarea>
                        `,
                        background: '#31b96c',
                        color: '#fff',
                        showCancelButton: true,
                        confirmButtonText: '📋 复制并关闭',
                        cancelButtonText: '❌ 关闭',
                        didOpen: () => {
                            const textarea = document.getElementById('export-text');
                            if (textarea) textarea.select();
                        },
                        preConfirm: () => {
                            const textarea = document.getElementById('export-text');
                            if (textarea) {
                                textarea.select();
                                if (navigator.clipboard) {
                                    navigator.clipboard.writeText(textarea.value).catch(() => document.execCommand('copy'));
                                } else {
                                    document.execCommand('copy');
                                }
                            }
                            return true;
                        }
                    });
                });

                // ==================== 导入配置 (排除 Token) ====================
                $('#IMPORT_CONFIG').on('click', () => {
                    Swal.fire({
                        title: '📥 导入配置',
                        html: `
                            <p style="color: #fff; font-size: 14px; margin-bottom: 10px;">请粘贴之前导出的配置码 (Base64),Token 不会被覆盖:</p>
                            <textarea id="import-text" style="width: 100%; height: 150px; background: rgba(0,0,0,0.3); color: #fff; border: 1px solid rgba(255,255,255,0.3); border-radius: 8px; padding: 10px; font-family: monospace; resize: none;" placeholder="粘贴配置码..."></textarea>
                        `,
                        background: '#31b96c',
                        color: '#fff',
                        showCancelButton: true,
                        confirmButtonText: '✅ 确认导入',
                        cancelButtonText: '❌ 取消',
                        preConfirm: () => {
                            const val = document.getElementById('import-text').value.trim();
                            if (!val) {
                                Swal.showValidationMessage('❌ 配置码不能为空');
                                return false;
                            }
                            try {
                                const jsonStr = decodeURIComponent(escape(atob(val)));
                                const importData = JSON.parse(jsonStr);
                                if (!importData.config_JSON) throw new Error('Invalid format');
                                return importData;
                            } catch (e) {
                                Swal.showValidationMessage('❌ 配置码解析失败,请检查格式');
                                return false;
                            }
                        }
                    }).then((result) => {
                        if (result.isConfirmed && result.value) {
                            const importData = result.value;

                            config_JSON = importData.config_JSON;
                            GM_setValue('config_JSON', JSON.stringify(config_JSON));

                            if (importData.CURRENT_THEME) {
                                CURRENT_THEME = importData.CURRENT_THEME;
                                GM_setValue('CURRENT_THEME', CURRENT_THEME);
                            }
                            // 注意:不导入 AWESOME_TOKEN,保留用户本地 Token

                            THEME_TYPE = getThemeType();
                            THEME = config_JSON[THEME_TYPE] || JSON.parse(JSON.stringify(default_THEME));

                            Swal.fire({
                                position: 'top-center',
                                background: '#31b96c',
                                icon: 'success',
                                iconColor: '#fff',
                                title: '<span style="color: #fff;">✅ 导入成功!正在刷新...</span>',
                                showConfirmButton: false,
                                timer: 1000,
                                toast: true
                            }).then(() => {
                                Swal.close();
                                createSettingsPanel();
                                runScript();
                            });
                        }
                    });
                });
            },
            didClose: () => {
                destroyAllPickrs();
            }
        });
    }

    function initSettings(theme) {
        if (!theme) theme = JSON.parse(JSON.stringify(default_THEME));

        initPickr({ el: '#BGC-highlight-color-pickr', default: theme.BGC.highlightColor });
        initPickr({ el: '#BGC-grey-color-pickr', default: theme.BGC.greyColor });
        initPickr({ el: '#FONT-highlight-color-pickr', default: theme.FONT.highlightColor });
        initPickr({ el: '#FONT-grey-color-pickr', default: theme.FONT.greyColor });
        initPickr({ el: '#DIR-highlight-color-pickr', default: theme.DIR.highlightColor });
        initPickr({ el: '#DIR-grey-color-pickr', default: theme.DIR.greyColor });

        $('#THEME-select').val(THEME_TYPE);
        $('#CURRENT_THEME-select').val(CURRENT_THEME);
        $('#AWESOME_TOKEN').val(AWESOME_TOKEN);
        handleData(theme);
    }

    // ==================== Element Styling ====================
    function setElementBGC(el, BGC, timeResult) {
        if (el.length && el[0] && BGC.isEnabled) {
            try { el[0].style.setProperty('background-color', timeResult ? BGC.highlightColor : BGC.greyColor, 'important'); } catch (e) {}
        }
    }

    function setElementDIR(el, DIR, timeResult) {
        if (el.length && DIR.isEnabled) {
            const color = timeResult ? DIR.highlightColor : DIR.greyColor;
            try {
                if (el[0]) {
                    el[0].style.setProperty('fill', color, 'important');
                    el[0].style.setProperty('stroke', color, 'important');
                }
                el.attr('fill', color); el.attr('stroke', color);
            } catch (e) {}
        }
    }

    function setElementFONT(el, FONT, timeResult) {
        if (el.length && el[0] && FONT.isEnabled) {
            try { el[0].style.setProperty('color', timeResult ? FONT.highlightColor : FONT.greyColor, 'important'); } catch (e) {}
        }
    }

    // ==================== Time Handling ====================
    function handleTime(time, time_boundary, type = 'ISO8601') {
        if (!time) return false;
        const { number, select } = time_boundary;
        let days = 0;
        switch (select) {
            case 'day': days = parseInt(number) || 30; break;
            case 'week': days = (parseInt(number) || 4) * 7; break;
            case 'month': days = (parseInt(number) || 1) * 30; break;
            case 'year': days = (parseInt(number) || 1) * 365; break;
            default: days = 30;
        }
        const thresholdDate = new Date();
        thresholdDate.setDate(thresholdDate.getDate() - days);
        thresholdDate.setHours(0, 0, 0, 0);

        let inputDate = null;
        if (type === 'UTC') {
            const formats = ["yyyy年M月d日 'GMT'Z HH:mm", "MMM d, yyyy, h:mm a 'GMT'Z", "MMM d, yyyy 'GMT'Z", "yyyy-MM-dd'T'HH:mm:ss'Z'", "yyyy-MM-dd HH:mm:ss"];
            for (const fmt of formats) {
                try {
                    const dt = DateTime.fromFormat(time, fmt, { zone: 'UTC' });
                    if (dt.isValid) { inputDate = dt.toJSDate(); break; }
                } catch (e) { continue; }
            }
            if (!inputDate) {
                const nativeDate = new Date(time);
                if (!isNaN(nativeDate.getTime())) inputDate = nativeDate;
            }
        } else { inputDate = new Date(time); }

        if (!inputDate || isNaN(inputDate.getTime())) return false;
        return inputDate >= thresholdDate;
    }

    // ==================== API ====================
    // ==================== 修复 #8: 正则去除多余反斜杠 ====================
    function toAPIUrl(href) {
        if (!href) return null;
        const match = href.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)/);
        if (!match) return null;
        const reserved = ['settings', 'notifications', 'marketplace', 'orgs', 'topics', 'collections', 'features', 'pricing', 'login', 'join', 'search', 'new', 'explore'];
        if (reserved.includes(match[1])) return null;
        return `https://api.github.com/repos/${match[1]}/${match[2]}`;
    }

    // ==================== 修复 #2: API 队列 callback 容错 + finally 解锁 ====================
    async function processApiQueue() {
        if (apiProcessing || apiQueue.length === 0) return;
        apiProcessing = true;
        try {
            while (apiQueue.length > 0) {
                const { apiHref, callback } = apiQueue.shift();
                if (apiCache.has(apiHref)) {
                    try { callback(apiCache.get(apiHref)); } catch (e) { /* ignore */ }
                    continue;
                }
                try {
                    const response = await fetch(apiHref, { headers: AWESOME_TOKEN ? { 'Authorization': `token ${AWESOME_TOKEN}` } : {} });
                    if (response.status === 403) {
                        try { callback(null); } catch (e) { /* ignore */ }
                    } else if (response.ok) {
                        const data = await response.json();
                        apiCacheSet(apiHref, data);
                        try { callback(data); } catch (e) { /* ignore */ }
                    } else {
                        try { callback(null); } catch (e) { /* ignore */ }
                    }
                } catch (error) {
                    try { callback(null); } catch (e) { /* ignore */ }
                }
                if (apiQueue.length > 0) await new Promise(resolve => setTimeout(resolve, API_DELAY));
            }
        } finally {
            apiProcessing = false;
        }
    }

    // ==================== 修复 #1: IntersectionObserver 单例化 ====================
    let awesomeObserver = null;

    function GitHub_FreshnessAwesome(theme) {
        if (awesomeObserver) {
            awesomeObserver.disconnect();
            awesomeObserver = null;
        }

        awesomeObserver = new IntersectionObserver((entries) => {
            entries.forEach(el => {
                if (el.isIntersecting && el.target.getAttribute('data-requested') !== 'true') {
                    const href = $(el.target).attr('href');
                    const apiHref = toAPIUrl(href);
                    if (!apiHref) return;
                    el.target.setAttribute('data-requested', 'true');
                    $(el.target).addClass('github-freshness-loading');
                    apiQueue.push({
                        apiHref: apiHref,
                        callback: (data) => {
                            $(el.target).removeClass('github-freshness-loading');
                            if (!data) return;
                            const timeResult = handleTime(data.updated_at, theme.TIME_BOUNDARY);
                            if (theme.AWESOME.isEnabled) {
                                const $existing = $(el.target).parent().find('.freshness-stars, .freshness-updated');
                                if ($existing.length === 0) {
                                    $(el.target).after(
                                        `<span class="freshness-stars">★${data.stargazers_count.toLocaleString()}</span>` +
                                        `<span class="freshness-updated">📅${formatDate(data.updated_at)}</span>`
                                    );
                                    $(el.target).css('padding', '0 12px');
                                }
                            }
                            setElementBGC($(el.target), theme.BGC, timeResult);
                            setElementFONT($(el.target), theme.FONT, timeResult);
                        }
                    });
                    processApiQueue();
                }
            });
        }, { threshold: 0.1, rootMargin: '50px' });

        // 修复 #8: 正则使用 [^/]+ 替代 [^\\/]+
        $('.Box-row a, .markdown-body a').each(function () {
            const href = $(this).attr('href');
            if (href && /^https:\/\/github\.com\/[^/]+\/[^/]+\/?$/.test(href)) {
                const apiHref = toAPIUrl(href);
                if (apiHref) {
                    awesomeObserver.observe(this);
                }
            }
        });
    }

    // ==================== Search Page ====================
    function GitHub_FreshnessSearchPage(theme) {
        const elements = $('relative-time[datetime], relative-time[title]');
        if (elements.length === 0) return;
        elements.each(function () {
            const $this = $(this);
            const datetime = $this.attr('datetime');
            const title = $this.attr('title');
            const timeStr = datetime || title;
            if (!timeStr) return;
            const isISO8601 = !!datetime;
            const timeResult = handleTime(timeStr, theme.TIME_BOUNDARY, isISO8601 ? 'ISO8601' : 'UTC');
            const BGC_element = $this.closest('div[data-testid*="results-list"], div[data-testid*="results-card"], li, article, .Box-row');
            setElementBGC(BGC_element, theme.BGC, timeResult);
            setElementFONT($this, theme.FONT, timeResult);
        });
    }

    // ==================== 排序辅助函数 ====================
    // ==================== 修复 #11: 增强特殊行检测 ====================
    function isSpecialRow(row) {
        if (!row) return false;
        if (row.querySelector('[data-testid="latest-commit-details"]')) return true;
        if (row.querySelector('[data-testid="latest-commit"]')) return true;
        if (row.querySelector('.react-directory-commit-row')) return true;
        if (row.getAttribute && row.getAttribute('role') === 'columnheader') return true;
        if (row.querySelector('[role="columnheader"]')) return true;
        if (!row.querySelector('relative-time[datetime]')) return true;
        const text = (row.textContent || '').trim();
        if (/^Latest commit/i.test(text)) return true;
        // 修复 #11: 更宽松的表头检测
        if (/^Name\b/i.test(text) && text.length < 100) return true;
        if (row.querySelector('input[type="checkbox"]') && !row.querySelector('relative-time[datetime]')) return true;
        return false;
    }

    function getRowTimestamp(row) {
        if (!row) return null;
        const timeEl = row.querySelector('relative-time[datetime]');
        if (!timeEl) return null;
        const datetime = timeEl.getAttribute('datetime');
        if (!datetime) return null;
        const ts = new Date(datetime).getTime();
        return isNaN(ts) ? null : ts;
    }

    // ==================== 修复 #4: 排序标志位,防止 MutationObserver 循环触发 ====================
    let isSorting = false;

    function applyCSSSortOrder(theme) {
        if (!theme || !theme.SORT || !theme.SORT.isEnabled) return;

        const containers = document.querySelectorAll(
            '#repos-file-tree, ' +
            '[role="rowgroup"], ' +
            '[role="grid"], ' +
            '[data-testid="repos-file-tree-container"], ' +
            '[data-testid="file-list-row-container"], ' +
            '.react-directory-truncate, ' +
            '.Box-body, ' +
            'table.files tbody, ' +
            'tbody'
        );

        if (containers.length === 0) return;

        isSorting = true;

        try {
            containers.forEach(container => {
                const tag = container.tagName.toUpperCase();
                let rows;

                if (tag === 'TBODY' || tag === 'TABLE') {
                    rows = Array.from(container.querySelectorAll(':scope > tr'));
                } else {
                    rows = Array.from(container.querySelectorAll(
                        ':scope > [role="row"], ' +
                        ':scope > [data-testid="file-list-row"], ' +
                        ':scope > .Box-row'
                    ));
                    if (rows.length === 0) {
                        rows = Array.from(container.querySelectorAll(':scope > div:has(relative-time[datetime])'));
                    }
                }

                if (rows.length <= 1) return;

                if (tag === 'TBODY' || tag === 'TABLE') {
                    const sorted = rows.slice().sort((a, b) => {
                        const aSpecial = isSpecialRow(a);
                        const bSpecial = isSpecialRow(b);
                        if (aSpecial && !bSpecial) return -1;
                        if (!aSpecial && bSpecial) return 1;
                        if (aSpecial && bSpecial) return 0;

                        const aTs = getRowTimestamp(a);
                        const bTs = getRowTimestamp(b);
                        if (aTs === null || bTs === null) return 0;
                        return theme.SORT.select === 'desc' ? bTs - aTs : aTs - bTs;
                    });
                    sorted.forEach(row => container.appendChild(row));
                    return;
                }

                container.classList.add('gh-freshness-flex-sort');

                const specialRows = [];
                const normalRows = [];
                rows.forEach(row => {
                    if (isSpecialRow(row)) {
                        specialRows.push(row);
                    } else {
                        normalRows.push(row);
                    }
                });

                specialRows.forEach(row => {
                    row.style.setProperty('order', '-1', 'important');
                });

                const rowsWithTs = normalRows
                    .map(row => ({ row, ts: getRowTimestamp(row) }))
                    .filter(item => item.ts !== null);

                const rowsWithoutTs = normalRows.filter(row => getRowTimestamp(row) === null);

                rowsWithTs.sort((a, b) => {
                    return theme.SORT.select === 'desc' ? b.ts - a.ts : a.ts - b.ts;
                });

                rowsWithTs.forEach((item, index) => {
                    item.row.style.setProperty('order', String(index), 'important');
                });

                rowsWithoutTs.forEach((row, index) => {
                    row.style.setProperty('order', String(rowsWithTs.length + index), 'important');
                });
            });
        } finally {
            isSorting = false;
        }
    }

    // ==================== 主高亮函数 ====================
    function GitHub_Freshness(theme) {
        if (!theme) theme = THEME;
        const matchUrl = isMatchedUrl();
        if (!matchUrl) return;

        if (matchUrl === 'matchSearchPage') {
            GitHub_FreshnessSearchPage(theme);
            return;
        }

        const timeElements = $(
            '[data-testid="file-list-row"] relative-time, ' +
            '.Box-row relative-time, ' +
            'tr relative-time, ' +
            'div[role="row"] relative-time, ' +
            '#repos-file-tree relative-time, ' +
            '[role="rowgroup"] relative-time'
        );

        if (timeElements.length > 0) {
            timeElements.each(function () {
                const $this = $(this);
                const datetime = $this.attr('datetime');
                if (!datetime) return;
                const trElement = $this.closest(
                    '[data-testid="file-list-row"], .Box-row, tr, div[role="row"], [role="rowgroup"] > div'
                );
                if (!trElement.length) return;
                const timeResult = handleTime(datetime, theme.TIME_BOUNDARY);
                let BGC_element = trElement.find(
                    '[data-testid="latest-commit-details"], td:last-child, div[role="gridcell"]:last-child'
                ).first();
                if (BGC_element.length === 0) BGC_element = $this.parent();
                const ICON_element = trElement.find('svg').first();
                setElementBGC(BGC_element, theme.BGC, timeResult);
                setElementDIR(ICON_element, theme.DIR, timeResult);
                setElementFONT($this.parent(), theme.FONT, timeResult);
            });
        }

        applyCSSSortOrder(theme);
    }

    // ==================== 修复 #3: URL 匹配增强 (支持尾部斜杠) ====================
    function isMatchedUrl() {
        const href = window.location.href;
        if (/^https:\/\/github\.com\/search\?.*$/.test(href)) return 'matchSearchPage';

        const reservedPaths = [
            'settings', 'notifications', 'marketplace', 'orgs', 'topics',
            'collections', 'features', 'pricing', 'login', 'join', 'new',
            'explore', 'sponsors', 'about', 'customer-stories', 'security',
            'enterprise', 'team', 'readme', 'nonprofit'
        ];
        const pathMatch = href.match(/^https:\/\/github\.com\/([^/]+)(?:\/([^/]+))?/);
        if (pathMatch) {
            const firstSegment = pathMatch[1];
            if (reservedPaths.includes(firstSegment)) return null;
            if (!pathMatch[2]) return null;
            if (['repositories', 'projects', 'packages', 'stars'].includes(pathMatch[2])) return null;
        }

        // 修复 #3: 添加 \/? 支持尾部斜杠
        if (/^https:\/\/github\.com\/[^/]+\/[^/]+\/?(?:\?.*)?$|^https:\/\/github\.com\/[^/]+\/[^/]+\/tree\/.+$/.test(href)) return 'matchRepoPage';
        return null;
    }

    // ==================== Date Formatting ====================
    function formatDate(dateStr) {
        if (!dateStr) return '';
        const date = new Date(dateStr);
        if (isNaN(date.getTime())) return dateStr;
        const Y = date.getFullYear();
        const M = String(date.getMonth() + 1).padStart(2, '0');
        const D = String(date.getDate()).padStart(2, '0');
        return `${Y}-${M}-${D}`;
    }

    // ==================== 修复 #5: 时间格式化去重 ====================
    function forceFormatRelativeTime() {
        if (!THEME.TIME_FORMAT.isEnabled) return;
        const elements = document.querySelectorAll('relative-time');
        if (elements.length === 0) return;
        elements.forEach((item) => {
            try {
                const dateStr = item.getAttribute('datetime');
                if (!dateStr) return;
                const date = new Date(dateStr);
                if (isNaN(date.getTime())) return;
                const Y = date.getFullYear();
                const M = String(date.getMonth() + 1).padStart(2, '0');
                const D = String(date.getDate()).padStart(2, '0');
                const hh = String(date.getHours()).padStart(2, '0');
                const mm = String(date.getMinutes()).padStart(2, '0');
                const str = `${Y}-${M}-${D} ${hh}:${mm}`;

                const parent = item.parentNode;
                if (!parent) return;
                const existing = parent.querySelector(':scope > .custom-time-format');
                if (existing) {
                    if (existing.textContent !== str) existing.textContent = str;
                    item.classList.add('custom-time-replaced');
                    return;
                }

                if (item.nextElementSibling && item.nextElementSibling.classList.contains('custom-time-format')) {
                    if (item.nextElementSibling.textContent !== str) item.nextElementSibling.textContent = str;
                    return;
                }

                item.classList.add('custom-time-replaced');
                const span = document.createElement('span');
                span.className = 'custom-time-format formatted-date-span';
                span.textContent = str;
                parent.insertBefore(span, item.nextSibling);
            } catch (e) {}
        });
    }

    // ==================== Debounce & Run ====================
    function debounce(func, wait) {
        let timeout;
        return function (...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }

    const runScript = debounce(() => {
        try {
            GitHub_Freshness();
            GitHub_FreshnessAwesome(THEME);
            forceFormatRelativeTime();
        } catch (e) { console.error('GitHub Freshness: Script execution error', e); }
    }, 350);

    // ==================== 修复 #4: MutationObserver 忽略排序引起的 DOM 变化 ====================
    let mutationObserver = null;
    function initMutationObserver() {
        if (mutationObserver) return;
        mutationObserver = new MutationObserver((mutations) => {
            // 修复 #4: 排序操作期间不触发
            if (isSorting) return;

            let shouldRun = false;
            for (const mutation of mutations) {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            if (node.matches && (node.matches('relative-time') || (node.querySelector && node.querySelector('relative-time, [data-testid="file-list-row"]')))) {
                                shouldRun = true; break;
                            }
                        }
                    }
                }
                if (shouldRun) break;
            }
            if (shouldRun) runScript();
        });
        mutationObserver.observe(document.body, { childList: true, subtree: true });
    }

    // ==================== Init ====================
    $(function() {
        console.log('GitHub Freshness v1.2.0 (All Fixes) Loaded');
        runScript();
        initMutationObserver();
    });

    document.addEventListener('pjax:end', () => { runScript(); initMutationObserver(); });
    window.addEventListener('popstate', () => setTimeout(runScript, 350));

    const originalPush = history.pushState;
    history.pushState = function (...args) { originalPush.apply(this, args); setTimeout(runScript, 350); };

    const originalReplace = history.replaceState;
    history.replaceState = function (...args) { originalReplace.apply(this, args); setTimeout(runScript, 350); };

    // ==================== 修复 #6: 定时更新 + 修复 #7: 检查页面匹配 ====================
    let lastTimeUpdate = 0;
    const TIME_UPDATE_INTERVAL = 2000;

    function scheduleTimeUpdate() {
        // 修复 #7: 非匹配页面不执行
        if (!isMatchedUrl()) return;

        const now = Date.now();
        if (now - lastTimeUpdate < TIME_UPDATE_INTERVAL) return;
        lastTimeUpdate = now;

        setTimeout(() => {
            try {
                forceFormatRelativeTime();
                if (THEME.SORT && THEME.SORT.isEnabled) applyCSSSortOrder(THEME);
            } catch (e) { /* ignore */ }
        }, 0);
    }

    setInterval(scheduleTimeUpdate, TIME_UPDATE_INTERVAL);

    // ==================== Menu Command ====================
    GM_registerMenuCommand('⚙️ Settings', createSettingsPanel);

    // ==================== Dark Mode Listener ====================
    const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    darkModeMediaQuery.addEventListener('change', (e) => {
        if (CURRENT_THEME === 'auto') {
            THEME_TYPE = e.matches ? 'dark' : 'light';
            THEME = config_JSON[THEME_TYPE] || JSON.parse(JSON.stringify(default_THEME));
            runScript();
        }
    });

})();