Greasy Fork is available in English.
智能高亮 GitHub 活跃仓库:根据最后更新时间自动标记颜色(越新越绿),支持文件按时间排序、自定义主题、显示精确时间(YYYY-MM-DD HH:mm)
اعتبارا من
// ==UserScript== // @name GitHub Style // @namespace http://tampermonkey.net/ // @version 1.0.2 // @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: 2px solid rgba(255, 255, 255, 0.3); border-radius: 8px; background-color: rgba(0, 0, 0, 0.2) !important; color: #fff !important; padding: 8px 12px; font-size: 14px; cursor: pointer; outline: none; min-width: 120px; } .row-box select:hover { border-color: rgba(255, 255, 255, 0.5); background-color: rgba(0, 0, 0, 0.3) !important; } .row-box select:focus { border-color: rgba(255, 255, 255, 0.8); background-color: rgba(0, 0, 0, 0.4) !important; box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1); } /* 下拉选项样式 */ .row-box select option { background-color: #2d3748 !important; color: #fff !important; padding: 8px; } /* 针对 Firefox 的特殊处理 */ .row-box select:-moz-focusring { color: transparent; text-shadow: 0 0 0 #fff; } /* 针对 Webkit 浏览器的下拉箭头 */ .row-box select { -webkit-appearance: none; -moz-appearance: none; appearance: none; background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right 8px center; background-size: 16px; padding-right: 32px; } .row-box { display: flex; margin: 20px 25px; align-items: center; justify-content: space-between; } .row-box .swal2-input { height: 40px; background-color: rgba(0, 0, 0, 0.2) !important; color: #fff !important; border: 2px solid rgba(255, 255, 255, 0.3); border-radius: 8px; } .row-box .swal2-input:focus { border-color: rgba(255, 255, 255, 0.8); box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1); } .row-box label { margin-right: 10px; color: #fff; font-weight: 500; } .row-box main input { background: rgba(15, 172, 83, 1); width: 70px; border: unset; box-shadow: unset; text-align: right; margin: 0; } .row-box main { display: flex; align-items: center; gap: 10px; } .freshness-stars { padding: 8px; } .freshness-updated { margin-left: 5px; } .freshness-force-color { color: inherit !important; } .github-freshness-loading { opacity: 0.6; pointer-events: none; } /* 复选框样式优化 */ input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; accent-color: #0fac53; } /* 数字输入框样式 */ input[type="number"] { background-color: rgba(0, 0, 0, 0.2) !important; color: #fff !important; border: 2px solid rgba(255, 255, 255, 0.3) !important; border-radius: 8px; padding: 8px; text-align: center; } input[type="number"]:focus { border-color: rgba(255, 255, 255, 0.8) !important; outline: none; } /* 密码输入框样式 */ input[type="password"] { background-color: rgba(0, 0, 0, 0.2) !important; color: #fff !important; border: 2px solid rgba(255, 255, 255, 0.3) !important; border-radius: 8px; padding: 8px 12px; min-width: 200px; } input[type="password"]:focus { border-color: rgba(255, 255, 255, 0.8) !important; outline: none; box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1); } /* 按钮样式优化 */ #RESET_DEFAULTS { background-color: #e53e3e !important; color: white !important; border: none !important; padding: 10px 24px !important; border-radius: 8px !important; cursor: pointer; font-weight: 500; transition: all 0.2s; } #RESET_DEFAULTS:hover { background-color: #c53030 !important; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(229, 62, 62, 0.4); } /* SweetAlert2 按钮样式 */ .swal2-confirm { background-color: #0fac53 !important; border-radius: 8px !important; } .swal2-cancel { background-color: #718096 !important; border-radius: 8px !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="" 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" style="color: #fff; text-decoration: underline;">🔑 AWESOME token:</a></label> <input type="checkbox" id="AWESOME-enabled"> </div> <main> <input id="AWESOME_TOKEN" type="password" class="swal2-input" value="" placeholder="ghp_xxxxxxxxxxxx"> </main> </div> <div class="row-box" style="justify-content: center; margin-top: 30px;"> <button type="button" id="RESET_DEFAULTS" class="swal2-styled">🔄 恢复默认设置</button> </div> <p style="font-size: 0.85em; opacity: 0.9; text-align: center; margin-top: 15px; color: rgba(255,255,255,0.8);"> 💡 提示:复选框切换需刷新页面生效 </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; 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: `<div style="display: flex; align-items: center; justify-content: center; gap: 10px;"> <span>🎨</span> <span>GitHub Freshness Settings</span> </div>`, html: PanelDom, focusConfirm: false, allowOutsideClick: false, background: '#31b96c', confirmButtonColor: '#0fac53', cancelButtonColor: '#718096', preConfirm: () => { const updated_THEME = getUpdatedThemeConfig(); CURRENT_THEME = $('#CURRENT_THEME-select').val(); AWESOME_TOKEN = $('#AWESOME_TOKEN').val().trim(); 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: '#31b96c', icon: 'success', iconColor: '#fff', title: '<span style="color: #fff;">✅ 保存成功!</span>', showConfirmButton: false, timer: 1000, toast: true }); }, heightAuto: false, showCancelButton: true, confirmButtonText: '💾 保存', cancelButtonText: '❌ 取消', didOpen: () => { initSettings(THEME); $('#THEME-select').on('change', function () { let selectedTheme = $(this).val(); let theme = config_JSON[selectedTheme] || JSON.parse(JSON.stringify(default_THEME)); handleData(theme); initPickr({ el: '#BGC-highlight-color-pickr', default: theme.BGC.highlightColor }); initPickr({ el: '#BGC-grey-color-pickr', default: theme.BGC.greyColor }); initPickr({ el: '#FONT-highlight-color-pickr', default: theme.FONT.highlightColor }); initPickr({ el: '#FONT-grey-color-pickr', default: theme.FONT.greyColor }); initPickr({ el: '#DIR-highlight-color-pickr', default: theme.DIR.highlightColor }); initPickr({ el: '#DIR-grey-color-pickr', default: theme.DIR.greyColor }); }); $('#RESET_DEFAULTS').on('click', () => { Swal.fire({ title: '🔄 确认恢复默认?', text: '当前主题的设置将被重置为默认值', icon: 'warning', iconColor: '#f6e05e', background: '#31b96c', color: '#fff', showCancelButton: true, confirmButtonColor: '#e53e3e', cancelButtonColor: '#718096', confirmButtonText: '⚠️ 确认重置', cancelButtonText: '✋ 取消' }).then((result) => { if (result.isConfirmed) { resetToDefaults(); Swal.fire({ position: 'top-center', background: '#31b96c', icon: 'success', iconColor: '#fff', title: '<span style="color: #fff;">✅ 已重置为默认值</span>', showConfirmButton: false, timer: 800, toast: true }); } }); }); } }); } 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; } } 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; } 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; } 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(); 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'); const timeStr = datetime || title; if (!timeStr) return; const isISO8601 = !!datetime; const timeResult = handleTime(timeStr, theme.TIME_BOUNDARY, isISO8601 ? 'ISO8601' : 'UTC'); const BGC_element = $this.closest('div[data-testid*="results-list"], div[data-testid*="results-card"], li, article, .Box-row'); setElementBGC(BGC_element, theme.BGC, timeResult); setElementFONT($this, theme.FONT, timeResult); }); } function 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; }); 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}`; } 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); } }); } 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); let mutationObserver = null; function initMutationObserver() { if (mutationObserver) return; mutationObserver = new MutationObserver((mutations) => { let shouldRun = false; for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches && (node.matches('relative-time') || node.querySelector && node.querySelector('relative-time, [data-testid="file-list-row"]'))) { shouldRun = true; break; } } } } } if (shouldRun) { runScript(); } }); mutationObserver.observe(document.body, { childList: true, subtree: true }); } $(function() { console.log('GitHub Freshness v2.0.1 (UI Fixed) 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); }; let timeUpdateScheduled = false; function scheduleTimeUpdate() { if (timeUpdateScheduled) return; timeUpdateScheduled = true; requestAnimationFrame(() => { forceFormatRelativeTime(); timeUpdateScheduled = false; }); } 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); })();