通过颜色高亮的方式,帮助你快速判断一个 GitHub 仓库是否在更新。
// ==UserScript== // @name GitHub Freshness // @namespace http://tampermonkey.net/ // @version 1.1.5 // @description 通过颜色高亮的方式,帮助你快速判断一个 GitHub 仓库是否在更新。 // @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/picgo/refs/heads/main/avatar.jpg // @match https://github.com/*/* // @match https://github.com/search?* // @match https://github.com/*/*/tree/* // @require https://code.jquery.com/jquery-3.6.0.min.js // @require https://cdn.jsdelivr.net/npm/sweetalert2@11 // @require https://cdn.jsdelivr.net/npm/@simonwep/[email protected]/dist/pickr.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/build/global/luxon.min.js // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // ==/UserScript== ; (function () { // 引入 Luxon const DateTime = luxon.DateTime // 解析日期(指定格式和时区) ; ('use strict') // 引入 Pickr CSS GM_addStyle(`@import url('https://cdn.jsdelivr.net/npm/@simonwep/[email protected]/dist/themes/classic.min.css');`) GM_addStyle(` .swal2-popup.swal2-modal.swal2-show{ color: #FFF; border-radius: 20px; background: #31b96c; box-shadow: 8px 8px 16px #217e49, -8px -8px 16px #41f48f; #swal2-title a{ display: inline-block; height: 40px; margin-right: 10px; border-radius: 10px; overflow: hidden; color: #fff; } #swal2-title { display: flex !important; justify-content: center; align-items: center; } .row-box select { border:unset; border-radius: .15em; } .row-box { display: flex; margin: 25px; align-items: center; justify-content: space-between; } .row-box .swal2-input { height: 40px; } .row-box label { margin-right: 10px; } .row-box main input{ background: rgba(15, 172, 83, 1); } .row-box main { display: flex; align-items: center; } .row-box main input{ width: 70px; border: unset; box-shadow: unset; text-align: right; margin:0; } `) const PanelDom = ` <div class="row-box"> <label for="rpcPort">主题设置:</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" for="rpcPort">时间阈值:</label> <main> <input id="TIME_BOUNDARY-number" type="number" class="swal2-input" value="" maxlength="3" pattern="\d{1,3}"> <select tabindex="-1" id="TIME_BOUNDARY-select" class="swal2-input"> <option value="day">日</option> <option value="week">周</option> <option value="month">月</option> <option value="year">年</option> </select> </main> </div> <div class="row-box"> <div> <label id="BGC-label">背景颜色:</label> <input type="checkbox" id="BGC-enabled"> </div> <main> <span id="BGC-highlight-color-value"> <div id="BGC-highlight-color-pickr"></div> </span> <span id="BGC-grey-color-value"> <div id="BGC-grey-color-pickr"></div> </span> </main> </div> <div class="row-box"> <div> <label id="FONT-label">字体颜色:</label> <input type="checkbox" id="FONT-enabled"> </div> <main> <span id="FONT-highlight-color-value"> <div id="FONT-highlight-color-pickr"></div> </span> <span id="FONT-grey-color-value"> <div id="FONT-grey-color-pickr"></div> </span> </main> </div> <div class="row-box"> <div> <label id="DIR-label">文件夹颜色:</label> <input type="checkbox" id="DIR-enabled"> </div> <main> <span id="DIR-highlight-color-value"> <div id="DIR-highlight-color-pickr"></div> </span> <span id="DIR-grey-color-value"> <div id="DIR-grey-color-pickr"></div> </span> </main> </div> <div class="row-box"> <div> <label id="TIME_FORMAT-label">时间格式化:</label> <input type="checkbox" id="TIME_FORMAT-enabled"> </div> </div> <div class="row-box"> <div> <label id="SORT-label">文件排序:</label> <input type="checkbox" id="SORT-enabled"> </div> <main> <select tabindex="-1" id="SORT-select" class="swal2-input"> <option value="asc">时间正序</option> <option value="desc">时间倒序</option> </select> </main> </div> <div class="row-box"> <label for="rpcPort">当前主题:</label> <main> <select tabindex="-1" id="CURRENT_THEME-select" class="swal2-input"> <option value="auto">auto</option> <option value="light">light</option> <option value="dark">dark</option> </select> </main> </div> <div class="row-box"> <div> <label id="AWESOME-label"><a target="_blank" href="https://github.com/settings/tokens">AWESOME token: </a></label> <input type="checkbox" id="AWESOME-enabled"> </div> <main> <input id="AWESOME_TOKEN" type="password" class="swal2-input" value=""> </main> </div> <p>当复选框切换到未勾选状态时,部分设置不会立即生效需重新刷新页面。AWESOME谨慎开启详细说明请看 <a target="_blank" href="https://docs.rational-stars.top/diy-settings/awesome-xxx.html"> 文档ℹ️</><p/> ` // === 配置项 === let default_THEME = { BGC: { highlightColor: 'rgba(15, 172, 83, 1)', // 高亮颜色(示例:金色) greyColor: 'rgba(245, 245, 245, 0.24)', // 灰色(示例:深灰) 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(154, 154, 154, 1)', // 灰色(示例:暗灰) isEnabled: true, // 是否启用文件夹颜色 }, SORT: { select: 'desc', // 排序方式(可能的值:"asc", "desc") isEnabled: true, // 是否启用排序 }, AWESOME: { isEnabled: false, // AWESOME项目是否启用 }, TIME_FORMAT: { isEnabled: true, // 是否启用时间格式化 }, } let CURRENT_THEME = GM_getValue('CURRENT_THEME', 'light') let AWESOME_TOKEN = GM_getValue('AWESOME_TOKEN', '') let THEME_TYPE = getThemeType() const config_JSON = JSON.parse( GM_getValue('config_JSON', JSON.stringify({ light: default_THEME })) ) let THEME = config_JSON[THEME_TYPE] // 当前主题 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, }, }, } 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' } } window.console.log("%c✅向前:" + "如果您觉得GitHub-Freshness好用,点击下方 github链接 给个 star 吧。非常感谢你!!!\n[https://github.com/rational-stars/GitHub-Freshness]", "color:green") return themeType } function initPickr(el_default) { const pickr = Pickr.create({ ...configPickr, ...el_default }) watchPickr(pickr) } function watchPickr(pickrName, el) { pickrName.on('save', (color, instance) => { pickrName.hide() }) } const preConfirm = () => { // 遍历默认主题配置,更新设置 const updated_THEME = getUpdatedThemeConfig(default_THEME) CURRENT_THEME = $('#CURRENT_THEME-select').val() AWESOME_TOKEN = $('#AWESOME_TOKEN').val() // 保存到油猴存储 GM_setValue( 'config_JSON', JSON.stringify({ ...config_JSON, [$('#THEME-select').val()]: updated_THEME, }) ) GM_setValue('CURRENT_THEME', CURRENT_THEME) GM_setValue('AWESOME_TOKEN', AWESOME_TOKEN) THEME = updated_THEME // 更新当前主题 GitHub_Freshness(updated_THEME) Swal.fire({ position: 'top-center', background: '#4ab96f', icon: 'success', title: '设置已保存', showConfirmButton: false, timer: 800, }) } function initSettings(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(getThemeType()) $('#CURRENT_THEME-select').val(CURRENT_THEME) $('#AWESOME_TOKEN').val(AWESOME_TOKEN) handelData(theme) } 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 = $(`#${themeKey}-highlight-color-value .pcr-button`).css( '--pcr-color' ) break case 'greyColor': // 获取灰色调(示例:深灰、标准灰、暗灰等) val = $(`#${themeKey}-grey-color-value .pcr-button`).css( '--pcr-color' ) 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 handelData(theme) { for (const [themeKey, themeVal] of Object.entries(theme)) { for (const [key, val] of Object.entries(themeVal)) { switch (key) { case 'highlightColor': $(`#${themeKey}-highlight-color-value .pcr-button`).css( '--pcr-color', val ) break case 'greyColor': $(`#${themeKey}-grey-color-value .pcr-button`).css( '--pcr-color', val ) break case 'isEnabled': $(`#${themeKey}-enabled`).prop('checked', val) // 选中 break case 'number': $(`#${themeKey}-number`).val(val) break case 'select': $(`#${themeKey}-select`).val(val) break default: break } } } } // === 创建设置面板 === function createSettingsPanel() { Swal.fire({ title: `<a target="_blank" tabindex="-1" id="swal2-title-div" href="https://home.rational-stars.top/"><img src="https://raw.githubusercontent.com/rational-stars/picgo/refs/heads/main/avatar.jpg" alt="向前" width="40"></a><a tabindex="-1" target="_blank" href="https://github.com/rational-stars/GitHub-Freshness">GitHub Freshness 设置</a>`, html: PanelDom, focusConfirm: false, preConfirm, heightAuto: false, showCancelButton: true, cancelButtonText: '取消', confirmButtonText: '保存设置', }) initSettings(THEME) $('#THEME-select').on('change', function () { let selectedTheme = $(this).val() // 获取选中的值 let theme = config_JSON[selectedTheme] console.log('主题设置变更:', selectedTheme) handelData(theme) }) } function setElementBGC(el, BGC, timeR###lt) { // el是元素 BGC是 theme BGC配置对象 if (el.length && BGC.isEnabled) { if (timeR###lt) { el[0].style.setProperty('background-color', BGC.highlightColor, 'important') } else { el[0].style.setProperty('background-color', BGC.greyColor, 'important') } } } function setElementDIR(el, DIR, timeR###lt) { if (el.length && DIR.isEnabled) { if (timeR###lt) { el.attr('fill', DIR.highlightColor) } else { el.attr('fill', DIR.greyColor) } } } 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, timeR###lt) { // el是元素 FONT是 theme FONT配置对象 if (FONT.isEnabled) { if (timeR###lt) { el.css('color', FONT.highlightColor) } else { el.css('color', FONT.greyColor) } } } 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 = new Date(time) // 传入的时间转换为 Date 对象 if (type === 'UTC') { // 解析日期(指定格式和时区) const dt = DateTime.fromFormat(time, "yyyy年M月d日 'GMT'Z HH:mm", { zone: 'UTC', }).setZone('Asia/Shanghai') const formattedDate = dt.toJSDate() inputDate = new Date(formattedDate) } return inputDate >= targetDate // 判断输入时间是否在 time_boundary 以内 } // 检查 href 是否符合 https://github.com/*/* 但不是 https://github.com/*/*/ 格式 const pattern = /^https:\/\/github\.com\/[^\/]+\/[^\/]+\/?$/; function isValidHref(href) { return pattern.test(href); } function toAPIUrl(href) { // 使用正则表达式从 href 中提取 owner 和 repo const githubPattern = /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)/; const match = href.match(githubPattern); // 如果匹配成功,则生成 API URL if (match) { let owner = match[1]; // GitHub 仓库所有者 let repo = match[2]; // GitHub 仓库名称 // 返回转换后的 GitHub API URL return 'https://api.github.com/repos/' + owner + '/' + repo; } else { console.log("无效的 GitHub 链接:", href); return null; } } // === 核心函数 === function GitHub_FreshnessSearchPage(theme = THEME) { const elements = $('.Text__StyledText-sc-17v1xeu-0.hWqAbU') if (elements.length === 0) return console.log('没有找到日期元素') let themeType = getThemeType() elements.each(function () { const title = $(this).attr('title') if (title) { const timeR###lt = handelTime(title, theme.TIME_BOUNDARY, 'UTC') const BGC_element = $(this).closest( `.Box-sc-g0xbh4-0 .${themeType === 'dark' ? 'iwUbcA' : 'flszRz'}` ) // 背景色 setElementBGC(BGC_element, theme.BGC, timeR###lt) // 字体颜色 setElementFONT($(this), theme.FONT, timeR###lt) // 时间格式化 if (theme.TIME_FORMAT.isEnabled) { // 解析日期(指定格式和时区) const dt = DateTime.fromFormat(title, "yyyy年M月d日 'GMT'Z HH:mm", { zone: 'UTC', }).setZone('Asia/Shanghai') // 格式化成 YYYY-MM-DD const formattedDate = dt.toFormat('yyyy-MM-dd') $(this).text(formattedDate) } } }) } function GitHub_FreshnessAwesome(theme = THEME) { // 选择符合条件的 <a> 标签 let elementsToObserve = []; $('.Box-sc-g0xbh4-0.csrIcr a').each(function () { let href = $(this).attr('href'); // 只处理符合 href 条件的 <a> 标签 if (isValidHref(href)) { elementsToObserve.push(this); // 存储符合条件的元素 } }); // 使用 IntersectionObserver 监听元素是否进入/离开视口 const observer = new IntersectionObserver(function (entries, observer) { entries.forEach(el => { const href = $(el.target).attr('href'); const apiHref = toAPIUrl(href) if (el.isIntersecting && el.target.getAttribute('request') !== 'true' && apiHref) { $.ajax({ url: apiHref, // API 地址 method: 'GET', // 请求方式 headers: { 'Authorization': `token ${AWESOME_TOKEN}` || '' // 替换为你的个人访问令牌 }, success: function (data) { const stars = data.stargazers_count; // 获取星标数 const time = data.updated_at; // 获取星标数 const timeR###lt = handelTime(time, theme.TIME_BOUNDARY); // 添加标签 if (theme.AWESOME.isEnabled && el.target.getAttribute('request') !== 'true') { $(el.target).after(`<span class="stars" style="padding: 8px">★${stars}</span><span class="updated-at">📅${formatDate(time)}</span>`); el.target.setAttribute('request', 'true') } setElementBGC($(el.target), theme.BGC, timeR###lt) // 字体颜色 setElementFONT($(el.target), theme.FONT, timeR###lt) $(el.target).css('padding', '0 12px') }, error: function (err) { if (err.status === 403) { Swal.fire({ position: 'top-center', icon: 'warning', title: '检测到AWESOME API 速率限制超出!', confirmButtonText: '查看详情', showConfirmButton: true, background: '#4ab96f', preConfirm: () => { window.open("https://home.rational-stars.top/", "_blank") } }) } } }); } else { // console.log('元素离开视口:', href); } }); }, { threshold: 0.5 }); // 当元素至少 50% 进入视口时触发回调 // 开始监听所有符合条件的元素 elementsToObserve.forEach(function (el) { observer.observe(el); }); } function GitHub_Freshness(theme = THEME) { const matchUrl = isMatchedUrl() if (!matchUrl) return if (matchUrl === 'matchSearchPage') return GitHub_FreshnessSearchPage(theme) const elements = $('.sc-aXZVg') if (elements.length === 0) return console.log('没有找到日期元素'); console.log("向前🇨🇳 ====> GitHub_Freshness ====> elements:", elements.length) let trRows = [] elements.each(function () { const datetime = $(this).attr('datetime') if (datetime) { const timeR###lt = handelTime(datetime, theme.TIME_BOUNDARY) const trElement = $(this).closest('tr.react-directory-row') trRows.push(trElement[0]) // 背景颜色和字体 const BGC_element = $(this).closest('td') // 在 tr 元素中查找 SVG 元素 const DIR_element = trElement.find('.icon-directory') const FILE_element = trElement.find('.color-fg-muted') // 背景色 setElementBGC(BGC_element, theme.BGC, timeR###lt) // 文件夹颜色和文件图标 setElementDIR(DIR_element, theme.DIR, timeR###lt) setElementDIR(FILE_element, theme.DIR, timeR###lt) // 时间格式化 setElementTIME_FORMAT($(this), theme.TIME_FORMAT, datetime) // 字体颜色 setElementFONT($(this).parent(), theme.FONT, timeR###lt) } }) // 文件排序 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; }); $('.Box-sc-g0xbh4-0.fdROMU tbody').append(trRows); } if (theme.AWESOME.isEnabled && $('#repo-title-component a').text().toLowerCase().includes('awesome')) { GitHub_FreshnessAwesome() } } function formatDate(isoDateString) { return DateTime.fromISO(isoDateString).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 } function debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } const runScript = debounce(() => { if (!isMatchedUrl()) return; GitHub_Freshness(); // 页面内容加载完成后执行 }, 350); // 设置合适的延迟,避免频繁执行 // 页面加载完成后执行 window.addEventListener('load', () => { console.log("页面加载完成 => 执行 runScript"); runScript(); // 页面加载完成后执行 GitHub_Freshness }); // 监听页面是否从不可见切换到可见 document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { console.log("页面切换到前台 => 执行 runScript"); runScript(); // 页面切换到前台时执行 } }); // 监听 pjax:end 事件,确保页面内容完全加载 document.addEventListener('pjax:end', () => { console.log('GitHub PJAX 跳转,页面内容已加载'); runScript(); // 页面内容加载完成后执行 GitHub_Freshness }); // 重写 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 console.log('pushState 触发,URL 变化:', url); setTimeout(runScript, 350); // 页面内容加载完成后执行 runScript }; // 监听 replaceState 事件,确保 URL 变化时执行 history.replaceState = function (state, title, url) { replaceState.apply(history, arguments); // 调用原始的 replaceState console.log('replaceState 触发,URL 变化:', url); setTimeout(runScript, 350); // 页面内容加载完成后执行 runScript }; // 监听浏览器的前进/后退按钮 (popstate) window.addEventListener('popstate', () => { console.log('popstate 触发,URL 变化:', window.location.href); setTimeout(runScript, 500); // 页面内容加载完成后执行 runScript }); })(window.history); // === 初始化设置面板 === // createSettingsPanel() // === 使用油猴菜单显示/隐藏设置面板 === GM_registerMenuCommand('⚙️ 设置面板', createSettingsPanel) // 监听主题变化 window .matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', (e) => { if (e.matches) { THEME = config_JSON['dark'] // console.log('系统切换到深色模式 🌙') GitHub_Freshness(THEME) } else { THEME = config_JSON['light'] // console.log('系统切换到浅色模式 ☀️') GitHub_Freshness(THEME) } }) })()