GitHub Freshness

通过颜色高亮的方式,帮助你快速判断一个 GitHub 仓库是否在更新。

当前为 2025-02-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Freshness
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.1
  5. // @description 通过颜色高亮的方式,帮助你快速判断一个 GitHub 仓库是否在更新。
  6. // @author 向前 https://home.rational-stars.top/
  7. // @license MIT
  8. // @icon https://raw.githubusercontent.com/rational-stars/picgo/refs/heads/main/avatar.jpg
  9. // @match https://github.com/*/*
  10. // @match https://github.com/*/*?*
  11. // @match https://github.com/search?*
  12. // @match https://github.com/*/*/tree/*/*
  13. // @exclude https://github.com/*/*/*/* /* 继续排除更深层级的路径 */
  14. // @exclude https://github.com/*/*/*/*?*
  15. // @require https://code.jquery.com/jquery-3.6.0.min.js
  16. // @require https://cdn.jsdelivr.net/npm/sweetalert2@11
  17. // @require https://cdn.jsdelivr.net/npm/@simonwep/pickr@1.9.1/dist/pickr.min.js
  18. // @require https://cdn.jsdelivr.net/npm/luxon@3.4.3/build/global/luxon.min.js
  19. // @grant GM_registerMenuCommand
  20. // @grant GM_setValue
  21. // @grant GM_getValue
  22. // @grant GM_addStyle
  23. // ==/UserScript==
  24.  
  25. ;(function () {
  26. // 引入 Luxon
  27. const DateTime = luxon.DateTime
  28. // 解析日期(指定格式和时区)
  29. ;('use strict')
  30. // 引入 Pickr CSS
  31. GM_addStyle(
  32. `@import url('https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/monolith.min.css');`
  33. )
  34. GM_addStyle(`
  35. .swal2-popup.swal2-modal.swal2-show{
  36. color: #FFF;
  37. border-radius: 20px;
  38. background: #31b96c;
  39. box-shadow: 8px 8px 16px #217e49,
  40. -8px -8px 16px #41f48f;
  41. #swal2-title a{
  42. display: inline-block;
  43. height: 40px;
  44. margin-right: 10px;
  45. border-radius: 10px;
  46. overflow: hidden;
  47. color: #fff;
  48. }
  49. #swal2-title {
  50. display: flex !important;
  51. justify-content: center;
  52. align-items: center;
  53. }
  54. .row-box select {
  55. border:unset;
  56. border-radius: .15em;
  57. }
  58. .row-box {
  59. display: flex;
  60. align-items: center;
  61. justify-content: space-between;
  62. margin: 25px;
  63. }
  64. .row-box .swal2-input {
  65. height: 40px;
  66. }
  67. .row-box label {
  68. margin-right: 10px;
  69. }
  70. .row-box main {
  71. display: flex;
  72. align-items: center;
  73. }
  74. .row-box main input{
  75. width: 70px;
  76. border: unset;
  77. box-shadow: unset;
  78. text-align: right;
  79. margin:0;
  80. }
  81. `)
  82. const PanelDom = `
  83. <div class="row-box">
  84. <label for="rpcPort">主题设置:</label>
  85. <main>
  86. <select tabindex="-1" id="THEME-select" class="swal2-input">
  87. <option value="light">light</option>
  88. <option value="dark">dark</option>
  89. </select>
  90. </main>
  91. </div>
  92. <div class="row-box">
  93. <div>
  94. <label id="BGC-label">背景颜色:</label>
  95. <input type="checkbox" id="BGC-enabled">
  96. </div>
  97. <main>
  98. <span id="BGC-highlight-color-value">
  99. <div id="BGC-highlight-color-pickr"></div>
  100. </span>
  101. <span id="BGC-grey-color-value">
  102. <div id="BGC-grey-color-pickr"></div>
  103. </span>
  104. </main>
  105. </div>
  106.  
  107. <div class="row-box">
  108. <label id="TIME_BOUNDARY-label" for="rpcPort">时间阈值:</label>
  109. <main>
  110. <input id="TIME_BOUNDARY-number" type="text" class="swal2-input" value="" maxlength="3" pattern="\d{1,3}">
  111. <select tabindex="-1" id="TIME_BOUNDARY-select" class="swal2-input">
  112. <option value="day">日</option>
  113. <option value="week">周</option>
  114. <option value="month">月</option>
  115. <option value="year">年</option>
  116. </select>
  117. </main>
  118. </div>
  119.  
  120.  
  121. <div class="row-box">
  122. <div>
  123. <label id="FONT-label">字体颜色:</label>
  124. <input type="checkbox" id="FONT-enabled">
  125. </div>
  126. <main>
  127. <span id="FONT-highlight-color-value">
  128. <div id="FONT-highlight-color-pickr"></div>
  129. </span>
  130. <span id="FONT-grey-color-value">
  131. <div id="FONT-grey-color-pickr"></div>
  132. </span>
  133. </main>
  134. </div>
  135.  
  136. <div class="row-box">
  137. <div>
  138. <label id="DIR-label">文件夹颜色:</label>
  139. <input type="checkbox" id="DIR-enabled">
  140. </div>
  141. <main>
  142. <span id="DIR-highlight-color-value">
  143. <div id="DIR-highlight-color-pickr"></div>
  144. </span>
  145. <span id="DIR-grey-color-value">
  146. <div id="DIR-grey-color-pickr"></div>
  147. </span>
  148. </main>
  149. </div>
  150. <div class="row-box">
  151. <div>
  152. <label id="TIME_FORMAT-label">时间格式化:</label>
  153. <input type="checkbox" id="TIME_FORMAT-enabled">
  154. </div>
  155. </div>
  156. <div class="row-box">
  157. <div>
  158. <label id="AWESOME-label" style="text-decoration: line-through;">awesome-xxx项目待开发:</label>
  159. <input type="checkbox" id="AWESOME-enabled">
  160. </div>
  161. </div>
  162.  
  163. <div class="row-box">
  164. <label for="rpcPort">当前主题:</label>
  165. <main>
  166. <select tabindex="-1" id="CURRENT_THEME-select" class="swal2-input">
  167. <option value="auto">auto</option>
  168. <option value="light">light</option>
  169. <option value="dark">dark</option>
  170. </select>
  171. </main>
  172. </div>
  173.  
  174. `
  175. // === 配置项 ===
  176. let default_THEME = {
  177. BGC: {
  178. highlightColor: 'rgba(15, 172, 83, 1)', // 高亮颜色(示例:金色)
  179. greyColor: 'rgba(245, 245, 245, 0.24)', // 灰色(示例:深灰)
  180. isEnabled: true, // 是否启用背景色
  181. },
  182. TIME_BOUNDARY: {
  183. number: 30, // 时间阈值(示例:30)
  184. select: 'day', // 可能的值: "day", "week", "month", "year"
  185. },
  186. FONT: {
  187. highlightColor: 'rgba(252, 252, 252, 1)', // 文字高亮颜色(示例:橙红色)
  188. greyColor: 'rgba(0, 0, 0, 1)', // 灰色(示例:标准灰)
  189. isEnabled: true, // 是否启用字体颜色
  190. },
  191. DIR: {
  192. highlightColor: 'rgba(15, 172, 83, 1)', // 目录高亮颜色(示例:道奇蓝)
  193. greyColor: 'rgba(154, 154, 154, 1)', // 灰色(示例:暗灰)
  194. isEnabled: true, // 是否启用文件夹颜色
  195. },
  196. AWESOME: {
  197. // awesome-xxx 项目待开发
  198. highlightColor: '#1E90FF', // 目录高亮颜色(示例:道奇蓝)
  199. greyColor: '#696969', // 灰色(示例:暗灰)
  200. isEnabled: true, // 是否启用文件夹颜色
  201. },
  202. TIME_FORMAT: {
  203. isEnabled: true, // 是否启用时间格式
  204. },
  205. }
  206. let CURRENT_THEME = GM_getValue('CURRENT_THEME', 'light')
  207. let THEME_TYPE = getThemeType()
  208. function getThemeType() {
  209. let themeType = CURRENT_THEME
  210. if (CURRENT_THEME === 'auto') {
  211. if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
  212. console.log('系统切换到深色模式 🌙')
  213. themeType = 'dark'
  214. } else {
  215. console.log('系统切换到浅色模式 ☀️')
  216. themeType = 'light'
  217. }
  218. }
  219. return themeType
  220. }
  221.  
  222. const config_JSON = JSON.parse(
  223. GM_getValue('config_JSON', JSON.stringify({ light: default_THEME }))
  224. )
  225. let THEME = config_JSON[THEME_TYPE] // 当前主题
  226.  
  227. const configPickr = {
  228. theme: 'monolith', // 使用经典主题
  229. components: {
  230. preview: true,
  231. opacity: true,
  232. hue: true,
  233. interaction: {
  234. rgba: true,
  235. // hex: true,
  236. // hsla: true,
  237. // hsva: true,
  238. // cmyk: true,
  239. input: true,
  240. clear: true,
  241. save: true,
  242. },
  243. },
  244. }
  245. function initPickr(el_default) {
  246. const pickr = Pickr.create({ ...configPickr, ...el_default })
  247. watchPickr(pickr)
  248. }
  249. function watchPickr(pickrName, el) {
  250. pickrName.on('save', (color, instance) => {
  251. pickrName.hide()
  252. })
  253. }
  254.  
  255. function successSwal() {
  256. Swal.fire({
  257. position: 'top-end',
  258. icon: 'success',
  259. title: '设置已保存',
  260. showConfirmButton: false,
  261. timer: 800,
  262. })
  263. }
  264. const preConfirm = () => {
  265. // 遍历默认主题配置,更新设置
  266. const updated_THEME = getUpdatedThemeConfig(default_THEME)
  267. CURRENT_THEME = $('#CURRENT_THEME-select').val()
  268. // 保存到油猴存储
  269. GM_setValue(
  270. 'config_JSON',
  271. JSON.stringify({
  272. ...config_JSON,
  273. [$('#THEME-select').val()]: updated_THEME,
  274. })
  275. )
  276. GM_setValue('CURRENT_THEME', CURRENT_THEME)
  277. THEME = updated_THEME // 更新当前主题
  278. console.log('向前🇨🇳 ====> preConfirm ====> updated_THEME:', updated_THEME)
  279. highlightDates(updated_THEME)
  280. // successSwal()
  281. }
  282. function initSettings(theme) {
  283. initPickr({
  284. el: '#BGC-highlight-color-pickr',
  285. default: theme.BGC.highlightColor,
  286. })
  287. initPickr({ el: '#BGC-grey-color-pickr', default: theme.BGC.greyColor })
  288. initPickr({
  289. el: '#FONT-highlight-color-pickr',
  290. default: theme.FONT.highlightColor,
  291. })
  292. initPickr({ el: '#FONT-grey-color-pickr', default: theme.FONT.greyColor })
  293. initPickr({
  294. el: '#DIR-highlight-color-pickr',
  295. default: theme.DIR.highlightColor,
  296. })
  297. initPickr({ el: '#DIR-grey-color-pickr', default: theme.DIR.greyColor })
  298. $('#THEME-select').val(getThemeType())
  299. $('#CURRENT_THEME-select').val(CURRENT_THEME)
  300. handelData(theme)
  301. }
  302. function getUpdatedThemeConfig() {
  303. // 创建一个新的对象,用于存储更新后的主题配置
  304. let updatedTheme = {}
  305.  
  306. // 遍历默认主题配置,更新需要的键值
  307. for (const [themeKey, themeVal] of Object.entries(default_THEME)) {
  308. updatedTheme[themeKey] = {} // 创建每个主题键名的嵌套对象
  309.  
  310. for (let [key, val] of Object.entries(themeVal)) {
  311. switch (key) {
  312. case 'highlightColor':
  313. // 获取高亮颜色(示例:金色、道奇蓝等)
  314. val = $(`#${themeKey}-highlight-color-value .pcr-button`).css(
  315. '--pcr-color'
  316. )
  317. break
  318. case 'greyColor':
  319. // 获取灰色调(示例:深灰、标准灰、暗灰等)
  320. val = $(`#${themeKey}-grey-color-value .pcr-button`).css(
  321. '--pcr-color'
  322. )
  323. break
  324. case 'isEnabled':
  325. // 判断该主题项是否启用
  326. val = $(`#${themeKey}-enabled`).prop('checked')
  327. break
  328. case 'number':
  329. // 获取时间阈值(示例:30)
  330. val = $(`#${themeKey}-number`).val()
  331. break
  332. case 'select':
  333. // 获取时间单位(可能的值:"day", "week", "month")
  334. val = $(`#${themeKey}-select`).val()
  335. break
  336. default:
  337. // 其他未定义的情况
  338. break
  339. }
  340.  
  341. // 更新当前键名对应的值
  342. updatedTheme[themeKey][key] = val
  343. }
  344. }
  345.  
  346. return updatedTheme
  347. }
  348. function handelData(theme) {
  349. for (const [themeKey, themeVal] of Object.entries(theme)) {
  350. for (const [key, val] of Object.entries(themeVal)) {
  351. switch (key) {
  352. case 'highlightColor':
  353. $(`#${themeKey}-highlight-color-value .pcr-button`).css(
  354. '--pcr-color',
  355. val
  356. )
  357. break
  358. case 'greyColor':
  359. $(`#${themeKey}-grey-color-value .pcr-button`).css(
  360. '--pcr-color',
  361. val
  362. )
  363. break
  364. case 'isEnabled':
  365. $(`#${themeKey}-enabled`).prop('checked', val) // 选中
  366. break
  367. case 'number':
  368. $(`#${themeKey}-number`).val(val)
  369. break
  370. case 'select':
  371. $(`#${themeKey}-select`).val(val)
  372. break
  373. default:
  374. break
  375. }
  376. }
  377. }
  378. }
  379. // === 创建设置面板 ===
  380. function createSettingsPanel() {
  381. Swal.fire({
  382. 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>`,
  383. html: PanelDom,
  384. focusConfirm: false,
  385. preConfirm,
  386. showCancelButton: true,
  387. cancelButtonText: '取消',
  388. confirmButtonText: '保存设置',
  389. })
  390.  
  391. initSettings(THEME)
  392.  
  393. $('#THEME-select').on('change', function () {
  394. let selectedTheme = $(this).val() // 获取选中的值
  395. let theme = config_JSON[selectedTheme]
  396. console.log('主题设置变更:', selectedTheme)
  397. handelData(theme)
  398. })
  399. }
  400.  
  401. function handelTime(time, time_boundary, type = 'ISO8601') {
  402. const { number, select } = time_boundary
  403. let days = 0
  404. // 根据 select 计算相应的天数
  405. switch (select) {
  406. case 'day':
  407. days = number
  408. break
  409. case 'week':
  410. days = number * 7
  411. break
  412. case 'month':
  413. days = number * 30
  414. break
  415. case 'year':
  416. days = number * 365
  417. break
  418. default:
  419. console.warn('无效的时间单位:', select)
  420. return false // 遇到无效单位直接返回 false
  421. }
  422.  
  423. const now = new Date() // 当前时间
  424. const targetDate = new Date(now) // 复制当前时间
  425. targetDate.setDate(now.getDate() - days) // 计算指定时间范围的起点
  426. let inputDate = new Date(time) // 传入的时间转换为 Date 对象
  427. if (type === 'UTC') {
  428. // 解析日期(指定格式和时区)
  429. const dt = DateTime.fromFormat(time, "yyyy年M月d日 'GMT'Z HH:mm", {
  430. zone: 'UTC',
  431. }).setZone('Asia/Shanghai')
  432. const formattedDate = dt.toJSDate()
  433. inputDate = new Date(formattedDate)
  434. }
  435. return inputDate >= targetDate // 判断输入时间是否在 time_boundary 以内
  436. }
  437.  
  438. // === 核心函数 ===
  439. function highlightDatesSearchPage(theme = THEME) {
  440. const elements = $('.Text__StyledText-sc-17v1xeu-0.hWqAbU')
  441. if (elements.length === 0) {
  442. console.log('没有找到日期元素')
  443. return
  444. }
  445. // return
  446. elements.each(function () {
  447. const title = $(this).attr('title')
  448. console.log('向前🇨🇳 ====> $(this):', $(this))
  449. if (title) {
  450. console.log('向前🇨🇳 ====> title:', title)
  451.  
  452. const timeResult = handelTime(title, theme.TIME_BOUNDARY, 'UTC')
  453. let themeType = getThemeType()
  454. console.log('向前🇨🇳 ====> themeType:', themeType)
  455. const BGC_element = $(this).closest(
  456. `.Box-sc-g0xbh4-0 .${themeType === 'dark' ? 'iwUbcA' : 'flszRz'}`
  457. )
  458. console.log('向前🇨🇳 ====> BGC_element:', BGC_element)
  459. // 背景色
  460. if (BGC_element.length && theme.BGC.isEnabled) {
  461. if (timeResult) {
  462. BGC_element.css('background-color', theme.BGC.highlightColor)
  463. } else {
  464. BGC_element.css('background-color', theme.BGC.greyColor)
  465. }
  466. }
  467. // 字体颜色
  468. if (theme.FONT.isEnabled) {
  469. if (timeResult) {
  470. $(this).css('color', theme.FONT.highlightColor)
  471. } else {
  472. $(this).css('color', theme.FONT.greyColor)
  473. }
  474. }
  475. // 时间格式化
  476. if (theme.TIME_FORMAT.isEnabled) {
  477. // 解析日期(指定格式和时区)
  478. const dt = DateTime.fromFormat(title, "yyyy年M月d日 'GMT'Z HH:mm", {
  479. zone: 'UTC',
  480. }).setZone('Asia/Shanghai')
  481.  
  482. // 格式化成 YYYY-MM-DD
  483. const formattedDate = dt.toFormat('yyyy-MM-dd')
  484. $(this).text(formattedDate)
  485. }
  486. }
  487. })
  488. }
  489. function highlightDates(theme = THEME) {
  490. const matchUrl = isMatchedUrl()
  491. if (!matchUrl) return
  492. if (matchUrl === 'matchSearchPage') return highlightDatesSearchPage(theme)
  493. const elements = $('.sc-aXZVg')
  494. if (elements.length === 0) {
  495. console.log('没有找到日期元素')
  496. return
  497. }
  498. // return
  499. elements.each(function () {
  500. const datetime = $(this).attr('datetime')
  501. if (datetime) {
  502. const timeResult = handelTime(datetime, theme.TIME_BOUNDARY)
  503. const trElement = $(this).closest('tr')
  504. // 背景颜色和字体
  505. const BGC_element = $(this).closest('td')
  506. console.log('向前🇨🇳 ====> BGC_element:', BGC_element)
  507. // 在 tr 元素中查找 SVG 元素
  508. const DIR_element = trElement.find('.icon-directory')
  509.  
  510. // 背景色
  511. if (BGC_element.length && theme.BGC.isEnabled) {
  512. if (timeResult) {
  513. BGC_element[0].style.setProperty(
  514. 'background-color',
  515. theme.BGC.highlightColor,
  516. 'important'
  517. )
  518. } else {
  519. BGC_element[0].style.setProperty(
  520. 'background-color',
  521. theme.BGC.greyColor,
  522. 'important'
  523. )
  524. }
  525. }
  526.  
  527. // 文件夹颜色
  528. if (DIR_element.length && theme.DIR.isEnabled) {
  529. if (timeResult) {
  530. DIR_element.attr('fill', theme.DIR.highlightColor)
  531. } else {
  532. DIR_element.attr('fill', theme.DIR.greyColor)
  533. }
  534. }
  535. // 时间格式化
  536. if (theme.TIME_FORMAT.isEnabled && $(this).css('display') !== 'none') {
  537. $(this).css('display', 'none')
  538. const formattedDate = formatDate(datetime)
  539. $(this).before(`<span>${formattedDate}</span>`)
  540. } else {
  541. $(this).parent().find('span').remove()
  542. $(this).css('display', 'block')
  543. }
  544. // 字体颜色
  545. if (theme.FONT.isEnabled) {
  546. if (timeResult) {
  547. $(this).parent().css('color', theme.FONT.highlightColor)
  548. } else {
  549. $(this).parent().css('color', theme.FONT.greyColor)
  550. }
  551. }
  552. }
  553. })
  554. }
  555. function formatDate(isoDateString) {
  556. // 将 ISO 字符串转换为 Date 对象
  557. const date = new Date(isoDateString)
  558.  
  559. // 提取年、月、日
  560. const year = date.getFullYear() // 获取年份
  561. const month = String(date.getMonth() + 1).padStart(2, '0') // 获取月份(补零)
  562. const day = String(date.getDate()).padStart(2, '0') // 获取日期(补零)
  563.  
  564. // 拼接为年月日格式
  565. return `${year}-${month}-${day}`
  566. }
  567. function isMatchedUrl() {
  568. const currentUrl = window.location.href
  569.  
  570. // 判断是否符合 @match 的 URL 模式
  571. const matchRepoPage =
  572. /^https:\/\/github\.com\/[^/]+\/[^/]+(?:\?.*)?$|^https:\/\/github\.com\/[^/]+\/[^/]+\/tree\/.+$/.test(
  573. currentUrl
  574. )
  575. // 判断是否符合 @match 的 URL 模式
  576. const matchSearchPage = /^https:\/\/github\.com\/search\?.*$/.test(
  577. currentUrl
  578. )
  579. // 如果当前是仓库页面,返回变量名
  580. if (matchRepoPage) return 'matchRepoPage'
  581.  
  582. // 如果当前是搜索页面,返回变量名
  583. if (matchSearchPage) return 'matchSearchPage'
  584.  
  585. // 如果没有匹配,返回 null 或空字符串
  586. return null
  587. }
  588.  
  589. function runScript() {
  590. if (!isMatchedUrl()) return // 确保 URL 匹配,避免在不需要的页面运行
  591. setTimeout(() => {
  592. highlightDates()
  593. }, 500)
  594. console.log('✅ 脚本运行在:', window.location.href)
  595. }
  596.  
  597. // **监听 GitHub PJAX 跳转**
  598. document.addEventListener('pjax:end', runScript)
  599.  
  600. // **监听前进/后退**
  601. window.addEventListener('popstate', () => setTimeout(runScript, 300))
  602.  
  603. // **拦截 pushState & replaceState**
  604. ;(function (history) {
  605. const originalPushState = history.pushState
  606. const originalReplaceState = history.replaceState
  607. function newHistoryMethod(method) {
  608. return function () {
  609. const result = method.apply(this, arguments)
  610. setTimeout(runScript, 50)
  611. return result
  612. }
  613. }
  614.  
  615. history.pushState = newHistoryMethod(originalPushState)
  616. history.replaceState = newHistoryMethod(originalReplaceState)
  617. })(window.history)
  618.  
  619. // === 初始化设置面板 ===
  620. // createSettingsPanel()
  621.  
  622. // === 使用油猴菜单显示/隐藏设置面板 ===
  623. GM_registerMenuCommand('⚙️ 设置面板', createSettingsPanel)
  624. // 监听主题变化
  625. window
  626. .matchMedia('(prefers-color-scheme: dark)')
  627. .addEventListener('change', (e) => {
  628. if (e.matches) {
  629. THEME = config_JSON['dark']
  630. console.log('系统切换到深色模式 🌙')
  631. highlightDates(THEME)
  632. } else {
  633. THEME = config_JSON['light']
  634. console.log('系统切换到浅色模式 ☀️')
  635. highlightDates(THEME)
  636. }
  637. })
  638. })()