GitHub Style

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

2026/03/12のページです。最新版はこちら

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();
        }
    });
})();