GitHub Style

通过颜色高亮判断仓库/文件更新活跃度 + 强制把 relative-time 显示为 YYYY-MM-DD HH:mm

От 12.03.2026. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         GitHub Style
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  通过颜色高亮判断仓库/文件更新活跃度 + 强制把 relative-time 显示为 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
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// ==/UserScript==

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

    const DateTime = luxon.DateTime;

    // Styles
    GM_addStyle(`@import url('https://cdn.jsdelivr.net/npm/@simonwep/[email protected]/dist/themes/classic.min.css');`);
    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: monospace;
          font-size: 14px !important;
          font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif !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; }
    `);

    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="" 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="">
          </main>
      </div>
      <p style="font-size: 0.9em; opacity: 0.8;">复选框切换需刷新页面生效。</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 })));
    let THEME = config_JSON[THEME_TYPE] || default_THEME;

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

    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';
                    val = $(`#${themeKey}-${type}-color-value .pcr-button`).css('--pcr-color');
                } else if (key === 'isEnabled') {
                    val = $(`#${themeKey}-enabled`).prop('checked');
                } else if (key === 'number' || key === 'select') {
                    val = $(`#${themeKey}-${key}`).val();
                }
                updatedTheme[themeKey][key] = val;
            }
        }
        return updatedTheme;
    }

    function handelData(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';
                    $(`#${themeKey}-${type}-color-value .pcr-button`).css('--pcr-color', val);
                } else if (key === 'isEnabled') {
                    $(`#${themeKey}-enabled`).prop('checked', val);
                } else if (key === 'number' || key === 'select') {
                    $(`#${themeKey}-${key}`).val(val);
                }
            }
        }
    }

    // UI
    function createSettingsPanel() {
        Swal.fire({
            title: `GitHub Freshness Settings`,
            html: PanelDom,
            focusConfirm: false,
            preConfirm: () => {
                const updated_THEME = getUpdatedThemeConfig();
                CURRENT_THEME = $('#CURRENT_THEME-select').val();
                AWESOME_TOKEN = $('#AWESOME_TOKEN').val();
                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] || default_THEME;
                    handelData(theme);
                });
            }
        });
    }

    function initSettings(theme) {
        if (!theme) theme = default_THEME;
        const setupPickr = (id, color) => initPickr({ el: id, default: color });
        setupPickr('#BGC-highlight-color-pickr', theme.BGC.highlightColor);
        setupPickr('#BGC-grey-color-pickr', theme.BGC.greyColor);
        setupPickr('#FONT-highlight-color-pickr', theme.FONT.highlightColor);
        setupPickr('#FONT-grey-color-pickr', theme.FONT.greyColor);
        setupPickr('#DIR-highlight-color-pickr', theme.DIR.highlightColor);
        setupPickr('#DIR-grey-color-pickr', theme.DIR.greyColor);
        $('#THEME-select').val(getThemeType());
        $('#CURRENT_THEME-select').val(CURRENT_THEME);
        $('#AWESOME_TOKEN').val(AWESOME_TOKEN);
        handelData(theme);
    }

    // DOM Helpers
    function setElementBGC(el, BGC, timeResult) {
        if (el.length && BGC.isEnabled) {
            el[0].style.setProperty('background-color', timeResult ? BGC.highlightColor : BGC.greyColor, 'important');
        }
    }

    function setElementDIR(el, DIR, timeResult) {
        if (el.length && DIR.isEnabled) {
            const color = timeResult ? DIR.highlightColor : DIR.greyColor;
            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);
        }
    }

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

    function handelTime(time, time_boundary, type = 'ISO8601') {
        const { number, select } = time_boundary;
        let days = 0;
        switch (select) {
            case 'day': days = number; break;
            case 'week': days = number * 7; break;
            case 'month': days = number * 30; break;
            case 'year': days = number * 365; break;
            default: days = 30;
        }
        const thresholdDate = new Date();
        thresholdDate.setDate(thresholdDate.getDate() - days);
        let inputDate;
        if (type === 'UTC') {
            try {
                const dt = DateTime.fromFormat(time, "yyyy年M月d日 'GMT'Z HH:mm", { zone: 'UTC' }).setZone('Asia/Shanghai');
                inputDate = dt.toJSDate();
            } catch(e) {
                console.error("Error parsing search result date:", e);
                inputDate = new Date();
            }
        } else {
            inputDate = new Date(time);
        }
        return inputDate >= thresholdDate;
    }

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

    function GitHub_FreshnessAwesome(theme) {
        const observer = new IntersectionObserver((entries) => {
            entries.forEach(el => {
                if (el.isIntersecting && el.target.getAttribute('request') !== 'true') {
                    const href = $(el.target).attr('href');
                    const apiHref = toAPIUrl(href);
                    if(!apiHref) return;
                    el.target.setAttribute('request', 'true');
                    $.ajax({
                        url: apiHref,
                        method: 'GET',
                        headers: AWESOME_TOKEN ? { 'Authorization': `token ${AWESOME_TOKEN}` } : {},
                        success: function (data) {
                            const timeResult = handelTime(data.updated_at, theme.TIME_BOUNDARY);
                            if (theme.AWESOME.isEnabled) {
                                $(el.target).after(
                                    `<span class="freshness-stars">★${data.stargazers_count}</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);
                        },
                        error: function (err) {
                            if (err.status === 403) console.warn("GitHub API Rate Limit Exceeded");
                        }
                    });
                }
            });
        }, { threshold: 0.5 });

        $('.Box-row a, .markdown-body a').each(function () {
            if (/^https:\/\/github\.com\/[^\/]+\/[^\/]+\/?$/.test($(this).attr('href'))) {
                observer.observe(this);
            }
        });
    }

    function GitHub_FreshnessSearchPage(theme) {
        const elements = $('relative-time[datetime]');
        if (elements.length === 0) return;
        elements.each(function () {
            const title = $(this).attr('title') || $(this).attr('datetime');
            if (title) {
                const timeResult = handelTime(title, theme.TIME_BOUNDARY, $(this).is('[datetime]') ? 'ISO8601' : 'UTC');
                const BGC_element = $(this).closest('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);
        }

        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 = [];
        timeElements.each(function () {
            const datetime = $(this).attr('datetime');
            if (!datetime) return;
            const timeResult = handelTime(datetime, theme.TIME_BOUNDARY);
            const trElement = $(this).closest('[data-testid="file-list-row"], .Box-row, tr, div[role="row"]');
            if (trElement.length) {
                trRows.push(trElement[0]);
                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 && trRows.length > 0) {
            const parentContainer = $(trRows[0]).parent();
            if (parentContainer.is('tbody, [role="grid"], [data-testid="file-list-row-container"]')) {
                trRows.sort((a, b) => {
                    const tA = new Date($(a).find('relative-time').attr('datetime'));
                    const tB = new Date($(b).find('relative-time').attr('datetime'));
                    return theme.SORT.select === 'asc' ? tA - tB : tB - tA;
                });
                parentContainer.append(trRows);
            }
        }
    }

    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 forceFormatRelativeTime() {
        if (!THEME.TIME_FORMAT.isEnabled) return;

        document.querySelectorAll('relative-time').forEach((item) => {
            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) {
                if (item.shadowRoot.textContent !== str) {
                    item.shadowRoot.textContent = str;
                }
            } else {
                if (item.textContent !== str) {
                    item.textContent = str;
                }
            }

            item.removeAttribute('prefix');
            item.setAttribute('format', 'manual');
        });
    }

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

    const runScript = debounce(() => {
        GitHub_Freshness();
        forceFormatRelativeTime();
    }, 350);

    // Init
    $(function() {
        console.log('GitHub Freshness + Absolute Time Loaded');
        runScript();
    });

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

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

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

    // 高频修正时间显示(应对 GitHub 动态重渲染)
    setInterval(forceFormatRelativeTime, 600);

    GM_registerMenuCommand('⚙️ Settings', createSettingsPanel);

    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
        if (CURRENT_THEME === 'auto') {
            THEME = config_JSON[e.matches ? 'dark' : 'light'] || default_THEME;
            runScript();
        }
    });
})();