GitHub Style

智能高亮 GitHub 活跃仓库:根据最后更新时间自动标记颜色(越新越绿),支持文件按时间排序、自定义主题、显示精确时间(YYYY-MM-DD HH:mm)

À partir de 2026-03-13. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         GitHub Style
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  智能高亮 GitHub 活跃仓库:根据最后更新时间自动标记颜色(越新越绿),支持文件按时间排序、自定义主题、显示精确时间(YYYY-MM-DD HH:mm)
// @author       XiMenChiXue
// @license      MIT
// @icon         https://raw.githubusercontent.com/rational-stars/picgo/refs/heads/main/avatar.jpg
// @match        https://github.com/*/*
// @match        https://github.com/search?*
// @match        https://github.com/*/*/tree/*
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @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
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_getResourceText
// ==/UserScript==

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

    const DateTime = luxon.DateTime;

    // 注入 Pickr CSS
    const pickrCss = GM_getResourceText('PICKR_CSS');
    if (pickrCss) GM_addStyle(pickrCss);

    // 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 {
          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: unset;
          border-radius: .15em;
      }
      .row-box {
          display: flex;
          margin: 25px;
          align-items: center;
          justify-content: space-between;
      }
      .row-box .swal2-input {
          height: 40px;
      }
      .row-box label {
          margin-right: 10px;
      }
      .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;
      }
      .freshness-stars { padding: 8px; }
      .freshness-updated { margin-left: 5px; }
      .freshness-force-color { color: inherit !important; }
      .github-freshness-loading {
          opacity: 0.6;
          pointer-events: none;
      }
    `);

    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">文件排序:</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">AWESOME token: </a></label>
              <input type="checkbox" id="AWESOME-enabled">
          </div>
          <main>
              <input id="AWESOME_TOKEN" type="password" class="swal2-input" value="" placeholder="ghp_xxxxxxxxxxxx">
          </main>
      </div>
      <div class="row-box" style="justify-content: center;">
          <button type="button" id="RESET_DEFAULTS" class="swal2-styled" style="background-color: #dc3545;">恢复默认设置</button>
      </div>
      <p style="font-size: 0.9em; opacity: 0.8; text-align: center; margin-top: 10px;">复选框切换需刷新页面生效。</p>
    `;

    // Configuration
    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 },
    };

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

    // API 请求缓存和队列
    const apiCache = new Map();
    const apiQueue = [];
    let apiProcessing = false;
    const API_DELAY = 100; // ms between requests

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

    // Helper 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 pickr = Pickr.create({ ...configPickr, ...el_default });
        pickr.on('save', (color, instance) => {
            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 colorVal = $(`#${themeKey}-${type}-color-value .pcr-button`).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);
                    }
                } else if (key === 'isEnabled') {
                    $(`#${themeKey}-enabled`).prop('checked', val);
                } else if (key === 'number' || key === 'select') {
                    $(`#${themeKey}-${key}`).val(val);
                }
            }
        }
    }

    function resetToDefaults() {
        const themeType = $('#THEME-select').val();
        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 });
    }

    // UI
    function createSettingsPanel() {
        Swal.fire({
            title: `GitHub Freshness Settings`,
            html: PanelDom,
            focusConfirm: false,
            allowOutsideClick: false,
            preConfirm: () => {
                const updated_THEME = getUpdatedThemeConfig();
                CURRENT_THEME = $('#CURRENT_THEME-select').val();
                AWESOME_TOKEN = $('#AWESOME_TOKEN').val().trim();

                // 验证 Token 格式
                if (updated_THEME.AWESOME.isEnabled && AWESOME_TOKEN && !AWESOME_TOKEN.match(/^ghp_[a-zA-Z0-9]{36}$/)) {
                    Swal.showValidationMessage('Token 格式不正确,应为 ghp_ 开头的 40 位字符串');
                    return false;
                }

                GM_setValue('config_JSON', JSON.stringify({
                    ...config_JSON,
                    [$('#THEME-select').val()]: updated_THEME,
                }));
                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: '#4ab96f',
                    icon: 'success',
                    title: 'Saved!',
                    showConfirmButton: false,
                    timer: 800
                });
            },
            heightAuto: false,
            showCancelButton: true,
            confirmButtonText: 'Save',
            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',
                        showCancelButton: true,
                        confirmButtonColor: '#3085d6',
                        cancelButtonColor: '#d33',
                        confirmButtonText: '确认',
                        cancelButtonText: '取消'
                    }).then((result) => {
                        if (result.isConfirmed) {
                            resetToDefaults();
                            Swal.fire({
                                position: 'top-center',
                                icon: 'success',
                                title: '已重置',
                                showConfirmButton: false,
                                timer: 500
                            });
                        }
                    });
                });
            }
        });
    }

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

    // DOM Helpers
    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) {
                console.warn('GitHub Freshness: Failed to set background color', 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) {
                console.warn('GitHub Freshness: Failed to set icon color', 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) {
                console.warn('GitHub Freshness: Failed to set font color', e);
            }
        }
    }

    // 改进的时间处理函数 - 支持多种格式
    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;
                }
            }

            // 如果都失败了,尝试原生 Date 解析
            if (!inputDate) {
                const nativeDate = new Date(time);
                if (!isNaN(nativeDate.getTime())) {
                    inputDate = nativeDate;
                }
            }
        } else {
            inputDate = new Date(time);
        }

        if (!inputDate || isNaN(inputDate.getTime())) {
            console.warn('GitHub Freshness: Unable to parse date:', time);
            return false;
        }

        return inputDate >= thresholdDate;
    }

    // Core Logic
    function toAPIUrl(href) {
        if (!href) return null;
        const match = href.match(/^https:\/\/github\.com\/([^\/]+)\/([^\/]+)/);
        return match ? `https://api.github.com/repos/${match[1]}/${match[2]}` : null;
    }

    // API 请求队列处理
    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) {
                    const remaining = response.headers.get('X-RateLimit-Remaining');
                    const resetTime = response.headers.get('X-RateLimit-Reset');
                    if (remaining === '0' && resetTime) {
                        const waitMs = (parseInt(resetTime) * 1000) - Date.now();
                        console.warn(`GitHub API 限流,将在 ${Math.ceil(waitMs/1000)} 秒后重置`);
                    } else {
                        console.warn('GitHub API 限流或 Token 无效');
                    }
                    callback(null);
                } else if (response.ok) {
                    const data = await response.json();
                    // 缓存结果 5 分钟
                    apiCache.set(apiHref, data);
                    setTimeout(() => apiCache.delete(apiHref), 5 * 60 * 1000);
                    callback(data);
                } else {
                    console.warn('GitHub API 请求失败:', response.status);
                    callback(null);
                }
            } catch (error) {
                console.error('GitHub API 请求异常:', error);
                callback(null);
            }

            // 延迟下一个请求
            if (apiQueue.length > 0) {
                await new Promise(resolve => setTimeout(resolve, API_DELAY));
            }
        }

        apiProcessing = false;
    }

    function GitHub_FreshnessAwesome(theme) {
        const observers = [];

        const observer = 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)) {
                observer.observe(this);
            }
        });

        observers.push(observer);
        return observers;
    }

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

            // 优先使用 datetime,如果没有则使用 title
            const timeStr = datetime || title;
            if (!timeStr) return;

            // 判断是否为 ISO8601 格式
            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 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');
        if (timeElements.length === 0) return;

        let trRows = [];
        let validRows = 0;

        timeElements.each(function () {
            const $this = $(this);
            const datetime = $this.attr('datetime');
            if (!datetime) return;

            const timeResult = handleTime(datetime, theme.TIME_BOUNDARY);
            const trElement = $this.closest('[data-testid="file-list-row"], .Box-row, tr, div[role="row"]');

            if (trElement.length) {
                trRows.push({
                    element: trElement[0],
                    datetime: datetime,
                    timeResult: timeResult,
                    $el: trElement
                });
                validRows++;

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

        // 排序功能 - 仅在启用且有有效行时执行
        if (theme.SORT.isEnabled && validRows > 1) {
            const parentContainer = $(trRows[0].element).parent();
            if (parentContainer.is('tbody, [role="grid"], [data-testid="file-list-row-container"]')) {
                trRows.sort((a, b) => {
                    const tA = new Date(a.datetime).getTime();
                    const tB = new Date(b.datetime).getTime();
                    // 检查无效日期
                    if (isNaN(tA) || isNaN(tB)) return 0;
                    return theme.SORT.select === 'asc' ? tA - tB : tB - tA;
                });

                // 使用 DocumentFragment 优化 DOM 操作
                const fragment = document.createDocumentFragment();
                trRows.forEach(row => fragment.appendChild(row.element));
                parentContainer.append(fragment);
            }
        }
    }

    function isMatchedUrl() {
        const href = window.location.href;
        if (/^https:\/\/github\.com\/search\?.*$/.test(href)) return 'matchSearchPage';
        if (/^https:\/\/github\.com\/[^/]+\/[^/]+(?:\?.*)?$|^https:\/\/github\.com\/[^/]+\/[^/]+\/tree\/.+$/.test(href)) return 'matchRepoPage';
        return null;
    }

    // 时间格式化
    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}`;
    }

    // 强制格式化 relative-time
    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}`;

                if (item.shadowRoot) {
                    const slot = item.shadowRoot.querySelector('slot');
                    if (slot) {
                        slot.textContent = str;
                    } else if (item.shadowRoot.textContent !== str) {
                        item.shadowRoot.textContent = str;
                    }
                } else if (item.textContent !== str) {
                    item.textContent = str;
                }

                item.removeAttribute('prefix');
                item.setAttribute('format', 'manual');
                item.setAttribute('datetime', dateStr); // 确保保留原属性
            } catch (e) {
                console.warn('GitHub Freshness: Error formatting time', 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();
            forceFormatRelativeTime();
        } catch (e) {
            console.error('GitHub Freshness: Script execution error', e);
        }
    }, 350);

    // MutationObserver 用于监听 DOM 变化
    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) {
                    // 检查是否添加了新的 relative-time 或文件列表
                    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) {
                runScript();
            }
        });

        mutationObserver.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // Init
    $(function() {
        console.log('GitHub Freshness v2.0 (Optimized) Loaded');
        runScript();
        initMutationObserver();
    });

    // 清理函数
    function cleanup() {
        if (mutationObserver) {
            mutationObserver.disconnect();
            mutationObserver = null;
        }
    }

    // 事件监听
    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);
    };

    // 使用 requestAnimationFrame 优化高频时间更新
    let timeUpdateScheduled = false;
    function scheduleTimeUpdate() {
        if (timeUpdateScheduled) return;
        timeUpdateScheduled = true;
        requestAnimationFrame(() => {
            forceFormatRelativeTime();
            timeUpdateScheduled = false;
        });
    }

    // 每 2 秒检查一次时间显示(降低频率,使用 RAF 优化)
    setInterval(scheduleTimeUpdate, 2000);

    GM_registerMenuCommand('⚙️ Settings', createSettingsPanel);

    // 主题切换监听
    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();
        }
    });

    // 页面卸载时清理
    window.addEventListener('beforeunload', cleanup);
})();