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