GitHub Freshness

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

Versión del día 17/2/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         GitHub Freshness
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  通过颜色高亮的方式,帮助你快速判断一个 GitHub 仓库是否在更新。
// @author       向前  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/*/*?*
// @match        https://github.com/search?*
// @match        https://github.com/*/*/tree/*/*
// @exclude      https://github.com/*/*/*/*  /* 继续排除更深层级的路径 */
// @exclude      https://github.com/*/*/*/*?*
// @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/pickr/dist/themes/monolith.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;
          align-items: center;
          justify-content: space-between;
          margin: 25px;
          }
          .row-box .swal2-input {
          height: 40px;
          }
          .row-box label {
          margin-right: 10px;
          }
          .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">
                  <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">
                  <label id="TIME_BOUNDARY-label" for="rpcPort">时间阈值:</label>
                  <main>
                      <input id="TIME_BOUNDARY-number" type="text" 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="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="AWESOME-label" style="text-decoration: line-through;">awesome-xxx项目待开发:</label>
                      <input type="checkbox" id="AWESOME-enabled">
                  </div>
              </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>

          `
    // === 配置项 ===
    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, // 是否启用文件夹颜色
      },
      AWESOME: {
        // awesome-xxx 项目待开发
        highlightColor: '#1E90FF', // 目录高亮颜色(示例:道奇蓝)
        greyColor: '#696969', // 灰色(示例:暗灰)
        isEnabled: true, // 是否启用文件夹颜色
      },
      TIME_FORMAT: {
        isEnabled: true, // 是否启用时间格式
      },
    }
    let CURRENT_THEME = GM_getValue('CURRENT_THEME', 'light')
    let THEME_TYPE = getThemeType()
    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'
        }
      }
      return themeType
    }

    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 initPickr(el_default) {
      const pickr = Pickr.create({ ...configPickr, ...el_default })
      watchPickr(pickr)
    }
    function watchPickr(pickrName, el) {
      pickrName.on('save', (color, instance) => {
        pickrName.hide()
      })
    }

    function successSwal() {
      Swal.fire({
        position: 'top-end',
        icon: 'success',
        title: '设置已保存',
        showConfirmButton: false,
        timer: 800,
      })
    }
    const preConfirm = () => {
      // 遍历默认主题配置,更新设置
      const updated_THEME = getUpdatedThemeConfig(default_THEME)
      CURRENT_THEME = $('#CURRENT_THEME-select').val()
      // 保存到油猴存储
      GM_setValue(
        'config_JSON',
        JSON.stringify({
          ...config_JSON,
          [$('#THEME-select').val()]: updated_THEME,
        })
      )
      GM_setValue('CURRENT_THEME', CURRENT_THEME)
      THEME = updated_THEME // 更新当前主题
      console.log('向前🇨🇳 ====> preConfirm ====> updated_THEME:', updated_THEME)
      highlightDates(updated_THEME)
      // successSwal()
    }
    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)
      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,
        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 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 以内
    }

    // === 核心函数 ===
    function highlightDatesSearchPage(theme = THEME) {
      const elements = $('.Text__StyledText-sc-17v1xeu-0.hWqAbU')
      if (elements.length === 0) {
        console.log('没有找到日期元素')
        return
      }
      // return
      elements.each(function () {
        const title = $(this).attr('title')
        console.log('向前🇨🇳 ====> $(this):', $(this))
        if (title) {
          console.log('向前🇨🇳 ====> title:', title)

          const timeResult = handelTime(title, theme.TIME_BOUNDARY, 'UTC')
          let themeType = getThemeType()
          console.log('向前🇨🇳 ====> themeType:', themeType)
          const BGC_element = $(this).closest(
            `.Box-sc-g0xbh4-0 .${themeType === 'dark' ? 'iwUbcA' : 'flszRz'}`
          )
          console.log('向前🇨🇳 ====> BGC_element:', BGC_element)
          // 背景色
          if (BGC_element.length && theme.BGC.isEnabled) {
            if (timeResult) {
              BGC_element.css('background-color', theme.BGC.highlightColor)
            } else {
              BGC_element.css('background-color', theme.BGC.greyColor)
            }
          }
          // 字体颜色
          if (theme.FONT.isEnabled) {
            if (timeResult) {
              $(this).css('color', theme.FONT.highlightColor)
            } else {
              $(this).css('color', theme.FONT.greyColor)
            }
          }
          // 时间格式化
          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 highlightDates(theme = THEME) {
      const matchUrl = isMatchedUrl()
      if (!matchUrl) return
      if (matchUrl === 'matchSearchPage') return highlightDatesSearchPage(theme)
      const elements = $('.sc-aXZVg')
      if (elements.length === 0) {
        console.log('没有找到日期元素')
        return
      }
      // return
      elements.each(function () {
        const datetime = $(this).attr('datetime')
        if (datetime) {
          const timeResult = handelTime(datetime, theme.TIME_BOUNDARY)
          const trElement = $(this).closest('tr')
          // 背景颜色和字体
          const BGC_element = $(this).closest('td')
          console.log('向前🇨🇳 ====> BGC_element:', BGC_element)
          // 在 tr 元素中查找 SVG 元素
          const DIR_element = trElement.find('.icon-directory')

          // 背景色
          if (BGC_element.length && theme.BGC.isEnabled) {
            if (timeResult) {
              BGC_element[0].style.setProperty(
                'background-color',
                theme.BGC.highlightColor,
                'important'
              )
            } else {
              BGC_element[0].style.setProperty(
                'background-color',
                theme.BGC.greyColor,
                'important'
              )
            }
          }

          // 文件夹颜色
          if (DIR_element.length && theme.DIR.isEnabled) {
            if (timeResult) {
              DIR_element.attr('fill', theme.DIR.highlightColor)
            } else {
              DIR_element.attr('fill', theme.DIR.greyColor)
            }
          }
          // 时间格式化
          if (theme.TIME_FORMAT.isEnabled && $(this).css('display') !== 'none') {
            $(this).css('display', 'none')
            const formattedDate = formatDate(datetime)
            $(this).before(`<span>${formattedDate}</span>`)
          } else {
            $(this).parent().find('span').remove()
            $(this).css('display', 'block')
          }
          // 字体颜色
          if (theme.FONT.isEnabled) {
            if (timeResult) {
              $(this).parent().css('color', theme.FONT.highlightColor)
            } else {
              $(this).parent().css('color', theme.FONT.greyColor)
            }
          }
        }
      })
    }
    function formatDate(isoDateString) {
      // 将 ISO 字符串转换为 Date 对象
      const date = new Date(isoDateString)

      // 提取年、月、日
      const year = date.getFullYear() // 获取年份
      const month = String(date.getMonth() + 1).padStart(2, '0') // 获取月份(补零)
      const day = String(date.getDate()).padStart(2, '0') // 获取日期(补零)

      // 拼接为年月日格式
      return `${year}-${month}-${day}`
    }
    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 runScript() {
      if (!isMatchedUrl()) return // 确保 URL 匹配,避免在不需要的页面运行
      setTimeout(() => {
        highlightDates()
      }, 500)
      console.log('✅ 脚本运行在:', window.location.href)
    }

    // **监听 GitHub PJAX 跳转**
    document.addEventListener('pjax:end', runScript)

    // **监听前进/后退**
    window.addEventListener('popstate', () => setTimeout(runScript, 300))

    // **拦截 pushState & replaceState**
    ;(function (history) {
      const originalPushState = history.pushState
      const originalReplaceState = history.replaceState
      function newHistoryMethod(method) {
        return function () {
          const result = method.apply(this, arguments)
          setTimeout(runScript, 50)
          return result
        }
      }

      history.pushState = newHistoryMethod(originalPushState)
      history.replaceState = newHistoryMethod(originalReplaceState)
    })(window.history)

    // === 初始化设置面板 ===
    // createSettingsPanel()

    // === 使用油猴菜单显示/隐藏设置面板 ===
    GM_registerMenuCommand('⚙️ 设置面板', createSettingsPanel)
    // 监听主题变化
    window
      .matchMedia('(prefers-color-scheme: dark)')
      .addEventListener('change', (e) => {
        if (e.matches) {
          THEME = config_JSON['dark']
          console.log('系统切换到深色模式 🌙')
          highlightDates(THEME)
        } else {
          THEME = config_JSON['light']
          console.log('系统切换到浅色模式 ☀️')
          highlightDates(THEME)
        }
      })
  })()