通过颜色高亮的方式,帮助你快速判断一个 GitHub 仓库是否在更新。
// ==UserScript== // @name GitHub Freshness // @namespace http://tampermonkey.net/ // @version 2.0.0 // @description 通过颜色高亮的方式,帮助你快速判断一个 GitHub 仓库是否在更新。 // @description:en Highlights GitHub repositories by freshness so you can quickly spot active projects. // @author 向前 https://docs.rational-stars.top/ https://github.com/rational-stars/GitHub-Freshness https://home.rational-stars.top/ // @license MIT // @icon https://raw.githubusercontent.com/rational-stars/GitHub-Freshness/refs/heads/main/docs/public/img/branch-clock.png // @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 pickrCSS https://cdn.jsdelivr.net/npm/@simonwep/[email protected]/dist/themes/monolith.min.css // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_getResourceText // @run-at document-start // ==/UserScript== ; (function () { // 引入 Luxon const DateTime = luxon.DateTime // 解析日期(指定格式和时区) ; ('use strict') const SETTINGS_ICON_URL = 'https://raw.githubusercontent.com/rational-stars/GitHub-Freshness/refs/heads/main/docs/public/img/branch-clock.png' // 引入 Pickr CSS。GitHub CSP 会拦截 style 内的 @import,需通过 @resource 内联。 const pickrCSS = GM_getResourceText('pickrCSS') if (pickrCSS) GM_addStyle(pickrCSS) GM_addStyle(` .swal2-popup.swal2-modal.swal2-show{ --gf-panel: #151e2b; --gf-control: #202c3b; --gf-primary: #35d39a; --gf-primary-text: #10251f; --gf-text: #f7fafc; --gf-muted: #9fb0bf; --gf-border: #344357; --gf-link: #58d5cc; color: var(--gf-text); width: min(600px, calc(100% - 24px)) !important; min-width: min(550px, calc(100% - 24px)); max-width: 600px; border: 1px solid var(--gf-border); border-radius: 12px; background: var(--gf-panel); box-shadow: 0 22px 56px rgba(6, 12, 20, .42); } #swal2-title a{ color: #fff; text-decoration: none; } #swal2-title { display: flex !important; gap: 10px; margin: 24px 24px 14px; padding: 0; justify-content: center; align-items: center; line-height: 1.2; color: var(--gf-text) !important; } #swal2-title-div { display: block; flex: 0 0 40px; width: 40px; height: 40px; border-radius: 8px; overflow: hidden; } #swal2-title-div img { display: block; width: 40px; height: 40px; object-fit: cover; } .github-freshness-title-link { display: flex; min-width: 0; align-items: center; } .row-box select { border: 1px solid var(--gf-border); border-radius: 6px; background: var(--gf-control); color: var(--gf-text); } .row-box { box-sizing: border-box; display: grid; grid-template-columns: minmax(120px, 1fr) auto; min-height: 54px; margin: 0; padding: 7px 25px; column-gap: 18px; align-items: center; border-bottom: 1px solid rgba(52, 67, 87, .55); } .row-box .swal2-input { height: 40px; margin: 0; border: 1px solid var(--gf-border); border-radius: 6px; background: var(--gf-control); color: var(--gf-text); } .row-box select.swal2-input{ padding-right: 38px; border: 0; appearance: none; -webkit-appearance: none; background-color: var(--gf-control); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='m4 6 4 4 4-4' stroke='%23f7fafc' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 14px center; background-size: 16px; box-shadow: none; } .row-box select.swal2-input:focus{ box-shadow: 0 0 0 2px rgba(88, 213, 204, .28); } .row-box label { margin-right: 10px; } .row-box > label, .row-box > div { justify-self: start; text-align: left; } .row-box main input{ background: var(--gf-control); } .row-box main { display: flex; align-items: center; justify-content: flex-end; } [id$="-highlight-color-value"] .pickr .pcr-button, [id$="-highlight-color-value"] .pickr .pcr-button::before, [id$="-highlight-color-value"] .pickr .pcr-button::after{ border-radius: 6px 0 0 6px; } [id$="-grey-color-value"] .pickr .pcr-button, [id$="-grey-color-value"] .pickr .pcr-button::before, [id$="-grey-color-value"] .pickr .pcr-button::after{ border-radius: 0 6px 6px 0; } [id$="-color-value"]{ position: relative; display: inline-flex; } [id$="-color-value"]::after{ position: absolute; z-index: 10002; bottom: calc(100% + 8px); left: 50%; padding: 5px 8px; transform: translate(-50%, 4px); transition: opacity .15s ease, transform .15s ease; border: 1px solid var(--gf-border); border-radius: 4px; background: #0d1520; color: var(--gf-text); content: attr(data-color-tooltip); font-size: 12px; line-height: 1.2; white-space: nowrap; opacity: 0; pointer-events: none; } [id$="-color-value"]:hover::after, [id$="-color-value"]:focus-within::after{ transform: translate(-50%, 0); opacity: 1; } .row-box main input{ width: 70px; border: unset; box-shadow: unset; text-align: right; margin:0; } #TIME_BOUNDARY-number{ margin-right: 8px; } .github-freshness-format-value{ box-sizing: border-box; display: inline-flex; min-width: 136px; max-width: 100%; height: 40px; padding: 0 14px; align-items: center; justify-content: center; border: 0; border-radius: 6px; background: var(--gf-control); color: var(--gf-text); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 14px; line-height: 1; white-space: nowrap; } .github-freshness-secret-input{ -webkit-text-security: disc; text-security: disc; } .github-freshness-control-width{ box-sizing: border-box; width: 136px !important; } .github-freshness-tooltip-link{ position: relative; display: inline-block; text-decoration: none; } .github-freshness-tooltip-link::after{ position: absolute; z-index: 10005; bottom: calc(100% + 8px); left: 0; box-sizing: border-box; width: max-content; max-width: min(280px, calc(100vw - 48px)); padding: 6px 9px; transform: translateY(4px); transition: opacity .15s ease, transform .15s ease; border: 1px solid var(--gf-border); border-radius: 4px; background: #0d1520; color: var(--gf-text); content: attr(data-tooltip); font-size: 12px; font-weight: 400; line-height: 1.45; text-align: left; white-space: normal; opacity: 0; pointer-events: none; } .github-freshness-tooltip-link:hover::after, .github-freshness-tooltip-link:focus-visible::after{ transform: translateY(0); opacity: 1; } .row-box input[type="checkbox"]{ accent-color: var(--gf-primary); } .swal2-html-container p{ color: var(--gf-muted); } .swal2-html-container a{ color: var(--gf-link); } .github-freshness-star-reminder{ margin: 0 24px 20px; color: var(--gf-muted); font-size: 12px; line-height: 1.5; text-align: center; animation-name: github-freshness-shake-y; animation-duration: 1s; animation-fill-mode: both; } .github-freshness-footer-links{ display: flex; gap: 10px; align-items: center; justify-content: center; } .github-freshness-footer-link{ display: inline-flex; width: 32px; height: 32px; padding: 0; align-items: center; justify-content: center; border-radius: 6px; color: var(--gf-muted); } .github-freshness-footer-link:hover, .github-freshness-footer-link:focus-visible{ background: var(--gf-control); color: var(--gf-link); outline: none; } .github-freshness-footer-link svg{ width: 18px; height: 18px; } .github-freshness-star-reminder a{ color: var(--gf-link); font-weight: 600; text-decoration: none; } .github-freshness-star-reminder a:hover{ text-decoration: underline; } @keyframes github-freshness-shake-y{ from, to { transform: translate3d(0, 0, 0); } 10%, 30%, 50%, 70%, 90% { transform: translate3d(0, -10px, 0); } 20%, 40%, 60%, 80% { transform: translate3d(0, 10px, 0); } } @media (prefers-reduced-motion: reduce) { .github-freshness-star-reminder{ animation: none; } } .swal2-popup.swal2-modal.swal2-show .swal2-footer{ display: flex; margin: 26px 0 12px; padding: 0 24px; flex-direction: column; gap: 12px; border-top: 0; } .swal2-popup.swal2-modal.swal2-show .swal2-actions{ margin-top: 24px; } .github-freshness-transfer-actions{ display: flex; gap: 10px; } .github-freshness-transfer-button{ min-width: 110px; padding: 9px 14px; border: 1px solid var(--gf-border); border-radius: 6px; background: var(--gf-control); color: var(--gf-text); cursor: pointer; font-weight: 600; } .github-freshness-transfer-button:hover{ border-color: var(--gf-link); background: #283647; } .swal2-confirm.swal2-styled{ background: var(--gf-primary) !important; color: var(--gf-primary-text) !important; } .swal2-cancel.swal2-styled{ border: 1px solid var(--gf-border); background: var(--gf-control) !important; color: var(--gf-text) !important; } .swal2-validation-message{ background: var(--gf-control); color: var(--gf-text); } .github-freshness-toolbar-button{ box-sizing: border-box; display: inline-flex; width: 32px; height: 32px; flex: 0 0 32px; margin-left: 0; padding: 0; align-items: center; justify-content: center; border: 1px solid var(--button-default-borderColor-rest, var(--color-btn-border)); border-radius: 6px; background: var(--button-default-bgColor-rest, var(--color-btn-bg)); box-shadow: var(--button-default-shadow-resting, var(--color-btn-shadow)); cursor: pointer; } .github-freshness-toolbar-button:hover{ background: var(--button-default-bgColor-hover, var(--color-btn-hover-bg)); } .github-freshness-toolbar-button img{ display: block; width: 100%; height: 100%; border-radius: 5px; } .github-freshness-toolbar-button.github-freshness-search-toolbar-button{ width: 28px; height: 28px; flex-basis: 28px; margin-left: 8px; padding: 0; } .github-freshness-toolbar-button.github-freshness-search-toolbar-button img{ width: 100%; height: 100%; border-radius: 5px; } @media (max-width: 480px) { .row-box { grid-template-columns: 1fr; min-height: 0; padding: 10px 16px; row-gap: 8px; } .row-box main { justify-content: flex-start; } .github-freshness-transfer-actions{ flex-wrap: wrap; } .github-freshness-transfer-button{ flex: 1 1 110px; } } `) let CURRENT_LANGUAGE = GM_getValue('CURRENT_LANGUAGE', 'zh') function resolveLocale(language = CURRENT_LANGUAGE) { if (language === 'zh' || language === 'en') return language const pageLang = document.documentElement?.lang || navigator.language || '' return pageLang.toLowerCase().startsWith('zh') ? 'zh' : 'en' } function getLocale() { return resolveLocale() } const messages = { zh: { settings: 'GitHub 新鲜度设置', themeSettings: '主题设置:', timeBoundary: '新鲜期限:', day: '日', week: '周', month: '月', year: '年', backgroundColor: '背景颜色:', fontColor: '文字颜色:', directoryColor: '文件图标:', freshColor: '新鲜颜色', staleColor: '过期颜色', timeFormat: '时间格式化:', fileSort: '文件排序:', sortAsc: '时间正序', sortDesc: '时间倒序', currentTheme: '当前主题:', currentLanguage: '当前语言:', auto: '自动', light: '浅色', dark: '深色', chinese: '中文', english: '英文', awesomeLabel: 'AWESOME', tokenLabel: 'token', awesomeTooltip: 'AWESOME 功能请谨慎开启,点击查看文档', tokenTooltip: '点击去往令牌生成页面', footerDocs: '文档', footerGitHub: 'GitHub 仓库', footerHome: '个人主页', footerTelegram: '交流群', docs: '文档', cancel: '取消', saveSettings: '保存设置', settingsSaved: '设置已保存', settingsTransfer: '设置备份:', importSettings: '导入 JSON', exportSettings: '导出 JSON', importSuccess: '设置导入成功,正在刷新页面', importFailed: '导入失败,请选择有效的 GitHub Freshness JSON 设置文件', rateLimit: '检测到 AWESOME API 速率限制超出!', details: '查看详情', menuSettings: '⚙️ 设置面板', openSettings: '打开 GitHub 新鲜度设置', starMessage: '如果您觉得 GitHub-Freshness 好用,点击下方 GitHub 链接给个 star 吧。非常感谢你!!!', starReminderBefore: '喜欢这个项目?', starReminderLink: '点击这里', starReminderAfter: '给个 Star,感谢支持。', }, en: { settings: 'GitHub Freshness Settings', themeSettings: 'Theme:', timeBoundary: 'Freshness period:', day: 'Day', week: 'Week', month: 'Month', year: 'Year', backgroundColor: 'Background color:', fontColor: 'Text color:', directoryColor: 'File icons:', freshColor: 'Fresh color', staleColor: 'Stale color', timeFormat: 'Date formatting:', fileSort: 'File sorting:', sortAsc: 'Oldest first', sortDesc: 'Newest first', currentTheme: 'Current theme:', currentLanguage: 'Current language:', auto: 'Auto', light: 'Light', dark: 'Dark', chinese: 'Chinese', english: 'English', awesomeLabel: 'AWESOME', tokenLabel: 'token', awesomeTooltip: 'Enable AWESOME with care. Click to view the documentation', tokenTooltip: 'Click to open the token creation page', footerDocs: 'Documentation', footerGitHub: 'GitHub repository', footerHome: 'Personal homepage', footerTelegram: 'Community group', docs: 'docs', cancel: 'Cancel', saveSettings: 'Save settings', settingsSaved: 'Settings saved', settingsTransfer: 'Settings backup:', importSettings: 'Import JSON', exportSettings: 'Export JSON', importSuccess: 'Settings imported. Refreshing the page', importFailed: 'Import failed. Select a valid GitHub Freshness JSON settings file', rateLimit: 'AWESOME API rate limit exceeded!', details: 'Details', menuSettings: '⚙️ Settings', openSettings: 'Open GitHub Freshness settings', starMessage: 'If GitHub-Freshness helps you, please give it a star from the GitHub link below. Thank you!', starReminderBefore: 'Enjoying this project? ', starReminderLink: 'Click here', starReminderAfter: ' to give it a Star. Thank you for your support.', }, } function tFor(key, language = CURRENT_LANGUAGE) { const locale = resolveLocale(language) return messages[locale][key] || messages.zh[key] || key } function t(key) { return tFor(key) } function getStarReminderDom(language = CURRENT_LANGUAGE) { const tr = (key) => tFor(key, language) return `${tr('starReminderBefore')}<a target="_blank" rel="noopener noreferrer" href="https://github.com/rational-stars/GitHub-Freshness">${tr('starReminderLink')}</a>${tr('starReminderAfter')}` } function getAwesomeLabelDom(language = CURRENT_LANGUAGE) { const tr = (key) => tFor(key, language) return `<a class="github-freshness-tooltip-link" target="_blank" rel="noopener noreferrer" href="https://docs.rational-stars.top/diy-settings/awesome-xxx.html" data-tooltip="${tr('awesomeTooltip')}" aria-label="${tr('awesomeTooltip')}">${tr('awesomeLabel')}</a> <a class="github-freshness-tooltip-link" target="_blank" rel="noopener noreferrer" href="https://github.com/settings/tokens" data-tooltip="${tr('tokenTooltip')}" aria-label="${tr('tokenTooltip')}">${tr('tokenLabel')}</a>:` } function getSettingsFooterDom(language = CURRENT_LANGUAGE) { const tr = (key) => tFor(key, language) return ` <div class="github-freshness-footer-links"> <a class="github-freshness-tooltip-link github-freshness-footer-link" target="_blank" rel="noopener noreferrer" href="https://docs.rational-stars.top/" data-tooltip="${tr('footerDocs')}" aria-label="${tr('footerDocs')}"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V5a2 2 0 0 1 2-2h5a3 3 0 0 1 3 3v15a3 3 0 0 0-3-3Z"/><path d="M21 18a1 1 0 0 0 1-1V5a2 2 0 0 0-2-2h-5a3 3 0 0 0-3 3v15a3 3 0 0 1 3-3Z"/></svg> </a> <a class="github-freshness-tooltip-link github-freshness-footer-link" target="_blank" rel="noopener noreferrer" href="https://github.com/rational-stars/GitHub-Freshness" data-tooltip="${tr('footerGitHub')}" aria-label="${tr('footerGitHub')}"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3.3 0 6.8-2 6.8-5.5A4.5 4.5 0 0 0 19.5 6 4.2 4.2 0 0 0 19.4 3S18.4 2.7 16 4.5a13.4 13.4 0 0 0-7 0C6.6 2.7 5.6 3 5.6 3A4.2 4.2 0 0 0 5.5 6a4.5 4.5 0 0 0-1.3 3c0 3.5 3.5 5.5 6.8 5.5-.4.5-.8 1.4-1 2.5-3 .5-3-1.5-4-2l-2-1.5"/><path d="M9 18c-4.5 2-5-2-7-2"/></svg> </a> <a class="github-freshness-tooltip-link github-freshness-footer-link" target="_blank" rel="noopener noreferrer" href="https://home.rational-stars.top/" data-tooltip="${tr('footerHome')}" aria-label="${tr('footerHome')}"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m3 11 9-9 9 9"/><path d="M5 10v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V10"/><path d="M9 22V12h6v10"/></svg> </a> <a class="github-freshness-tooltip-link github-freshness-footer-link" target="_blank" rel="noopener noreferrer" href="https://t.me/GitHubFreshness" data-tooltip="${tr('footerTelegram')}" aria-label="${tr('footerTelegram')}"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg> </a> </div> ` } function getStarReminderElementDom(language = CURRENT_LANGUAGE) { return `<p id="STAR-reminder" class="github-freshness-star-reminder">${getStarReminderDom(language)}</p>` } function getPanelDom() { return ` <div class="row-box"> <label id="THEME-label" for="THEME-select">${t('themeSettings')}</label> <main> <select tabindex="-1" id="THEME-select" class="swal2-input"> <option value="light">${t('light')}</option> <option value="dark">${t('dark')}</option> </select> </main> </div> <div class="row-box"> <label id="TIME_BOUNDARY-label" for="rpcPort">${t('timeBoundary')}</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">${t('day')}</option> <option value="week">${t('week')}</option> <option value="month">${t('month')}</option> <option value="year">${t('year')}</option> </select> </main> </div> <div class="row-box"> <div> <label id="BGC-label">${t('backgroundColor')}</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">${t('fontColor')}</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">${t('directoryColor')}</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">${t('timeFormat')}</label> <input type="checkbox" id="TIME_FORMAT-enabled"> </div> <main> <code class="github-freshness-format-value">yyyy-MM-dd</code> </main> </div> <div class="row-box"> <div> <label id="SORT-label">${t('fileSort')}</label> <input type="checkbox" id="SORT-enabled"> </div> <main> <select tabindex="-1" id="SORT-select" class="swal2-input github-freshness-control-width"> <option value="asc">${t('sortAsc')}</option> <option value="desc">${t('sortDesc')}</option> </select> </main> </div> <div class="row-box"> <div> <label id="AWESOME-label">${getAwesomeLabelDom()}</label> <input type="checkbox" id="AWESOME-enabled"> </div> <main> <input id="AWESOME_TOKEN" type="text" class="swal2-input github-freshness-secret-input github-freshness-control-width" value="" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-lpignore="true" data-1p-ignore="true"> </main> </div> <div class="row-box"> <label id="CURRENT_THEME-label" for="CURRENT_THEME-select">${t('currentTheme')}</label> <main> <select tabindex="-1" id="CURRENT_THEME-select" class="swal2-input"> <option value="auto">${t('auto')}</option> <option value="light">${t('light')}</option> <option value="dark">${t('dark')}</option> </select> </main> </div> <div class="row-box"> <label id="LANGUAGE-label" for="LANGUAGE-select">${t('currentLanguage')}</label> <main> <select tabindex="-1" id="LANGUAGE-select" class="swal2-input"> <option value="auto">${t('auto')}</option> <option value="zh">${t('chinese')}</option> <option value="en">${t('english')}</option> </select> </main> </div> <div class="row-box github-freshness-transfer-row"> <label id="SETTINGS_TRANSFER-label">${t('settingsTransfer')}</label> <main class="github-freshness-transfer-actions"> <button type="button" id="SETTINGS-import" class="github-freshness-transfer-button">${t('importSettings')}</button> <button type="button" id="SETTINGS-export" class="github-freshness-transfer-button">${t('exportSettings')}</button> <input type="file" id="SETTINGS-import-file" accept="application/json,.json" hidden> </main> </div> ` } // === 配置项 === let default_THEME = { BGC: { highlightColor: 'rgba(15, 172, 83, 1)', // 高亮颜色(示例:金色) greyColor: 'rgba(108, 183, 140, 1)', // 灰色(示例:深灰) isEnabled: true, // 是否启用背景色 }, TIME_BOUNDARY: { number: '30', // 时间阈值(示例:30) select: 'day', // 可能的值: "day", "week", "month", "year" }, FONT: { highlightColor: 'rgba(252, 252, 252, 1)', // 文字高亮颜色(示例:橙红色) greyColor: 'rgba(0, 0, 0, 1)', // 灰色(示例:标准灰) isEnabled: true, // 是否启用字体颜色 }, DIR: { highlightColor: 'rgba(15, 172, 83, 1)', // 目录高亮颜色(示例:道奇蓝) greyColor: 'rgba(108, 183, 140, 1)', // 灰色(示例:暗灰) isEnabled: true, // 是否启用文件夹颜色 }, SORT: { select: 'desc', // 排序方式(可能的值:"asc", "desc") isEnabled: true, // 是否启用排序 }, AWESOME: { isEnabled: false, // AWESOME项目是否启用 }, TIME_FORMAT: { isEnabled: true, // 是否启用时间格式化 }, } const default_DARK_THEME = JSON.parse(JSON.stringify(default_THEME)) default_DARK_THEME.BGC.highlightColor = 'rgba(25, 44, 226, 1)' default_DARK_THEME.BGC.greyColor = 'rgba(112, 147, 194, 1)' default_DARK_THEME.DIR.highlightColor = 'rgba(25, 44, 226, 1)' default_DARK_THEME.DIR.greyColor = 'rgba(112, 147, 194, 1)' const default_CONFIG = { light: default_THEME, dark: default_DARK_THEME } let CURRENT_THEME = GM_getValue('CURRENT_THEME', 'auto') let AWESOME_TOKEN = GM_getValue('AWESOME_TOKEN', '') let AWESOME_ENABLED = GM_getValue('AWESOME_ENABLED', null) let THEME_TYPE = getThemeType() let config_JSON = normalizeConfig( JSON.parse(GM_getValue('config_JSON', JSON.stringify(default_CONFIG))) ) let THEME = getThemeConfig(THEME_TYPE) // 当前主题 if (typeof AWESOME_ENABLED !== 'boolean') { AWESOME_ENABLED = Boolean(THEME.AWESOME.isEnabled) } const DEBUG = GM_getValue('DEBUG', false) const PROCESSED_ATTR = 'data-github-freshness' const CODE_BUTTON_ATTR = 'data-github-freshness-code-button' const TOOLBAR_SETTINGS_ID = 'github-freshness-toolbar-settings' const SEARCH_TEXT_SELECTOR = [ '[class*="Content-module__Content"]', '[class*="Content-module__Content"] *', '[class*="Footer-module__footer"]', '[class*="Footer-module__footer"] *', ].join(',') const AWESOME_OBSERVED_ATTR = 'data-github-freshness-awesome-observed' const AWESOME_PROCESSED_ATTR = 'data-github-freshness-awesome-processed' const awesomeRepoCache = new Map() let awesomeRateLimitWarned = false const configPickr = { theme: 'monolith', // 使用经典主题 components: { preview: true, opacity: true, hue: true, interaction: { rgba: true, // hex: true, // hsla: true, // hsva: true, // cmyk: true, input: true, clear: true, save: true, }, }, } const pickrInstances = new Map() let settingsThemeDrafts = null let settingsEditingTheme = null function getThemeType() { let themeType = CURRENT_THEME if (CURRENT_THEME === 'auto') { if (window.matchMedia('(prefers-color-scheme: dark)').matches) { // console.log('当前系统是深色模式 🌙') themeType = 'dark' } else { // console.log('当前系统是浅色模式 ☀️') themeType = 'light' } } showStarMessage() return themeType } function debugLog(...args) { if (DEBUG) console.log(...args) } function showStarMessage() { const logKey = 'github-freshness-star-log' if (sessionStorage.getItem(logKey)) return sessionStorage.setItem(logKey, 'true') window.console.log("%c✅ " + t('starMessage') + "\n[https://github.com/rational-stars/GitHub-Freshness]", "color:green") } function cloneTheme(theme = default_THEME) { return JSON.parse(JSON.stringify(theme)) } function normalizeTheme(theme = {}) { const normalizedTheme = cloneTheme(default_THEME) for (const [themeKey, themeVal] of Object.entries(theme || {})) { normalizedTheme[themeKey] = { ...(normalizedTheme[themeKey] || {}), ...(themeVal || {}), } } return normalizedTheme } function normalizeConfig(config = {}) { const light = normalizeTheme(config.light || default_THEME) const dark = normalizeTheme(config.dark || config.light || default_THEME) return { ...config, light, dark } } function getThemeConfig(themeType = THEME_TYPE) { const theme = normalizeTheme(config_JSON[themeType] || config_JSON.light || default_THEME) if (typeof AWESOME_ENABLED === 'boolean') theme.AWESOME.isEnabled = AWESOME_ENABLED return theme } function syncAwesomeEnabled(config, isEnabled) { const normalizedConfig = normalizeConfig(config) for (const themeName of ['light', 'dark']) { normalizedConfig[themeName].AWESOME.isEnabled = isEnabled } return normalizedConfig } function initPickr(key, options) { pickrInstances.get(key)?.destroyAndRemove() const pickr = Pickr.create({ ...configPickr, ...options }) watchPickr(pickr) pickrInstances.set(key, pickr) return pickr } function watchPickr(pickr) { pickr.on('save', () => pickr.hide()) } function destroyPickrs() { for (const pickr of pickrInstances.values()) pickr.destroyAndRemove() pickrInstances.clear() } function destroySettingsPanelState() { destroyPickrs() settingsThemeDrafts = null settingsEditingTheme = null } const preConfirm = async () => { captureSettingsThemeDraft() CURRENT_THEME = $('#CURRENT_THEME-select').val() CURRENT_LANGUAGE = $('#LANGUAGE-select').val() AWESOME_ENABLED = $('#AWESOME-enabled').prop('checked') const previousAwesomeToken = AWESOME_TOKEN AWESOME_TOKEN = $('#AWESOME_TOKEN').val() if (AWESOME_TOKEN !== previousAwesomeToken) { awesomeRepoCache.clear() awesomeRateLimitWarned = false } // 保存到油猴存储 config_JSON = syncAwesomeEnabled({ ...config_JSON, light: settingsThemeDrafts.light, dark: settingsThemeDrafts.dark, }, AWESOME_ENABLED) GM_setValue('config_JSON', JSON.stringify(config_JSON)) GM_setValue('CURRENT_THEME', CURRENT_THEME) GM_setValue('CURRENT_LANGUAGE', CURRENT_LANGUAGE) GM_setValue('AWESOME_TOKEN', AWESOME_TOKEN) GM_setValue('AWESOME_ENABLED', AWESOME_ENABLED) THEME_TYPE = getThemeType() THEME = getThemeConfig(THEME_TYPE) GitHub_Freshness(THEME) await Swal.fire({ position: 'top', background: '#151e2b', color: '#f7fafc', icon: 'success', iconColor: '#35d39a', title: t('settingsSaved'), showConfirmButton: false, timer: 800, }) window.location.reload() } function initSettings() { destroyPickrs() settingsThemeDrafts = { light: getThemeConfig('light'), dark: getThemeConfig('dark'), } settingsEditingTheme = getThemeType() const theme = settingsThemeDrafts[settingsEditingTheme] initPickr('BGC.highlightColor', { el: '#BGC-highlight-color-pickr', default: theme.BGC.highlightColor, }) initPickr('BGC.greyColor', { el: '#BGC-grey-color-pickr', default: theme.BGC.greyColor }) initPickr('FONT.highlightColor', { el: '#FONT-highlight-color-pickr', default: theme.FONT.highlightColor, }) initPickr('FONT.greyColor', { el: '#FONT-grey-color-pickr', default: theme.FONT.greyColor }) initPickr('DIR.highlightColor', { el: '#DIR-highlight-color-pickr', default: theme.DIR.highlightColor, }) initPickr('DIR.greyColor', { el: '#DIR-grey-color-pickr', default: theme.DIR.greyColor }) $('#THEME-select').val(settingsEditingTheme) $('#CURRENT_THEME-select').val(CURRENT_THEME) $('#LANGUAGE-select').val(CURRENT_LANGUAGE) $('#AWESOME_TOKEN').val(AWESOME_TOKEN) handelData(theme) $('#AWESOME-enabled').prop('checked', AWESOME_ENABLED) updateColorPickerLabels() } function getUpdatedThemeConfig() { // 创建一个新的对象,用于存储更新后的主题配置 let updatedTheme = {} // 遍历默认主题配置,更新需要的键值 for (const [themeKey, themeVal] of Object.entries(default_THEME)) { updatedTheme[themeKey] = {} // 创建每个主题键名的嵌套对象 for (let [key, val] of Object.entries(themeVal)) { switch (key) { case 'highlightColor': // 获取高亮颜色(示例:金色、道奇蓝等) val = pickrInstances .get(`${themeKey}.highlightColor`) ?.getColor() ?.toRGBA() .toString(3) || val break case 'greyColor': // 获取灰色调(示例:深灰、标准灰、暗灰等) val = pickrInstances .get(`${themeKey}.greyColor`) ?.getColor() ?.toRGBA() .toString(3) || val break case 'isEnabled': // 判断该主题项是否启用 val = $(`#${themeKey}-enabled`).prop('checked') break case 'number': // 获取时间阈值(示例:30) val = $(`#${themeKey}-number`).val() break case 'select': // 获取时间单位(可能的值:"day", "week", "month") val = $(`#${themeKey}-select`).val() break default: // 其他未定义的情况 break } // 更新当前键名对应的值 updatedTheme[themeKey][key] = val } } return updatedTheme } function captureSettingsThemeDraft() { if (!settingsThemeDrafts || !settingsEditingTheme) return const draft = getUpdatedThemeConfig() draft.AWESOME.isEnabled = $('#AWESOME-enabled').prop('checked') settingsThemeDrafts[settingsEditingTheme] = draft } function handelData(theme) { for (const [themeKey, themeVal] of Object.entries(theme)) { for (const [key, val] of Object.entries(themeVal)) { switch (key) { case 'highlightColor': pickrInstances .get(`${themeKey}.highlightColor`) ?.setColor(val, false) break case 'greyColor': pickrInstances .get(`${themeKey}.greyColor`) ?.setColor(val, false) break case 'isEnabled': if (themeKey !== 'AWESOME') { $(`#${themeKey}-enabled`).prop('checked', val) // 选中 } break case 'number': $(`#${themeKey}-number`).val(val) break case 'select': $(`#${themeKey}-select`).val(val) break default: break } } } } function updateSettingsLanguagePreview(language) { const tr = (key) => tFor(key, language) $('.github-freshness-title-link').text(tr('settings')) $('#THEME-label').text(tr('themeSettings')) $('#TIME_BOUNDARY-label').text(tr('timeBoundary')) $('#BGC-label').text(tr('backgroundColor')) $('#FONT-label').text(tr('fontColor')) $('#DIR-label').text(tr('directoryColor')) $('#TIME_FORMAT-label').text(tr('timeFormat')) $('#SORT-label').text(tr('fileSort')) $('#CURRENT_THEME-label').text(tr('currentTheme')) $('#LANGUAGE-label').text(tr('currentLanguage')) $('#SETTINGS_TRANSFER-label').text(tr('settingsTransfer')) $('#AWESOME-label').html(getAwesomeLabelDom(language)) $('#THEME-select option[value="light"]').text(tr('light')) $('#THEME-select option[value="dark"]').text(tr('dark')) $('#TIME_BOUNDARY-select option[value="day"]').text(tr('day')) $('#TIME_BOUNDARY-select option[value="week"]').text(tr('week')) $('#TIME_BOUNDARY-select option[value="month"]').text(tr('month')) $('#TIME_BOUNDARY-select option[value="year"]').text(tr('year')) $('#SORT-select option[value="asc"]').text(tr('sortAsc')) $('#SORT-select option[value="desc"]').text(tr('sortDesc')) $('#CURRENT_THEME-select option[value="auto"]').text(tr('auto')) $('#CURRENT_THEME-select option[value="light"]').text(tr('light')) $('#CURRENT_THEME-select option[value="dark"]').text(tr('dark')) $('#LANGUAGE-select option[value="auto"]').text(tr('auto')) $('#LANGUAGE-select option[value="zh"]').text(tr('chinese')) $('#LANGUAGE-select option[value="en"]').text(tr('english')) $('#SETTINGS-import').text(tr('importSettings')) $('#SETTINGS-export').text(tr('exportSettings')) $('.swal2-footer').html(getSettingsFooterDom(language)) $('#STAR-reminder').html(getStarReminderDom(language)) updateColorPickerLabels(language) $('.swal2-confirm').text(tr('saveSettings')) $('.swal2-cancel').text(tr('cancel')) } function updateColorPickerLabels(language = CURRENT_LANGUAGE) { const tr = (key) => tFor(key, language) $('[id$="-highlight-color-value"] .pcr-button') .attr('title', tr('freshColor')) .attr('aria-label', tr('freshColor')) $('[id$="-highlight-color-value"]') .attr('data-color-tooltip', tr('freshColor')) $('[id$="-grey-color-value"] .pcr-button') .attr('title', tr('staleColor')) .attr('aria-label', tr('staleColor')) $('[id$="-grey-color-value"]') .attr('data-color-tooltip', tr('staleColor')) } function getSettingsFromPanel() { captureSettingsThemeDraft() const awesomeEnabled = $('#AWESOME-enabled').prop('checked') const config = syncAwesomeEnabled({ ...config_JSON, light: settingsThemeDrafts.light, dark: settingsThemeDrafts.dark, }, awesomeEnabled) return { schemaVersion: 1, config_JSON: config, CURRENT_THEME: $('#CURRENT_THEME-select').val() || CURRENT_THEME, CURRENT_LANGUAGE: $('#LANGUAGE-select').val() || CURRENT_LANGUAGE, AWESOME_ENABLED: awesomeEnabled, } } function exportSettings() { const settings = getSettingsFromPanel() const locale = resolveLocale(settings.CURRENT_LANGUAGE) const filename = locale === 'zh' ? 'github-freshness-设置.json' : 'github-freshness-settings.json' const blob = new Blob([JSON.stringify(settings, null, 2)], { type: 'application/json;charset=utf-8', }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = filename document.body.appendChild(link) link.click() link.remove() URL.revokeObjectURL(url) } function normalizeImportedSettings(value) { if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid settings') let importedConfig = value.config_JSON if (typeof importedConfig === 'string') importedConfig = JSON.parse(importedConfig) if (!importedConfig || typeof importedConfig !== 'object' || Array.isArray(importedConfig)) { throw new Error('Invalid config_JSON') } if (!importedConfig.light && !importedConfig.dark) throw new Error('Missing theme config') for (const themeName of ['light', 'dark']) { const theme = importedConfig[themeName] if (theme && (typeof theme !== 'object' || Array.isArray(theme))) { throw new Error(`Invalid ${themeName} theme`) } } const hasLanguage = Object.prototype.hasOwnProperty.call(value, 'CURRENT_LANGUAGE') if (hasLanguage && !['auto', 'zh', 'en'].includes(value.CURRENT_LANGUAGE)) { throw new Error('Invalid CURRENT_LANGUAGE') } const currentTheme = ['auto', 'light', 'dark'].includes(value.CURRENT_THEME) ? value.CURRENT_THEME : 'light' const hasAwesomeEnabled = Object.prototype.hasOwnProperty.call(value, 'AWESOME_ENABLED') if (hasAwesomeEnabled && typeof value.AWESOME_ENABLED !== 'boolean') { throw new Error('Invalid AWESOME_ENABLED') } const normalizedConfig = normalizeConfig(importedConfig) const effectiveTheme = currentTheme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : currentTheme const awesomeEnabled = hasAwesomeEnabled ? value.AWESOME_ENABLED : Boolean(normalizedConfig[effectiveTheme].AWESOME.isEnabled) return { config_JSON: syncAwesomeEnabled(normalizedConfig, awesomeEnabled), CURRENT_THEME: currentTheme, CURRENT_LANGUAGE: hasLanguage ? value.CURRENT_LANGUAGE : CURRENT_LANGUAGE, AWESOME_ENABLED: awesomeEnabled, hasLanguage, } } async function importSettings(file) { try { const settings = normalizeImportedSettings(JSON.parse(await file.text())) GM_setValue('config_JSON', JSON.stringify(settings.config_JSON)) GM_setValue('CURRENT_THEME', settings.CURRENT_THEME) GM_setValue('AWESOME_ENABLED', settings.AWESOME_ENABLED) if (settings.hasLanguage) GM_setValue('CURRENT_LANGUAGE', settings.CURRENT_LANGUAGE) const effectiveLanguage = settings.hasLanguage ? settings.CURRENT_LANGUAGE : CURRENT_LANGUAGE await Swal.fire({ position: 'top', background: '#151e2b', color: '#f7fafc', icon: 'success', iconColor: '#35d39a', title: messages[resolveLocale(effectiveLanguage)].importSuccess, showConfirmButton: false, timer: 900, }) window.location.reload() } catch (error) { debugLog('导入设置失败:', error) Swal.showValidationMessage(t('importFailed')) } } // === 创建设置面板 === function createSettingsPanel() { Swal.fire({ title: `<a target="_blank" tabindex="-1" id="swal2-title-div" href="https://home.rational-stars.top/"><img src="${SETTINGS_ICON_URL}" alt="GitHub Freshness"></a><a class="github-freshness-title-link" tabindex="-1" target="_blank" href="https://github.com/rational-stars/GitHub-Freshness">${t('settings')}</a>`, html: getPanelDom(), footer: getSettingsFooterDom(), focusConfirm: false, preConfirm, didClose: destroySettingsPanelState, heightAuto: false, showCancelButton: true, cancelButtonText: t('cancel'), confirmButtonText: t('saveSettings'), }) $('.swal2-footer').after(getStarReminderElementDom()) initSettings() $('#THEME-select').on('change', function () { const selectedTheme = $(this).val() captureSettingsThemeDraft() settingsEditingTheme = selectedTheme debugLog('主题设置变更:', selectedTheme) handelData(settingsThemeDrafts[selectedTheme]) }) $('#LANGUAGE-select').on('change', function () { updateSettingsLanguagePreview($(this).val()) }) $('#SETTINGS-export').on('click', exportSettings) $('#SETTINGS-import').on('click', () => $('#SETTINGS-import-file').trigger('click')) $('#SETTINGS-import-file').on('change', async function () { const file = this.files && this.files[0] if (file) await importSettings(file) this.value = '' }) } function getRepoCodeButton() { return $('button, summary').filter(function () { if (!$(this).find('svg.octicon-code').length) return false const label = $(this).text().replace(/\s+/g, ' ').trim() return label === 'Code' || label === '代码' }).first() } function resetCodeButtonStyle(button) { if (button.attr(CODE_BUTTON_ATTR) !== 'true') return const element = button[0] element.style.removeProperty('background-color') element.style.removeProperty('border-color') element.style.removeProperty('color') button.find('svg').each(function () { this.style.removeProperty('color') this.style.removeProperty('fill') }) button.removeAttr(CODE_BUTTON_ATTR) } function getToolbarSettingsButton() { let settingsButton = document.getElementById(TOOLBAR_SETTINGS_ID) if (!settingsButton) { settingsButton = document.createElement('button') settingsButton.type = 'button' settingsButton.id = TOOLBAR_SETTINGS_ID settingsButton.className = 'github-freshness-toolbar-button' settingsButton.innerHTML = `<img src="${SETTINGS_ICON_URL}" alt="">` settingsButton.addEventListener('click', createSettingsPanel) } settingsButton.title = t('openSettings') settingsButton.setAttribute('aria-label', t('openSettings')) return settingsButton } function setupRepoToolbar(theme = THEME) { const codeButton = getRepoCodeButton() if (!codeButton.length) return false resetCodeButtonStyle(codeButton) if (theme.BGC.isEnabled) { codeButton[0].style.setProperty('background-color', theme.BGC.highlightColor, 'important') codeButton[0].style.setProperty('border-color', theme.BGC.highlightColor, 'important') } if (theme.FONT.isEnabled) { codeButton[0].style.setProperty('color', theme.FONT.highlightColor, 'important') codeButton.find('svg').each(function () { this.style.setProperty('color', theme.FONT.highlightColor, 'important') this.style.setProperty('fill', theme.FONT.highlightColor, 'important') }) } codeButton.attr(CODE_BUTTON_ATTR, 'true') const details = codeButton.closest('details') const anchor = details.length ? details[0] : codeButton[0] const settingsButton = getToolbarSettingsButton() settingsButton.classList.remove('github-freshness-search-toolbar-button') if (anchor.nextElementSibling !== settingsButton) { anchor.insertAdjacentElement('afterend', settingsButton) } return true } function setupSearchToolbar() { const controls = document.querySelector( '[data-testid="search-sub-header"] [class*="SearchSubHeader-module__controlsWrapper"]' ) if (!controls) return false const settingsButton = getToolbarSettingsButton() settingsButton.classList.add('github-freshness-search-toolbar-button') if (settingsButton.parentElement !== controls || controls.lastElementChild !== settingsButton) { controls.appendChild(settingsButton) } return true } function setElementBGC(el, BGC, timeResult) { // el是元素 BGC是 theme BGC配置对象 if (el.length && BGC.isEnabled) { if (timeResult) { el[0].style.setProperty('background-color', BGC.highlightColor, 'important') } else { el[0].style.setProperty('background-color', BGC.greyColor, 'important') } } } function setElementDIR(el, DIR, timeResult) { if (el.length && DIR.isEnabled) { const color = timeResult ? DIR.highlightColor : DIR.greyColor el.each(function () { this.style.setProperty('color', color, 'important') this.style.setProperty('fill', color, 'important') $(this) .find('path') .each(function () { this.style.setProperty('fill', color, 'important') }) }) } } function setElementTIME_FORMAT(el, TIME_FORMAT, datetime) { if (TIME_FORMAT.isEnabled && el.css('display') !== 'none') { el.css('display', 'none') const formattedDate = formatDate(datetime) el.before(`<span>${formattedDate}</span>`) } else if (TIME_FORMAT.isEnabled === false) { el.parent().find('span').remove() el.css('display', 'block') } } // 设置字体颜色 function setElementFONT(el, FONT, timeResult) { // el是元素 FONT是 theme FONT配置对象 if (FONT.isEnabled) { if (timeResult) { el.css('color', FONT.highlightColor) } else { el.css('color', FONT.greyColor) } } } function setSearchResultFONT(result, FONT, timeResult) { if (!result.length) return const targets = result.find(SEARCH_TEXT_SELECTOR) targets.each(function () { this.style.removeProperty('color') }) if (!FONT.isEnabled) return const color = timeResult ? FONT.highlightColor : FONT.greyColor targets.each(function () { this.style.setProperty('color', color, 'important') }) } function isValidDate(date) { return date instanceof Date && !Number.isNaN(date.getTime()) } function parseGitHubNumericDate(time) { const normalizedTime = String(time || '').trim() const match = normalizedTime.match( /^(\d{4})[年/-](\d{1,2})[月/-](\d{1,2})日?(?:\s+GMT([+-])(\d{1,2})(?::?(\d{2}))?)?\s+(\d{1,2}):(\d{2})(?:\s+GMT([+-])(\d{1,2})(?::?(\d{2}))?)?$/ ) if (!match) return null const [ , year, month, day, prefixSign, prefixOffsetHour, prefixOffsetMinute = '0', hour, minute, suffixSign, suffixOffsetHour, suffixOffsetMinute = '0', ] = match const sign = prefixSign || suffixSign || '+' const offsetHour = prefixOffsetHour || suffixOffsetHour || '0' const offsetMinute = prefixOffsetMinute || suffixOffsetMinute || '0' const offsetMinutes = (Number(offsetHour) * 60 + Number(offsetMinute)) * (sign === '+' ? 1 : -1) const utcTime = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute)) - offsetMinutes * 60 * 1000 return new Date(utcTime) } function parseGitHubDateTitle(time) { const numericDate = parseGitHubNumericDate(time) if (numericDate) return numericDate const nativeTimestamp = Date.parse(time) if (!Number.isNaN(nativeTimestamp)) return new Date(nativeTimestamp) return null } function parseTime(time, type = 'ISO8601') { if (type === 'UTC') { const dt = DateTime.fromFormat(time, "yyyy年M月d日 'GMT'Z HH:mm", { zone: 'UTC', }).setZone('Asia/Shanghai') if (dt.isValid) return dt.toJSDate() const githubDate = parseGitHubDateTitle(time) if (githubDate) return githubDate } return new Date(time) } function handelTime(time, time_boundary, type = 'ISO8601') { const { number, select } = time_boundary let days = 0 // 根据 select 计算相应的天数 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: console.warn('无效的时间单位:', select) return false // 遇到无效单位直接返回 false } const now = new Date() // 当前时间 const targetDate = new Date(now) // 复制当前时间 targetDate.setDate(now.getDate() - days) // 计算指定时间范围的起点 let inputDate = parseTime(time, type) // 传入的时间转换为 Date 对象 if (!isValidDate(inputDate)) return false return inputDate >= targetDate // 判断输入时间是否在 time_boundary 以内 } const reservedGitHubPaths = new Set([ 'about', 'apps', 'blog', 'collections', 'contact', 'customer-stories', 'dashboard', 'enterprise', 'events', 'explore', 'features', 'issues', 'login', 'marketplace', 'new', 'notifications', 'orgs', 'pricing', 'pulls', 'search', 'settings', 'signup', 'sponsors', 'topics', 'trending', ]) function getGitHubRepoInfo(href) { try { const url = new URL(href, window.location.origin) if (url.origin !== 'https://github.com') return null const [owner, repo, extraPath] = url.pathname.split('/').filter(Boolean) if (!owner || !repo || extraPath || reservedGitHubPaths.has(owner.toLowerCase())) { return null } return { owner, repo } } catch (err) { debugLog('无效的 GitHub 链接:', href) return null } } function isValidHref(href) { return !!getGitHubRepoInfo(href); } function toAPIUrl(href) { const repoInfo = getGitHubRepoInfo(href) if (!repoInfo) return null return 'https://api.github.com/repos/' + repoInfo.owner + '/' + repoInfo.repo; } function requestAwesomeRepo(apiHref) { if (!awesomeRepoCache.has(apiHref)) { awesomeRepoCache.set( apiHref, $.ajax({ url: apiHref, method: 'GET', headers: AWESOME_TOKEN ? { 'Authorization': `token ${AWESOME_TOKEN}` } : {}, }) ) } return awesomeRepoCache.get(apiHref) } function showAwesomeRateLimitWarning() { if (awesomeRateLimitWarned) return awesomeRateLimitWarned = true Swal.fire({ position: 'top', icon: 'warning', iconColor: '#ffc24b', title: t('rateLimit'), confirmButtonText: t('details'), showConfirmButton: true, background: '#151e2b', color: '#f7fafc', preConfirm: () => { window.open("https://home.rational-stars.top/", "_blank") } }) } // === 核心函数 === function GitHub_FreshnessSearchPage(theme = THEME) { setupSearchToolbar() const elements = getSearchDateElements() if (elements.length === 0) return debugLog('没有找到日期元素') elements.each(function () { const title = $(this).attr('title') if (title) { const timeResult = handelTime(title, theme.TIME_BOUNDARY, 'UTC') const BGC_element = getSearchResultElement($(this)) BGC_element.attr(PROCESSED_ATTR, 'true') // 背景色 setElementBGC(BGC_element, theme.BGC, timeResult) // 字体颜色 setSearchResultFONT(BGC_element, theme.FONT, timeResult) // 时间格式化 if (theme.TIME_FORMAT.isEnabled) { const formattedDate = formatDate(title, 'UTC') $(this).text(formattedDate) } } }) } function getSearchDateElements() { const oldElements = $('.Text__StyledText-sc-17v1xeu-0.hWqAbU') if (oldElements.length) return oldElements return $('[data-testid="results-list"] [title]').filter(function () { const title = $(this).attr('title') || '' const hasNestedDateTitle = $(this) .children('[title]') .toArray() .some((el) => $(el).attr('title') === title) const looksLikeDateTitle = /\b\d{4}\b/.test(title) && /(GMT[+-]|\d{1,2}:\d{2}|T\d{2}:\d{2})/.test(title) return looksLikeDateTitle && isValidDate(parseTime(title, 'UTC')) && !hasNestedDateTitle }) } function getSearchResultElement(el) { const result = el.closest('[class*="Result-module__Result"]') if (result.length) return result const resultRow = el.closest('[class*="Repositories-module__resultRow"]') if (resultRow.length) { const resultContainer = resultRow.parent().filter(function () { return $(this).parent().is('[data-testid="results-list"]') }) return resultContainer.length ? resultContainer : resultRow } const resultsList = el.closest('[data-testid="results-list"]') return resultsList.children('div').has(el).first() } function GitHub_FreshnessAwesome(theme = THEME) { // 选择符合条件的 <a> 标签 let elementsToObserve = []; $('.markdown-body a[href], article.markdown-body a[href], .Box-sc-g0xbh4-0.csrIcr a').each(function () { let href = $(this).attr('href'); // 只处理符合 href 条件的 <a> 标签 if (isValidHref(href) && this.getAttribute(AWESOME_OBSERVED_ATTR) !== 'true') { this.setAttribute(AWESOME_OBSERVED_ATTR, 'true') elementsToObserve.push(this); // 存储符合条件的元素 } }); // 使用 IntersectionObserver 监听元素是否进入/离开视口 const observer = new IntersectionObserver(function (entries, observer) { entries.forEach(el => { if (!el.isIntersecting) return const target = el.target const href = $(el.target).attr('href'); const apiHref = toAPIUrl(href) if (target.getAttribute(AWESOME_PROCESSED_ATTR) !== 'true' && apiHref) { requestAwesomeRepo(apiHref) .done(function (data) { const stars = data.stargazers_count; // 获取星标数 const time = data.updated_at; // 获取星标数 const timeResult = handelTime(time, theme.TIME_BOUNDARY); // 添加标签 if (theme.AWESOME.isEnabled && target.getAttribute(AWESOME_PROCESSED_ATTR) !== 'true') { $(target).after(`<span class="github-freshness-awesome-meta" style="padding: 8px">★${stars} 📅${formatDate(time)}</span>`); target.setAttribute(AWESOME_PROCESSED_ATTR, 'true') } setElementBGC($(target), theme.BGC, timeResult) // 字体颜色 setElementFONT($(target), theme.FONT, timeResult) $(target).css('padding', '0 12px') }) .fail(function (err) { if (err.status === 403) { showAwesomeRateLimitWarning() } }); } else { debugLog('AWESOME 链接已处理或无效:', href); } }); }, { threshold: 0.5 }); // 当元素至少 50% 进入视口时触发回调 // 开始监听所有符合条件的元素 elementsToObserve.forEach(function (el) { observer.observe(el); }); } function GitHub_Freshness(theme = THEME) { const matchUrl = isMatchedUrl() if (!matchUrl) return true if (matchUrl === 'matchSearchPage') { GitHub_FreshnessSearchPage(theme) return true } setupRepoToolbar(theme) const elements = $('tr.react-directory-row relative-time[datetime], .sc-aXZVg[datetime]') if (elements.length === 0) { debugLog('等待 GitHub 加载真实日期元素') return false } debugLog("向前🇨🇳 ====> GitHub_Freshness ====> elements:", elements.length) let trRows = [] elements.each(function () { const datetime = $(this).attr('datetime') if (datetime) { const timeResult = handelTime(datetime, theme.TIME_BOUNDARY) const trElement = $(this).closest('tr.react-directory-row') if (!trElement.length) return trElement.attr(PROCESSED_ATTR, 'true') trRows.push(trElement[0]) // 背景颜色和字体 const BGC_element = $(this).closest('td') // 在 tr 元素中查找 SVG 元素 const DIR_element = trElement.find( 'svg.icon-directory, svg.octicon-file-directory, svg.octicon-file-directory-fill' ) const FILE_element = trElement.find('svg.octicon-file') // 背景色 setElementBGC(BGC_element, theme.BGC, timeResult) // 文件夹颜色和文件图标 setElementDIR(DIR_element, theme.DIR, timeResult) setElementDIR(FILE_element, theme.DIR, timeResult) // 时间格式化 setElementTIME_FORMAT($(this), theme.TIME_FORMAT, datetime) // 字体颜色 setElementFONT($(this).parent(), theme.FONT, timeResult) } }) // 文件排序 if (theme.SORT.isEnabled) { // 将 tr 元素按日期排序 trRows.sort((a, b) => { // 获取 datetime 属性 let dateA = new Date(a.querySelector('relative-time').getAttribute('datetime')); let dateB = new Date(b.querySelector('relative-time').getAttribute('datetime')); // 根据 isAscending 变量控制排序顺序 return theme.SORT.select === 'asc' ? dateA - dateB : dateB - dateA; }); const tbody = $('table[aria-labelledby="folders-and-files"] tbody, .Box-sc-g0xbh4-0.fdROMU tbody').first() if (tbody.length) tbody.append(trRows); } const repoTitle = $('#repo-title-component a, [itemprop="name"] a, strong[itemprop="name"] a') .first() .text() .toLowerCase() if (theme.AWESOME.isEnabled && repoTitle.includes('awesome')) { GitHub_FreshnessAwesome() } return true } function formatDate(dateString, type = 'ISO8601') { if (type === 'UTC') { const parsedDate = parseTime(dateString, type) if (!isValidDate(parsedDate)) return dateString return DateTime.fromJSDate(parsedDate).setZone('Asia/Shanghai').toFormat('yyyy-MM-dd'); } return DateTime.fromISO(dateString).toFormat('yyyy-MM-dd'); } function isMatchedUrl() { const currentUrl = window.location.href // 判断是否符合 @match 的 URL 模式 const matchRepoPage = /^https:\/\/github\.com\/[^/]+\/[^/]+(?:\?.*)?$|^https:\/\/github\.com\/[^/]+\/[^/]+\/tree\/.+$/.test( currentUrl ) // 判断是否符合 @match 的 URL 模式 const matchSearchPage = /^https:\/\/github\.com\/search\?.*$/.test( currentUrl ) // 如果当前是仓库页面,返回变量名 if (matchRepoPage) return 'matchRepoPage' // 如果当前是搜索页面,返回变量名 if (matchSearchPage) return 'matchSearchPage' // 如果没有匹配,返回 null 或空字符串 return null } let runScheduled = false function runScript() { if (runScheduled) return runScheduled = true queueMicrotask(() => { runScheduled = false GitHub_Freshness() }) } function containsPendingRepoContent(node) { if (!(node instanceof Element)) return false const dateElements = node.matches('relative-time[datetime]') ? [node] : [...node.querySelectorAll('tr.react-directory-row relative-time[datetime]')] if (dateElements.some((dateElement) => { const row = dateElement.closest('tr.react-directory-row') return row && row.getAttribute(PROCESSED_ATTR) !== 'true' })) return true const codeIcon = node.matches('svg.octicon-code') ? node : node.querySelector('button svg.octicon-code, summary svg.octicon-code') const codeControl = codeIcon && codeIcon.closest('button, summary') return !!codeControl && codeControl.getAttribute(CODE_BUTTON_ATTR) !== 'true' } function containsPendingSearchContent(node) { if (!(node instanceof Element)) return false const selector = [ '[data-testid="search-sub-header"]', '[data-testid="results-list"]', '[class*="Result-module__Result"]', ].join(',') return node.matches(selector) || !!node.querySelector(selector) } // 页面加载完成后执行 window.addEventListener('load', () => { debugLog("页面加载完成 => 执行 runScript"); runScript(); // 页面加载完成后执行 GitHub_Freshness }); runScript(); // 监听页面是否从不可见切换到可见 document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { debugLog("页面切换到前台 => 执行 runScript"); runScript(); // 页面切换到前台时执行 } }); // 监听 pjax:end 事件,确保页面内容完全加载 document.addEventListener('pjax:end', () => { debugLog('GitHub PJAX 跳转,页面内容已加载'); runScript(); // 页面内容加载完成后执行 GitHub_Freshness }); document.addEventListener('turbo:render', runScript) document.addEventListener('turbo:load', runScript) const repositoryObserver = new MutationObserver((mutations) => { const matchUrl = isMatchedUrl() let shouldProcess = false if (matchUrl === 'matchRepoPage') { shouldProcess = mutations.some((mutation) => [...mutation.addedNodes].some(containsPendingRepoContent) ) } else if (matchUrl === 'matchSearchPage') { shouldProcess = mutations.some((mutation) => [...mutation.addedNodes].some(containsPendingSearchContent) ) } if (!shouldProcess) return runScript() }) function observeRepositoryRoot() { if (document.documentElement) { repositoryObserver.observe(document.documentElement, { childList: true, subtree: true }) return } const rootObserver = new MutationObserver(() => { if (!document.documentElement) return rootObserver.disconnect() repositoryObserver.observe(document.documentElement, { childList: true, subtree: true }) runScript() }) rootObserver.observe(document, { childList: true }) } observeRepositoryRoot(); // 重写 history.pushState 和 history.replaceState 来处理 URL 变化 (function (history) { const pushState = history.pushState; const replaceState = history.replaceState; // 监听 pushState 事件,确保 URL 变化时执行 history.pushState = function (state, title, url) { pushState.apply(history, arguments); // 调用原始的 pushState debugLog('pushState 触发,URL 变化:', url); runScript() }; // 监听 replaceState 事件,确保 URL 变化时执行 history.replaceState = function (state, title, url) { replaceState.apply(history, arguments); // 调用原始的 replaceState debugLog('replaceState 触发,URL 变化:', url); runScript() }; // 监听浏览器的前进/后退按钮 (popstate) window.addEventListener('popstate', () => { debugLog('popstate 触发,URL 变化:', window.location.href); runScript() }); })(window.history); // === 初始化设置面板 === // createSettingsPanel() // === 使用油猴菜单显示/隐藏设置面板 === GM_registerMenuCommand(t('menuSettings'), createSettingsPanel) // 监听主题变化 window .matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', (e) => { if (e.matches) { THEME = getThemeConfig('dark') // console.log('系统切换到深色模式 🌙') GitHub_Freshness(THEME) } else { THEME = getThemeConfig('light') // console.log('系统切换到浅色模式 ☀️') GitHub_Freshness(THEME) } }) })()