您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Shows total downloads for releases.
- // ==UserScript==
- // @name GitHub Release Downloads
- // @description Shows total downloads for releases.
- // @icon https://github.githubassets.com/favicons/favicon-dark.svg
- // @version 1.1
- // @author afkarxyz
- // @namespace https://github.com/afkarxyz/userscripts/
- // @supportURL https://github.com/afkarxyz/userscripts/issues
- // @license MIT
- // @match https://github.com/*
- // @grant GM_xmlhttpRequest
- // @connect api.codetabs.com
- // @connect api.cors.lol
- // @connect api.allorigins.win
- // @connect everyorigin.jwvbremen.nl
- // @connect api.github.com
- // @run-at document-start
- // ==/UserScript==
- ;(() => {
- const proxyServices = [
- {
- name: "Direct GitHub API",
- url: "https://api.github.com/repos/",
- parseResponse: (response) => {
- return JSON.parse(response)
- },
- },
- {
- name: "CodeTabs Proxy",
- url: "https://api.codetabs.com/v1/proxy/?quest=https://api.github.com/repos/",
- parseResponse: (response) => {
- return JSON.parse(response)
- },
- },
- {
- name: "CORS.lol Proxy",
- url: "https://api.cors.lol/?url=https://api.github.com/repos/",
- parseResponse: (response) => {
- return JSON.parse(response)
- },
- },
- {
- name: "AllOrigins Proxy",
- url: "https://api.allorigins.win/get?url=https://api.github.com/repos/",
- parseResponse: (response) => {
- const parsed = JSON.parse(response)
- return JSON.parse(parsed.contents)
- },
- },
- {
- name: "EveryOrigin Proxy",
- url: "https://everyorigin.jwvbremen.nl/api/get?url=https://api.github.com/repos/",
- parseResponse: (response) => {
- const parsed = JSON.parse(response)
- return JSON.parse(parsed.html)
- },
- },
- ]
- async function fetchFromApi(proxyService, owner, repo, tag) {
- const apiUrl = `${proxyService.url}${owner}/${repo}/releases/tags/${tag}`
- return new Promise((resolve) => {
- if (typeof GM_xmlhttpRequest === "undefined") {
- resolve({ success: false, error: "GM_xmlhttpRequest is not defined" })
- return
- }
- GM_xmlhttpRequest({
- method: "GET",
- url: apiUrl,
- headers: {
- Accept: "application/vnd.github.v3+json",
- },
- onload: (response) => {
- if (response.responseText.includes("limit") && response.responseText.includes("API")) {
- resolve({
- success: false,
- error: "Rate limit exceeded",
- isRateLimit: true,
- })
- return
- }
- if (response.status >= 200 && response.status < 300) {
- try {
- const releaseData = proxyService.parseResponse(response.responseText)
- resolve({ success: true, data: releaseData })
- } catch (e) {
- resolve({ success: false, error: "JSON parse error" })
- }
- } else {
- resolve({
- success: false,
- error: `Status ${response.status}`,
- })
- }
- },
- onerror: () => {
- resolve({ success: false, error: "Network error" })
- },
- ontimeout: () => {
- resolve({ success: false, error: "Timeout" })
- },
- })
- })
- }
- async function getReleaseData(owner, repo, tag) {
- for (let i = 0; i < proxyServices.length; i++) {
- const proxyService = proxyServices[i]
- const result = await fetchFromApi(proxyService, owner, repo, tag)
- if (result.success) {
- return result.data
- }
- }
- return null
- }
- function createDownloadCounter() {
- const getThemeColor = () => {
- const isDarkTheme = document.documentElement.getAttribute('data-color-mode') === 'dark' ||
- document.body.classList.contains('dark') ||
- window.matchMedia('(prefers-color-scheme: dark)').matches
- return isDarkTheme ? '#3fb950' : '#1a7f37'
- }
- const downloadCounter = document.createElement('span')
- downloadCounter.className = 'download-counter-simple'
- downloadCounter.style.cssText = `
- margin-left: 8px;
- color: ${getThemeColor()};
- font-size: 14px;
- font-weight: 400;
- display: inline;
- `
- const downloadIcon = `
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 384 512" fill="currentColor" style="margin-right: 2px; vertical-align: -2px;">
- <path d="M32 480c-17.7 0-32-14.3-32-32s14.3-32 32-32l320 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 480zM214.6 342.6c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 242.7 160 64c0-17.7 14.3-32 32-32s32 14.3 32 32l0 178.7 73.4-73.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3l-128 128z"/>
- </svg>
- `
- downloadCounter.innerHTML = `${downloadIcon}Loading...`
- return downloadCounter
- }
- function getCachedDownloads(owner, repo, tag) {
- const key = `ghdl_${owner}_${repo}_${tag}`
- const cached = localStorage.getItem(key)
- return cached ? parseInt(cached, 10) : null
- }
- function setCachedDownloads(owner, repo, tag, count) {
- const key = `ghdl_${owner}_${repo}_${tag}`
- if (localStorage.getItem(key) === null) {
- localStorage.setItem(key, count)
- }
- }
- function updateDownloadCounter(counter, totalDownloads, diff) {
- const formatNumber = (num) => {
- return num.toLocaleString('en-US')
- }
- const isDarkTheme = document.documentElement.getAttribute('data-color-mode') === 'dark' ||
- document.body.classList.contains('dark') ||
- window.matchMedia('(prefers-color-scheme: dark)').matches
- const diffColor = isDarkTheme ? '#888' : '#1f2328'
- const downloadIcon = `
- <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 384 512" fill="currentColor" style="margin-right: 2px; vertical-align: -2px;">
- <path d="M32 480c-17.7 0-32-14.3-32-32s14.3-32 32-32l320 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 480zM214.6 342.6c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 242.7 160 64c0-17.7 14.3-32 32-32s32 14.3 32 32l0 178.7 73.4-73.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3l-128 128z"/>
- </svg>
- `
- let diffText = ''
- if (typeof diff === 'number' && diff > 0) {
- diffText = ` <span class="download-diff" style="color:${diffColor};font-size:12px;">(+${formatNumber(diff)})</span>`
- }
- counter.innerHTML = `${downloadIcon}${formatNumber(totalDownloads)}${diffText}`
- counter.style.fontWeight = '600'
- }
- function setupThemeObserver(counter) {
- const getThemeColor = () => {
- const isDarkTheme = document.documentElement.getAttribute('data-color-mode') === 'dark' ||
- document.body.classList.contains('dark') ||
- window.matchMedia('(prefers-color-scheme: dark)').matches
- return isDarkTheme ? '#3fb950' : '#1a7f37'
- }
- const getDiffColor = () => {
- const isDarkTheme = document.documentElement.getAttribute('data-color-mode') === 'dark' ||
- document.body.classList.contains('dark') ||
- window.matchMedia('(prefers-color-scheme: dark)').matches
- return isDarkTheme ? '#888' : '#1f2328'
- }
- const updateCounterColor = () => {
- if (counter) {
- counter.style.color = getThemeColor()
- const diffSpan = counter.querySelector('.download-diff')
- if (diffSpan) {
- diffSpan.style.color = getDiffColor()
- }
- }
- }
- const observer = new MutationObserver((mutations) => {
- mutations.forEach((mutation) => {
- if (mutation.type === 'attributes' &&
- (mutation.attributeName === 'data-color-mode' ||
- mutation.attributeName === 'class')) {
- updateCounterColor()
- }
- })
- })
- observer.observe(document.documentElement, {
- attributes: true,
- attributeFilter: ['data-color-mode', 'class']
- })
- observer.observe(document.body, {
- attributes: true,
- attributeFilter: ['class']
- })
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
- mediaQuery.addEventListener('change', updateCounterColor)
- }
- async function addDownloadCounter() {
- if (isProcessing) {
- return
- }
- isProcessing = true
- const currentUrl = window.location.href
- const urlMatch = currentUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/releases\/tag\/([^\/\?]+)/)
- if (!urlMatch) {
- isProcessing = false
- return
- }
- const [, owner, repo, tag] = urlMatch
- const existingCounter = document.querySelector('.download-counter-simple')
- if (existingCounter) {
- isProcessing = false
- return
- }
- let attempts = 0
- const maxAttempts = 50
- const waitForBreadcrumb = () => {
- return new Promise((resolve) => {
- const checkBreadcrumb = () => {
- const selectedBreadcrumb = document.querySelector('.breadcrumb-item-selected a')
- if (selectedBreadcrumb) {
- resolve(selectedBreadcrumb)
- return
- }
- attempts++
- if (attempts < maxAttempts) {
- setTimeout(checkBreadcrumb, 100)
- } else {
- resolve(null)
- }
- }
- checkBreadcrumb()
- })
- }
- const selectedBreadcrumb = await waitForBreadcrumb()
- if (!selectedBreadcrumb) {
- isProcessing = false
- return
- }
- const downloadCounter = createDownloadCounter()
- selectedBreadcrumb.appendChild(downloadCounter)
- setupThemeObserver(downloadCounter)
- try {
- const releaseData = await getReleaseData(owner, repo, tag)
- if (!releaseData) {
- downloadCounter.remove()
- isProcessing = false
- return
- }
- const totalDownloads = releaseData.assets.reduce((total, asset) => {
- return total + asset.download_count
- }, 0)
- const cached = getCachedDownloads(owner, repo, tag)
- let diff = null
- if (cached !== null && totalDownloads > cached) {
- diff = totalDownloads - cached
- }
- updateDownloadCounter(downloadCounter, totalDownloads, diff)
- setCachedDownloads(owner, repo, tag, totalDownloads)
- } catch (error) {
- downloadCounter.remove()
- } finally {
- isProcessing = false
- }
- }
- let navigationTimeout = null
- let lastUrl = window.location.href
- let isProcessing = false
- function handleNavigation() {
- const currentUrl = window.location.href
- if (navigationTimeout) {
- clearTimeout(navigationTimeout)
- }
- if (currentUrl === lastUrl && isProcessing) {
- return
- }
- lastUrl = currentUrl
- navigationTimeout = setTimeout(() => {
- const existingCounters = document.querySelectorAll('.download-counter-simple')
- existingCounters.forEach(counter => counter.remove())
- if (currentUrl.includes('/releases/tag/')) {
- addDownloadCounter()
- }
- }, 300)
- }
- function init() {
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', handleNavigation)
- } else {
- handleNavigation()
- }
- document.addEventListener('turbo:load', handleNavigation)
- document.addEventListener('turbo:render', handleNavigation)
- document.addEventListener('turbo:frame-load', handleNavigation)
- document.addEventListener('pjax:end', handleNavigation)
- document.addEventListener('pjax:success', handleNavigation)
- window.addEventListener('popstate', handleNavigation)
- const originalPushState = history.pushState
- const originalReplaceState = history.replaceState
- history.pushState = function(...args) {
- originalPushState.apply(history, args)
- setTimeout(handleNavigation, 100)
- }
- history.replaceState = function(...args) {
- originalReplaceState.apply(history, args)
- setTimeout(handleNavigation, 100)
- }
- }
- init()
- })()