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

图片[1]-通过颜色高亮的方式,帮助你快速判断一个 GitHub 仓库是否在更新。-小金同学

原作者:https://github.com/rational-stars/GitHub-Freshness

原作者断更,拿ai修复了一下,可以正常使用了。

// ==UserScript==
// @name         GitHub 颜色高亮
// @namespace    http://tampermonkey.net/
// @version      1.2.0
// @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/pickr@1.9.1/dist/pickr.min.js
// @require      https://cdn.jsdelivr.net/npm/luxon@3.4.3/build/global/luxon.min.js
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @downloadURL https://update.greasyfork.org/scripts/524465/GitHub%20Freshness.user.js
// @updateURL https://update.greasyfork.org/scripts/524465/GitHub%20Freshness.meta.js
// ==/UserScript==

;(function () {
  'use strict'  // fix: 必须是函数体第一条语句才生效
  const DateTime = luxon.DateTime

  GM_addStyle(`@import url('https://cdn.jsdelivr.net/npm/@simonwep/pickr@1.9.1/dist/themes/classic.min.css');`)
  // fix: 展开 CSS 嵌套规则(GM_addStyle 不支持 CSS Nesting 语法)
  // fix: 合并两处重复的 .row-box main input 规则
  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-popup.swal2-modal.swal2-show #swal2-title a {
      display: inline-block;
      height: 40px;
      margin-right: 10px;
      border-radius: 10px;
      overflow: hidden;
      color: #fff;
    }
    .swal2-popup.swal2-modal.swal2-show #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 {
      display: flex;
      align-items: center;
    }
    .row-box main input {
      width: 70px;
      border: unset;
      box-shadow: unset;
      text-align: right;
      margin: 0;
      background: rgba(15, 172, 83, 1);
    }
  `)

  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">文档ℹ️</a></p>
  `

  // fix: const(从未重新赋值)
  const default_THEME = {
    BGC: {
      highlightColor: 'rgba(15, 172, 83, 1)',
      greyColor: 'rgba(245, 245, 245, 0.24)',
      isEnabled: true,
    },
    TIME_BOUNDARY: {
      number: 30,
      select: 'day',
    },
    FONT: {
      highlightColor: 'rgba(252, 252, 252, 1)',
      greyColor: 'rgba(0, 0, 0, 1)',
      isEnabled: true,
    },
    DIR: {
      highlightColor: 'rgba(15, 172, 83, 1)',
      greyColor: 'rgba(154, 154, 154, 1)',
      isEnabled: true,
    },
    SORT: {
      select: 'desc',
      isEnabled: true,
    },
    AWESOME: {
      isEnabled: false,
    },
    TIME_FORMAT: {
      isEnabled: true,
    },
  }

  let CURRENT_THEME = GM_getValue('CURRENT_THEME', 'light')
  let AWESOME_TOKEN = GM_getValue('AWESOME_TOKEN', '')
  const THEME_TYPE = getThemeType()
  const config_JSON = JSON.parse(
    GM_getValue('config_JSON', JSON.stringify({ light: default_THEME }))
  )
  // fix: 首次使用深色主题时 config_JSON['dark'] 为 undefined,加 fallback 防崩溃
  let THEME = config_JSON[THEME_TYPE] || JSON.parse(JSON.stringify(default_THEME))

  const configPickr = {
    theme: 'monolith',
    components: {
      preview: true,
      opacity: true,
      hue: true,
      interaction: {
        rgba: true,
        input: true,
        clear: true,
        save: true,
      },
    },
  }

  // fix: 移除副作用 console.log(每次调用都打印),日志统一在脚本初始化时打印一次
  function getThemeType() {
    if (CURRENT_THEME === 'auto') {
      return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
    }
    return CURRENT_THEME
  }

  function initPickr(el_default) {
    const pickr = Pickr.create({ ...configPickr, ...el_default })
    watchPickr(pickr)
  }

  // fix: 移除未使用的参数 el;回调中未使用的 color/instance 也一并移除
  function watchPickr(pickr) {
    pickr.on('save', () => {
      pickr.hide()
    })
  }

  // fix: 返回 updated_THEME,让 SweetAlert2 知道验证通过并拿到结果值
  // fix: 将成功通知移至 .then(),避免在 preConfirm 内嵌套 Swal.fire 造成双弹窗
  const preConfirm = () => {
    const updated_THEME = getUpdatedThemeConfig()
    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)
    return updated_THEME
  }

  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)
    handleData(theme)
  }

  // fix: 移除函数签名中的多余参数(原调用传入 default_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':
            val = $(`#${themeKey}-number`).val()
            break
          case 'select':
            val = $(`#${themeKey}-select`).val()
            break
          default:
            break
        }
        updatedTheme[themeKey][key] = val
      }
    }
    return updatedTheme
  }

  // fix: 拼写 handel → handle(贯穿全文)
  function handleData(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() {
    // fix: 成功通知改到 .then(),不在 preConfirm 内嵌套 Swal
    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: '保存设置',
    }).then((result) => {
      if (result.isConfirmed) {
        Swal.fire({
          position: 'top-center',
          background: '#4ab96f',
          icon: 'success',
          title: '设置已保存',
          showConfirmButton: false,
          timer: 800,
        })
      }
    })

    initSettings(THEME)

    $('#THEME-select').on('change', function () {
      const selectedTheme = $(this).val()
      // fix: 选择未保存过的主题时 config_JSON[selectedTheme] 为 undefined,加 fallback
      const theme = config_JSON[selectedTheme] || JSON.parse(JSON.stringify(default_THEME))
      handleData(theme)
    })
  }

  function setElementBGC(el, BGC, timeResult) {
    if (el.length && BGC.isEnabled) {
      el[0].style.setProperty(
        'background-color',
        timeResult ? BGC.highlightColor : BGC.greyColor,
        'important'
      )
    }
  }

  function setElementDIR(el, DIR, timeResult) {
    if (el.length && DIR.isEnabled) {
      el.attr('fill', timeResult ? DIR.highlightColor : DIR.greyColor)
    }
  }

  function setElementTIME_FORMAT(el, TIME_FORMAT, datetime) {
    if (TIME_FORMAT.isEnabled && el.css('display') !== 'none') {
      el.css('display', 'none')
      // fix: 加专属 class,避免 disable 时误删 GitHub 原有 span 元素
      el.before(`<span class="gf-formatted-date">${formatDate(datetime)}</span>`)
    } else if (TIME_FORMAT.isEnabled === false) {
      el.parent().find('span.gf-formatted-date').remove()
      el.css('display', 'block')
    }
  }

  function setElementFONT(el, FONT, timeResult) {
    if (FONT.isEnabled) {
      el.css('color', timeResult ? FONT.highlightColor : FONT.greyColor)
    }
  }

  // fix: 移除 type 参数和 UTC 分支(搜索页改用 ISO datetime 后该分支成为死代码)
  function handleTime(time, time_boundary) {
    const { number, select } = time_boundary
    let days = 0
    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
    }
    const targetDate = new Date()
    targetDate.setDate(targetDate.getDate() - days)
    return new Date(time) >= targetDate
  }

  const pattern = /^https:\/\/github\.com\/[^/]+\/[^/]+\/?$/
  function isValidHref(href) {
    return pattern.test(href)
  }

  function toAPIUrl(href) {
    const match = href.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)/)
    return match ? `https://api.github.com/repos/${match[1]}/${match[2]}` : null
  }

  function GitHub_FreshnessSearchPage(theme = THEME) {
    const elements = $('relative-time[datetime]')
    if (elements.length === 0) return
    elements.each(function () {
      const datetime = $(this).attr('datetime')
      if (!datetime) return
      const timeResult = handleTime(datetime, theme.TIME_BOUNDARY)
      const BGC_element = $(this).closest('li, article, [data-testid*="result"], [class*="search-result"]')
      setElementBGC(BGC_element, theme.BGC, timeResult)
      setElementFONT($(this), theme.FONT, timeResult)
      if (theme.TIME_FORMAT.isEnabled) {
        $(this).text(formatDate(datetime))
      }
    })
  }

  function GitHub_FreshnessAwesome(theme = THEME) {
    const elementsToObserve = []
    // Awesome README 中的链接,使用语义选择器
    $('#readme a, .markdown-body a').each(function () {
      const href = $(this).attr('href')
      if (isValidHref(href)) elementsToObserve.push(this)
    })

    const observer = new IntersectionObserver((entries) => {
      entries.forEach(el => {
        const href = $(el.target).attr('href')
        const apiHref = toAPIUrl(href)
        if (!el.isIntersecting || el.target.getAttribute('request') === 'true' || !apiHref) return
        $.ajax({
          url: apiHref,
          method: 'GET',
          headers: {
            'Authorization': AWESOME_TOKEN ? `Bearer ${AWESOME_TOKEN}` : undefined,
          },
          success(data) {
            const stars = data.stargazers_count
            const time = data.updated_at
            const timeResult = handleTime(time, theme.TIME_BOUNDARY)
            if (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, timeResult)
            setElementFONT($(el.target), theme.FONT, timeResult)
            $(el.target).css('padding', '0 12px')
          },
          error(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'),
              })
            }
          },
        })
      })
    }, { threshold: 0.5 })

    elementsToObserve.forEach(el => observer.observe(el))
  }

  function GitHub_Freshness(theme = THEME) {
    const matchUrl = isMatchedUrl()
    if (!matchUrl) return
    if (matchUrl === 'matchSearchPage') return GitHub_FreshnessSearchPage(theme)

    const elements = $('relative-time[datetime]').filter(function () {
      return $(this).closest('tr').length > 0
    })
    if (elements.length === 0) return

    const trRows = []
    elements.each(function () {
      const datetime = $(this).attr('datetime')
      if (!datetime) return
      const timeResult = handleTime(datetime, theme.TIME_BOUNDARY)
      const trElement = $(this).closest('tr[class*="react-directory"], tr')
      if (!trElement.length || trRows.includes(trElement[0])) return
      trRows.push(trElement[0])
      const BGC_element = $(this).closest('td')
      const DIR_element = trElement.find('.icon-directory')
      const FILE_element = trElement.find('.color-fg-muted')
      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 && trRows.length > 0) {
      trRows.sort((a, b) => {
        const rtA = a.querySelector('relative-time')
        const rtB = b.querySelector('relative-time')
        if (!rtA || !rtB) return 0
        const dateA = new Date(rtA.getAttribute('datetime'))
        const dateB = new Date(rtB.getAttribute('datetime'))
        return theme.SORT.select === 'asc' ? dateA - dateB : dateB - dateA
      })
      $(trRows[0]).closest('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 url = window.location.href
    if (/^https:\/\/github\.com\/[^/]+\/[^/]+(?:\?.*)?$|^https:\/\/github\.com\/[^/]+\/[^/]+\/tree\/.+$/.test(url)) return 'matchRepoPage'
    if (/^https:\/\/github\.com\/search\?.*$/.test(url)) return 'matchSearchPage'
    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()) GitHub_Freshness()
  }, 350)

  // fix: 移除各事件回调中的 console.log
  window.addEventListener('load', runScript)
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'visible') runScript()
  })
  document.addEventListener('pjax:end', runScript)

  ;(function (history) {
    const pushState = history.pushState
    const replaceState = history.replaceState
    history.pushState = function () {
      pushState.apply(history, arguments)
      setTimeout(runScript, 350)
    }
    history.replaceState = function () {
      replaceState.apply(history, arguments)
      setTimeout(runScript, 350)
    }
    window.addEventListener('popstate', () => setTimeout(runScript, 500))
  })(window.history)

  const _domObserver = new MutationObserver(runScript)
  _domObserver.observe(document.body, { childList: true, subtree: true })

  GM_registerMenuCommand('⚙️ 设置面板', createSettingsPanel)

  // fix: 广告日志只打印一次(原来在 getThemeType 中,每次调用都打印)
  window.console.log(
    '%c✅向前:如果您觉得GitHub-Freshness好用,点击下方 github链接 给个 star 吧。非常感谢你!!!\n[https://github.com/rational-stars/GitHub-Freshness]',
    'color:green'
  )

  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
    THEME = config_JSON[e.matches ? 'dark' : 'light'] || JSON.parse(JSON.stringify(default_THEME))
    GitHub_Freshness(THEME)
  })
})()

 

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情图片快捷回复

    暂无评论内容