智能高亮 GitHub 活跃仓库:集成 CSS 视觉排序、配置导入/导出,彻底解决 React 冲突与时间闪烁。
От
// ==UserScript== // @name GitHub Style // @namespace http://tampermonkey.net/ // @version 1.2.0 // @description 智能高亮 GitHub 活跃仓库:集成 CSS 视觉排序、配置导入/导出,彻底解决 React 冲突与时间闪烁。 // @author XiMenChuiXue // @license MIT // @icon https://raw.githubusercontent.com/rational-stars/picgo/refs/heads/main/avatar.jpg // @match https://github.com/* // @exclude https://github.com/settings/* // @exclude https://github.com/notifications/* // @exclude https://github.com/marketplace/* // @exclude https://github.com/orgs/* // @exclude https://github.com/topics/* // @exclude https://github.com/collections/* // @exclude https://github.com/features/* // @exclude https://github.com/pricing* // @exclude https://github.com/login* // @exclude https://github.com/join* // @noframes // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_getResourceText // @run-at document-start // @require https://code.jquery.com/jquery-3.6.0.min.js#sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4= // @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 // ==/UserScript== /* global luxon, Pickr, Swal, $ */ (function () { 'use strict'; const DateTime = luxon.DateTime; // ==================== 修复 #12: GM_getResourceText 容错 ==================== try { const pickrCss = GM_getResourceText('PICKR_CSS'); if (pickrCss) GM_addStyle(pickrCss); } catch (e) { console.warn('GitHub Freshness: Failed to load Pickr CSS resource', e); } // ==================== 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, .custom-time-format { 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; -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 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; } .row-box select:-moz-focusring { color: transparent; text-shadow: 0 0 0 #fff; } .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; } .github-freshness-loading { opacity: 0.6; pointer-events: none; } input[type="checkbox"] { width: 18px; height: 18px; cursor: pointer; accent-color: #0fac53; } input[type="number"], 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; text-align: center; } input[type="password"] { min-width: 200px; text-align: left; padding: 8px 12px; } input[type="number"]:focus, 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); } .swal2-confirm { background-color: #0fac53 !important; border-radius: 8px !important; } .swal2-cancel { background-color: #718096 !important; border-radius: 8px !important; } relative-time.custom-time-replaced { display: none !important; } .gh-freshness-flex-sort { display: flex !important; flex-direction: column !important; } .gh-freshness-flex-sort > [role="row"], .gh-freshness-flex-sort > [data-testid="file-list-row"], .gh-freshness-flex-sort > .Box-row { width: 100% !important; box-sizing: border-box !important; margin-left: 0 !important; margin-right: 0 !important; flex-shrink: 0 !important; } `); // ==================== Panel DOM ==================== 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">🔀 文件排序 (CSS视觉排序):</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_xxx 或 github_pat_xxx"> </main> </div> <div class="row-box" style="justify-content: center; gap: 15px; margin-top: 25px; flex-wrap: wrap;"> <button type="button" id="EXPORT_CONFIG" class="swal2-styled" style="background-color: #3182ce !important; border-radius: 8px !important; margin: 5px; padding: 10px 20px !important;">📤 导出配置</button> <button type="button" id="IMPORT_CONFIG" class="swal2-styled" style="background-color: #805ad5 !important; border-radius: 8px !important; margin: 5px; padding: 10px 20px !important;">📥 导入配置</button> </div> <div class="row-box" style="justify-content: center; margin-top: 15px;"> <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);"> 💡 提示:排序采用 CSS Flex 黑科技,不破坏 React 架构。导入/导出不包含 Token,请单独配置。 </p> `; // ==================== Default Config ==================== 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 }, }; // ==================== 修复 #3: 配置解析容错 ==================== function safeParseConfig() { try { const raw = GM_getValue('config_JSON', null); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== 'object') return null; if (!parsed.light || !parsed.dark) return null; for (const themeKey of ['light', 'dark']) { const t = parsed[themeKey]; if (!t.BGC || !t.TIME_BOUNDARY || !t.FONT || !t.DIR || !t.SORT || !t.AWESOME || !t.TIME_FORMAT) { return null; } } return parsed; } catch (e) { console.warn('GitHub Freshness: Config parse failed, using defaults', e); return null; } } let CURRENT_THEME = GM_getValue('CURRENT_THEME', 'light'); let AWESOME_TOKEN = GM_getValue('AWESOME_TOKEN', ''); let THEME_TYPE = getThemeType(); let config_JSON = safeParseConfig() || { light: JSON.parse(JSON.stringify(default_THEME)), dark: JSON.parse(JSON.stringify(default_THEME)) }; let THEME = config_JSON[THEME_TYPE] || JSON.parse(JSON.stringify(default_THEME)); // ==================== 修复 #9: API 缓存加上限 (LRU) + 修复 #5: 定时器去重 ==================== const API_CACHE_MAX = 100; const apiCache = new Map(); const cacheTimers = new Map(); const apiQueue = []; let apiProcessing = false; const API_DELAY = 100; function apiCacheSet(key, value) { if (apiCache.size >= API_CACHE_MAX) { const firstKey = apiCache.keys().next().value; apiCache.delete(firstKey); if (cacheTimers.has(firstKey)) { clearTimeout(cacheTimers.get(firstKey)); cacheTimers.delete(firstKey); } } // 修复 #5: 清除同一 key 的旧定时器 if (cacheTimers.has(key)) { clearTimeout(cacheTimers.get(key)); } apiCache.set(key, value); cacheTimers.set(key, setTimeout(() => { apiCache.delete(key); cacheTimers.delete(key); }, 5 * 60 * 1000)); } const configPickr = { theme: 'classic', components: { preview: true, opacity: true, hue: true, interaction: { rgba: true, input: true, clear: true, save: true }, }, }; // ==================== 修复 #2: Pickr 实例管理 ==================== const pickrInstances = {}; function destroyPickr(key) { if (pickrInstances[key]) { try { pickrInstances[key].destroyAndRemove(); } catch (e) { /* ignore */ } delete pickrInstances[key]; } } function destroyAllPickrs() { Object.keys(pickrInstances).forEach(destroyPickr); } // ==================== Utility Functions ==================== function getThemeType() { let themeType = CURRENT_THEME; if (CURRENT_THEME === 'auto') { themeType = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } return themeType; } // ==================== 修复 #10: 使用 Pickr init 事件替代硬编码延时 ==================== function initPickr(el_default) { const key = el_default.el; destroyPickr(key); const pickr = Pickr.create({ ...configPickr, ...el_default }); pickrInstances[key] = pickr; pickr.on('init', (instance) => { const btn = instance.getRoot().button; if (btn && el_default.default) { btn.setAttribute('data-pickr-color', el_default.default); } }); pickr.on('save', (color, instance) => { const hex = color.toHEXA().toString(); instance.getRoot().button.setAttribute('data-pickr-color', hex); 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 $btn = $(`#${themeKey}-${type}-color-value .pcr-button`); let colorVal = $btn.attr('data-pickr-color'); if (!colorVal) colorVal = $btn.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; } // ==================== 修复 #6: handleData 同步更新 Pickr 实例颜色 ==================== 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 pickrKey = `#${themeKey}-${type}-color-pickr`; const $btn = $(`#${themeKey}-${type}-color-value .pcr-button`); if ($btn.length && val) { $btn.css('--pcr-color', val); $btn.attr('data-pickr-color', val); } // 修复 #6: 同步 Pickr 实例内部颜色 if (pickrInstances[pickrKey] && val) { try { pickrInstances[pickrKey].setColor(val, true); } catch (e) { /* ignore */ } } } else if (key === 'isEnabled') { $(`#${themeKey}-enabled`).prop('checked', val); } else if (key === 'number' || key === 'select') { $(`#${themeKey}-${key}`).val(val); } } } } function resetToDefaults() { 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 }); } // ==================== Settings Panel ==================== 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(); const tokenRegex = /^(ghp_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9_]+)$/; if (updated_THEME.AWESOME.isEnabled && AWESOME_TOKEN && !AWESOME_TOKEN.match(tokenRegex)) { Swal.showValidationMessage('❌ Token 格式不正确 (支持 ghp_ 或 github_pat_)'); return false; } config_JSON[$('#THEME-select').val()] = updated_THEME; GM_setValue('config_JSON', JSON.stringify(config_JSON)); GM_setValue('CURRENT_THEME', CURRENT_THEME); GM_setValue('AWESOME_TOKEN', AWESOME_TOKEN); // ==================== 修复 #1: 保存后使用正确的当前主题 ==================== THEME_TYPE = getThemeType(); THEME = config_JSON[THEME_TYPE] || JSON.parse(JSON.stringify(default_THEME)); GitHub_Freshness(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 }); } }); }); // ==================== 导出配置 (排除 Token) ==================== $('#EXPORT_CONFIG').on('click', () => { const exportData = { config_JSON: config_JSON, CURRENT_THEME: CURRENT_THEME // 注意:不包含 AWESOME_TOKEN }; const b64 = btoa(unescape(encodeURIComponent(JSON.stringify(exportData)))); Swal.fire({ title: '📤 导出配置', html: ` <p style="color: #fff; font-size: 14px; margin-bottom: 10px;">✅ 已排除 Token 等敏感信息,可安全分享。</p> <textarea id="export-text" style="width: 100%; height: 150px; background: rgba(0,0,0,0.3); color: #fff; border: 1px solid rgba(255,255,255,0.3); border-radius: 8px; padding: 10px; font-family: monospace; resize: none;">${b64}</textarea> `, background: '#31b96c', color: '#fff', showCancelButton: true, confirmButtonText: '📋 复制并关闭', cancelButtonText: '❌ 关闭', didOpen: () => { const textarea = document.getElementById('export-text'); if (textarea) textarea.select(); }, preConfirm: () => { const textarea = document.getElementById('export-text'); if (textarea) { textarea.select(); if (navigator.clipboard) { navigator.clipboard.writeText(textarea.value).catch(() => document.execCommand('copy')); } else { document.execCommand('copy'); } } return true; } }); }); // ==================== 导入配置 (排除 Token) ==================== $('#IMPORT_CONFIG').on('click', () => { Swal.fire({ title: '📥 导入配置', html: ` <p style="color: #fff; font-size: 14px; margin-bottom: 10px;">请粘贴之前导出的配置码 (Base64),Token 不会被覆盖:</p> <textarea id="import-text" style="width: 100%; height: 150px; background: rgba(0,0,0,0.3); color: #fff; border: 1px solid rgba(255,255,255,0.3); border-radius: 8px; padding: 10px; font-family: monospace; resize: none;" placeholder="粘贴配置码..."></textarea> `, background: '#31b96c', color: '#fff', showCancelButton: true, confirmButtonText: '✅ 确认导入', cancelButtonText: '❌ 取消', preConfirm: () => { const val = document.getElementById('import-text').value.trim(); if (!val) { Swal.showValidationMessage('❌ 配置码不能为空'); return false; } try { const jsonStr = decodeURIComponent(escape(atob(val))); const importData = JSON.parse(jsonStr); if (!importData.config_JSON) throw new Error('Invalid format'); return importData; } catch (e) { Swal.showValidationMessage('❌ 配置码解析失败,请检查格式'); return false; } } }).then((result) => { if (result.isConfirmed && result.value) { const importData = result.value; config_JSON = importData.config_JSON; GM_setValue('config_JSON', JSON.stringify(config_JSON)); if (importData.CURRENT_THEME) { CURRENT_THEME = importData.CURRENT_THEME; GM_setValue('CURRENT_THEME', CURRENT_THEME); } // 注意:不导入 AWESOME_TOKEN,保留用户本地 Token THEME_TYPE = getThemeType(); THEME = config_JSON[THEME_TYPE] || JSON.parse(JSON.stringify(default_THEME)); Swal.fire({ position: 'top-center', background: '#31b96c', icon: 'success', iconColor: '#fff', title: '<span style="color: #fff;">✅ 导入成功!正在刷新...</span>', showConfirmButton: false, timer: 1000, toast: true }).then(() => { Swal.close(); createSettingsPanel(); runScript(); }); } }); }); }, didClose: () => { destroyAllPickrs(); } }); } 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); } // ==================== Element Styling ==================== 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) {} } } 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) {} } } 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) {} } } // ==================== Time Handling ==================== 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())) return false; return inputDate >= thresholdDate; } // ==================== API ==================== // ==================== 修复 #8: 正则去除多余反斜杠 ==================== function toAPIUrl(href) { if (!href) return null; const match = href.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)/); if (!match) return null; const reserved = ['settings', 'notifications', 'marketplace', 'orgs', 'topics', 'collections', 'features', 'pricing', 'login', 'join', 'search', 'new', 'explore']; if (reserved.includes(match[1])) return null; return `https://api.github.com/repos/${match[1]}/${match[2]}`; } // ==================== 修复 #2: API 队列 callback 容错 + finally 解锁 ==================== async function processApiQueue() { if (apiProcessing || apiQueue.length === 0) return; apiProcessing = true; try { while (apiQueue.length > 0) { const { apiHref, callback } = apiQueue.shift(); if (apiCache.has(apiHref)) { try { callback(apiCache.get(apiHref)); } catch (e) { /* ignore */ } continue; } try { const response = await fetch(apiHref, { headers: AWESOME_TOKEN ? { 'Authorization': `token ${AWESOME_TOKEN}` } : {} }); if (response.status === 403) { try { callback(null); } catch (e) { /* ignore */ } } else if (response.ok) { const data = await response.json(); apiCacheSet(apiHref, data); try { callback(data); } catch (e) { /* ignore */ } } else { try { callback(null); } catch (e) { /* ignore */ } } } catch (error) { try { callback(null); } catch (e) { /* ignore */ } } if (apiQueue.length > 0) await new Promise(resolve => setTimeout(resolve, API_DELAY)); } } finally { apiProcessing = false; } } // ==================== 修复 #1: IntersectionObserver 单例化 ==================== let awesomeObserver = null; function GitHub_FreshnessAwesome(theme) { if (awesomeObserver) { awesomeObserver.disconnect(); awesomeObserver = null; } awesomeObserver = 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' }); // 修复 #8: 正则使用 [^/]+ 替代 [^\\/]+ $('.Box-row a, .markdown-body a').each(function () { const href = $(this).attr('href'); if (href && /^https:\/\/github\.com\/[^/]+\/[^/]+\/?$/.test(href)) { const apiHref = toAPIUrl(href); if (apiHref) { awesomeObserver.observe(this); } } }); } // ==================== Search Page ==================== 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); }); } // ==================== 排序辅助函数 ==================== // ==================== 修复 #11: 增强特殊行检测 ==================== function isSpecialRow(row) { if (!row) return false; if (row.querySelector('[data-testid="latest-commit-details"]')) return true; if (row.querySelector('[data-testid="latest-commit"]')) return true; if (row.querySelector('.react-directory-commit-row')) return true; if (row.getAttribute && row.getAttribute('role') === 'columnheader') return true; if (row.querySelector('[role="columnheader"]')) return true; if (!row.querySelector('relative-time[datetime]')) return true; const text = (row.textContent || '').trim(); if (/^Latest commit/i.test(text)) return true; // 修复 #11: 更宽松的表头检测 if (/^Name\b/i.test(text) && text.length < 100) return true; if (row.querySelector('input[type="checkbox"]') && !row.querySelector('relative-time[datetime]')) return true; return false; } function getRowTimestamp(row) { if (!row) return null; const timeEl = row.querySelector('relative-time[datetime]'); if (!timeEl) return null; const datetime = timeEl.getAttribute('datetime'); if (!datetime) return null; const ts = new Date(datetime).getTime(); return isNaN(ts) ? null : ts; } // ==================== 修复 #4: 排序标志位,防止 MutationObserver 循环触发 ==================== let isSorting = false; function applyCSSSortOrder(theme) { if (!theme || !theme.SORT || !theme.SORT.isEnabled) return; const containers = document.querySelectorAll( '#repos-file-tree, ' + '[role="rowgroup"], ' + '[role="grid"], ' + '[data-testid="repos-file-tree-container"], ' + '[data-testid="file-list-row-container"], ' + '.react-directory-truncate, ' + '.Box-body, ' + 'table.files tbody, ' + 'tbody' ); if (containers.length === 0) return; isSorting = true; try { containers.forEach(container => { const tag = container.tagName.toUpperCase(); let rows; if (tag === 'TBODY' || tag === 'TABLE') { rows = Array.from(container.querySelectorAll(':scope > tr')); } else { rows = Array.from(container.querySelectorAll( ':scope > [role="row"], ' + ':scope > [data-testid="file-list-row"], ' + ':scope > .Box-row' )); if (rows.length === 0) { rows = Array.from(container.querySelectorAll(':scope > div:has(relative-time[datetime])')); } } if (rows.length <= 1) return; if (tag === 'TBODY' || tag === 'TABLE') { const sorted = rows.slice().sort((a, b) => { const aSpecial = isSpecialRow(a); const bSpecial = isSpecialRow(b); if (aSpecial && !bSpecial) return -1; if (!aSpecial && bSpecial) return 1; if (aSpecial && bSpecial) return 0; const aTs = getRowTimestamp(a); const bTs = getRowTimestamp(b); if (aTs === null || bTs === null) return 0; return theme.SORT.select === 'desc' ? bTs - aTs : aTs - bTs; }); sorted.forEach(row => container.appendChild(row)); return; } container.classList.add('gh-freshness-flex-sort'); const specialRows = []; const normalRows = []; rows.forEach(row => { if (isSpecialRow(row)) { specialRows.push(row); } else { normalRows.push(row); } }); specialRows.forEach(row => { row.style.setProperty('order', '-1', 'important'); }); const rowsWithTs = normalRows .map(row => ({ row, ts: getRowTimestamp(row) })) .filter(item => item.ts !== null); const rowsWithoutTs = normalRows.filter(row => getRowTimestamp(row) === null); rowsWithTs.sort((a, b) => { return theme.SORT.select === 'desc' ? b.ts - a.ts : a.ts - b.ts; }); rowsWithTs.forEach((item, index) => { item.row.style.setProperty('order', String(index), 'important'); }); rowsWithoutTs.forEach((row, index) => { row.style.setProperty('order', String(rowsWithTs.length + index), 'important'); }); }); } finally { isSorting = false; } } // ==================== 主高亮函数 ==================== 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, ' + '#repos-file-tree relative-time, ' + '[role="rowgroup"] relative-time' ); if (timeElements.length > 0) { timeElements.each(function () { const $this = $(this); const datetime = $this.attr('datetime'); if (!datetime) return; const trElement = $this.closest( '[data-testid="file-list-row"], .Box-row, tr, div[role="row"], [role="rowgroup"] > div' ); if (!trElement.length) return; const timeResult = handleTime(datetime, theme.TIME_BOUNDARY); 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); }); } applyCSSSortOrder(theme); } // ==================== 修复 #3: URL 匹配增强 (支持尾部斜杠) ==================== function isMatchedUrl() { const href = window.location.href; if (/^https:\/\/github\.com\/search\?.*$/.test(href)) return 'matchSearchPage'; const reservedPaths = [ 'settings', 'notifications', 'marketplace', 'orgs', 'topics', 'collections', 'features', 'pricing', 'login', 'join', 'new', 'explore', 'sponsors', 'about', 'customer-stories', 'security', 'enterprise', 'team', 'readme', 'nonprofit' ]; const pathMatch = href.match(/^https:\/\/github\.com\/([^/]+)(?:\/([^/]+))?/); if (pathMatch) { const firstSegment = pathMatch[1]; if (reservedPaths.includes(firstSegment)) return null; if (!pathMatch[2]) return null; if (['repositories', 'projects', 'packages', 'stars'].includes(pathMatch[2])) return null; } // 修复 #3: 添加 \/? 支持尾部斜杠 if (/^https:\/\/github\.com\/[^/]+\/[^/]+\/?(?:\?.*)?$|^https:\/\/github\.com\/[^/]+\/[^/]+\/tree\/.+$/.test(href)) return 'matchRepoPage'; return null; } // ==================== Date Formatting ==================== 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}`; } // ==================== 修复 #5: 时间格式化去重 ==================== 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}`; const parent = item.parentNode; if (!parent) return; const existing = parent.querySelector(':scope > .custom-time-format'); if (existing) { if (existing.textContent !== str) existing.textContent = str; item.classList.add('custom-time-replaced'); return; } if (item.nextElementSibling && item.nextElementSibling.classList.contains('custom-time-format')) { if (item.nextElementSibling.textContent !== str) item.nextElementSibling.textContent = str; return; } item.classList.add('custom-time-replaced'); const span = document.createElement('span'); span.className = 'custom-time-format formatted-date-span'; span.textContent = str; parent.insertBefore(span, item.nextSibling); } catch (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(); GitHub_FreshnessAwesome(THEME); forceFormatRelativeTime(); } catch (e) { console.error('GitHub Freshness: Script execution error', e); } }, 350); // ==================== 修复 #4: MutationObserver 忽略排序引起的 DOM 变化 ==================== let mutationObserver = null; function initMutationObserver() { if (mutationObserver) return; mutationObserver = new MutationObserver((mutations) => { // 修复 #4: 排序操作期间不触发 if (isSorting) return; 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) break; } if (shouldRun) runScript(); }); mutationObserver.observe(document.body, { childList: true, subtree: true }); } // ==================== Init ==================== $(function() { console.log('GitHub Freshness v1.2.0 (All Fixes) Loaded'); runScript(); initMutationObserver(); }); 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); }; // ==================== 修复 #6: 定时更新 + 修复 #7: 检查页面匹配 ==================== let lastTimeUpdate = 0; const TIME_UPDATE_INTERVAL = 2000; function scheduleTimeUpdate() { // 修复 #7: 非匹配页面不执行 if (!isMatchedUrl()) return; const now = Date.now(); if (now - lastTimeUpdate < TIME_UPDATE_INTERVAL) return; lastTimeUpdate = now; setTimeout(() => { try { forceFormatRelativeTime(); if (THEME.SORT && THEME.SORT.isEnabled) applyCSSSortOrder(THEME); } catch (e) { /* ignore */ } }, 0); } setInterval(scheduleTimeUpdate, TIME_UPDATE_INTERVAL); // ==================== Menu Command ==================== GM_registerMenuCommand('⚙️ Settings', createSettingsPanel); // ==================== Dark Mode Listener ==================== 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(); } }); })();