Greasy Fork is available in English.

GitHub Style

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

Versão de: 20/07/2026. Veja: a última versão.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name           GitHub Style
// @namespace      http://tampermonkey.net/
// @version        1.1.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*
// @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) ====================
    const API_CACHE_MAX = 100;
    const apiCache = 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);
        }
        apiCache.set(key, value);
        setTimeout(() => apiCache.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;
    }

    function initPickr(el_default) {
        const key = el_default.el;
        // 修复 #2: 销毁旧实例
        destroyPickr(key);

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

        pickr.on('save', (color, instance) => {
            const hex = color.toHEXA().toString();
            instance.options.el.setAttribute('data-pickr-color', hex);
            pickr.hide();
        });
        setTimeout(() => {
            const el = document.querySelector(el_default.el + ' .pcr-button');
            if (el && el_default.default) {
                el.setAttribute('data-pickr-color', el_default.default);
            }
        }, 300);
        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;
    }

    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 $btn = $(`#${themeKey}-${type}-color-value .pcr-button`);
                    if ($btn.length && val) {
                        $btn.css('--pcr-color', val);
                        $btn.attr('data-pickr-color', val);
                    }
                } 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);
                THEME = updated_THEME;
                GitHub_Freshness(updated_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
                            });
                        }
                    });
                });

                // 导出配置
                $('#EXPORT_CONFIG').on('click', () => {
                    const exportData = {
                        config_JSON: config_JSON,
                        CURRENT_THEME: CURRENT_THEME,
                        AWESOME_TOKEN: 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;
                        }
                    });
                });

                // 导入配置
                $('#IMPORT_CONFIG').on('click', () => {
                    Swal.fire({
                        title: '📥 导入配置',
                        html: `
                            <p style="color: #fff; font-size: 14px; margin-bottom: 10px;">请粘贴之前导出的配置码 (Base64):</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);
                            }
                            if (importData.AWESOME_TOKEN !== undefined) {
                                AWESOME_TOKEN = importData.AWESOME_TOKEN;
                                GM_setValue('AWESOME_TOKEN', AWESOME_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();
                            });
                        }
                    });
                });
            },
            // 修复 #2: 面板关闭时销毁 Pickr 实例
            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 ====================
    function toAPIUrl(href) {
        if (!href) return null;
        const match = href.match(/^https:\/\/github\.com\/([^\\/]+)\/([^\\/]+)/);
        if (!match) return null;
        // 修复 #7: 排除非仓库路径
        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]}`;
    }

    async function processApiQueue() {
        if (apiProcessing || apiQueue.length === 0) return;
        apiProcessing = true;
        while (apiQueue.length > 0) {
            const { apiHref, callback } = apiQueue.shift();
            if (apiCache.has(apiHref)) { callback(apiCache.get(apiHref)); continue; }
            try {
                const response = await fetch(apiHref, { headers: AWESOME_TOKEN ? { 'Authorization': `token ${AWESOME_TOKEN}` } : {} });
                if (response.status === 403) { callback(null); }
                else if (response.ok) {
                    const data = await response.json();
                    apiCacheSet(apiHref, data);
                    callback(data);
                } else { callback(null); }
            } catch (error) { callback(null); }
            if (apiQueue.length > 0) await new Promise(resolve => setTimeout(resolve, API_DELAY));
        }
        apiProcessing = false;
    }

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

    function GitHub_FreshnessAwesome(theme) {
        // 断开旧 observer
        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' });

        $('.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);
        });
    }

    // ==================== 排序辅助函数 ====================
    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;
        if (/^Name\s/.test(text) && text.length < 50) 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: 核心排序函数 (归一化 order) ====================
    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;

        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;

            // Table 布局:DOM 重排
            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;
            }

            // Div 布局:CSS flex order(归一化索引,避免溢出)
            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');
            });

            // 普通行按时间排序后分配归一化 order
            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');
            });
        });
    }

    // ==================== 主高亮函数 ====================
    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);
    }

    // ==================== 修复 #7: 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;
            // 需要至少两段路径才是仓库页 (user/repo)
            if (!pathMatch[2]) return null;
            // 第二段也不能是保留词(如 user/repositories 这种)
            if (pathMatch[2] === 'repositories' || pathMatch[2] === 'projects' || pathMatch[2] === 'packages' || pathMatch[2] === 'stars') return null;
        }

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

                // 修复 #5: 在父容器中查找已有的格式化 span,防止重复
                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;
                }

                // 也检查 nextElementSibling(兼容旧逻辑)
                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);

    // ==================== Mutation Observer ====================
    let mutationObserver = null;
    function initMutationObserver() {
        if (mutationObserver) return;
        mutationObserver = new MutationObserver((mutations) => {
            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.1.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: 定时更新 (修复 rAF 竞态) ====================
    let lastTimeUpdate = 0;
    const TIME_UPDATE_INTERVAL = 2000;

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

        // 使用 setTimeout 替代 rAF,避免 tab 不可见时卡死
        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();
        }
    });

})();