// ==UserScript==
// @name GitHub Star History ⭐
// @name:zh GitHub 之星历史 ⭐
// @name:zh-CN GitHub 之星历史 ⭐
// @name:zh-HK GitHub 之星曆史 ⭐
// @name:zh-SG GitHub 之星历史 ⭐
// @name:zh-TW GitHub 之星曆史 ⭐
// @description Adds star history chart to sidebar of GitHub repos
// @description:zh 将明星历史图表添加到 GitHub 存储库的侧边栏
// @description:zh-CN 将明星历史图表添加到 GitHub 存储库的侧边栏
// @description:zh-HK 將明星曆史圖表添加到 GitHub 存儲庫的側邊欄
// @description:zh-SG 将明星历史图表添加到 GitHub 存储库的侧边栏
// @description:zh-TW 將明星曆史圖表添加到 GitHub 存儲庫的側邊欄
// @author Adam Lui
// @namespace https://github.com/adamlui
// @version 2024.11.14
// @license MIT
// @icon https://github.githubassets.com/favicons/favicon.png
// @compatible chrome
// @compatible firefox
// @compatible edge
// @compatible brave
// @match *://github.com/*
// @connect api.star-history.com
// @connect cdn.jsdelivr.net
// @connect update.greasyfork.org
// @require https://cdn.jsdelivr.net/npm/@kudoai/chatgpt.js@3.3.5/dist/chatgpt.min.js#sha256-rfC4kk8q0byrafp7X0Qf9vaa3JNvkHRwNnUt6uL2hUE=
// @grant GM_registerMenuCommand
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @homepageURL https://github.com/adamlui/github-star-history
// @supportURL https://github.com/adamlui/github-star-history/issues
// ==/UserScript==
(async () => {
// Init ENV vars
const env = { scriptManager: (() => { try { return GM_info.scriptHandler } catch (err) { return 'unknown' } })() },
xhr = env.scriptManager == 'OrangeMonkey' ? GM_xmlhttpRequest : GM.xmlHttpRequest
// Init APP info
const app = { latestAssetCommitHash: '8405505' },
assetHostURL = `https://cdn.jsdelivr.net/gh/adamlui/github-star-history@${app.latestAssetCommitHash}`
Object.assign(app, await new Promise(resolve => xhr({
method: 'GET', url: `${assetHostURL}/app.json`,
onload: resp => resolve(JSON.parse(resp.responseText))
})))
app.urls.update = app.urls.greasyFork.replace('https://', 'https://update.')
.replace(/(\d+)-?([a-z-]*)$/i, (_, id, name) => `${id}/${name || 'script'}.meta.js`)
// Define SCRIPT functions
function safeWinOpen(url) { open(url, '_blank', 'noopener') } // to prevent backdoor vulnerabilities
function updateCheck() {
// Fetch latest meta
const currentVer = GM_info.script.version
xhr({
method: 'GET', url: app.urls.update + '?t=' + Date.now(),
headers: { 'Cache-Control': 'no-cache' },
onload: response => { const updateAlertWidth = 377
// Compare versions
const latestVer = /@version +(.*)/.exec(response.responseText)[1]
for (let i = 0 ; i < 4 ; i++) { // loop thru subver's
const currentSubVer = parseInt(currentVer.split('.')[i], 10) || 0,
latestSubVer = parseInt(latestVer.split('.')[i], 10) || 0
if (currentSubVer > latestSubVer) break // out of comparison since not outdated
else if (latestSubVer > currentSubVer) { // if outdated
// Alert to update
chatgpt.alert('🚀 Update available!', // title
`A newer version of ${app.name} `
+ `(v${latestVer}) is available! `
+ '<a target="_blank" rel="noopener" style="font-size: 0.7rem" '
+ 'href="' + app.urls.gitHub + '/commits/main/greasemonkey/'
+ app.urls.update.replace(/[^/]*\/([^/]*?)meta\.js/, '$1user.js') + '"'
+ '> View changes</a>',
function update() { // button
GM_openInTab(app.urls.update.replace('meta.js', 'user.js') + '?t=' + Date.now(),
{ active: true, insert: true } // focus, make adjacent
).onclose = () => location.reload()
},
'', updateAlertWidth // width
)
return
}}
// Alert to no update
chatgpt.alert('Up to date!', `${app.name} (v${currentVer}) is up to date!`)
}})}
function isDarkMode() {
return document.documentElement.dataset.colorMode == 'dark'
|| document.documentElement.dataset.darkreaderScheme == 'dark'
|| window.matchMedia?.('(prefers-color-scheme: dark)')?.matches
}
// Define CHART functions
function sanitizeImgURL(url) {
if (!url.startsWith('https://api.star-history.com/svg'))
throw new Error('>> Invalid URL')
return url
}
function getUserAndRepoOfCurrentPage() {
const reGitHubURL = /github\.com\/(?<user>[\w-]+)\/(?<repo>[\w.-]+)\/?/,
currentURL = location.href,
groups = reGitHubURL.exec(currentURL)?.groups
if (!groups?.user || !groups?.repo)
throw new Error(`Invalid Github repository URL: ${currentURL}`)
return groups
}
async function insertStarHistory() {
const { user, repo } = getUserAndRepoOfCurrentPage()
try { // to load/insert star history chart
// Fetch image as blob
const imgURL = sanitizeImgURL('https://api.star-history.com/svg?repos='
+ `${user}/${repo}&type=Date` + (isDarkMode() ? '&theme=dark' : ''))
const response = await new Promise((resolve, reject) => xhr({
method: 'GET', url: imgURL, responseType: 'blob', onload: resolve, onerror: reject
}))
if (response.status != 200) throw new Error('>> Failed to fetch image')
if (!document.querySelector('#star-history')) {
// Convert blob to data URL
const imgDataURL = await new Promise(resolve => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result)
reader.readAsDataURL(response.response)
})
// Create/size/pad star history img
const starHistoryImg = document.createElement('img')
starHistoryImg.style.width = '100%'; starHistoryImg.style.padding = '20px 0'
starHistoryImg.src = imgDataURL
// Create #star-history div, add attrs/img/listener
const starHistoryDiv = document.createElement('div')
starHistoryDiv.id = 'star-history'; starHistoryDiv.style.cursor = 'crosshair'
starHistoryDiv.append(starHistoryImg)
starHistoryDiv.onclick = () => zoomStarHistory(imgDataURL)
// Insert div
const aboutSection = document.querySelector('.about-margin > div')
aboutSection.insertAdjacentElement('afterend', starHistoryDiv)
//移动设备添加顶部按钮
insertBtn(imgDataURL)
}
} catch (err) { console.error('>> Error loading star history chart:', err) }
}
function insertBtn(imgDataURL) {
const el = document.querySelector('#responsive-meta-container .d-flex.gap-2.mt-n3.mb-3.flex-wrap')
if (!el) return
if (document.getElementById('zoomStarHistory')) return
const svgStr = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="16px" height="16px" viewBox="0 0 64 64" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
<g>
<path style="opacity:0.76" fill="#05ca00" d="M 63.5,33.5 C 63.5,34.5 63.5,35.5 63.5,36.5C 58.7514,40.2077 53.4181,42.8743 47.5,44.5C 45.9969,51.0438 43.9969,57.3771 41.5,63.5C 40.1667,63.5 38.8333,63.5 37.5,63.5C 34.1218,60.1239 31.1218,56.4572 28.5,52.5C 28.8966,51.475 29.5632,51.3083 30.5,52C 32.7089,53.8734 34.5422,56.04 36,58.5C 36.5,58 37,57.5 37.5,57C 33.0463,52.5508 29.3796,47.5508 26.5,42C 26.7432,41.0979 27.0766,40.2646 27.5,39.5C 28.7899,40.0577 29.6232,41.0577 30,42.5C 33.2391,40.648 36.7391,39.648 40.5,39.5C 40.6646,36.4816 40.498,33.4816 40,30.5C 37.1383,28.9296 34.3049,27.263 31.5,25.5C 29.8144,26.8569 28.1477,28.1902 26.5,29.5C 25.6236,29.6309 24.9569,29.2975 24.5,28.5C 29.8152,23.1463 29.1486,22.4797 22.5,26.5C 21.8933,26.3764 21.56,26.0431 21.5,25.5C 25.2992,23.6276 25.2992,21.9609 21.5,20.5C 27.6877,20.5267 33.1877,22.86 38,27.5C 38.7504,26.8742 39.5838,26.3742 40.5,26C 35.5908,22.8756 29.5908,19.8756 22.5,17C 21.1175,15.5391 21.4508,14.7058 23.5,14.5C 25.91,15.0629 28.2433,15.5629 30.5,16C 33.7853,11.0463 37.452,6.37966 41.5,2C 45.029,-0.379275 47.529,0.454059 49,4.5C 49.032,11.8717 49.1987,19.2051 49.5,26.5C 54.5427,28.2445 59.2094,30.5778 63.5,33.5 Z M 41.5,12.5 C 42.0967,12.7352 42.4301,13.2352 42.5,14C 41.8148,16.7952 41.3148,19.6285 41,22.5C 39.1839,21.6705 37.3506,20.8372 35.5,20C 37.3336,17.3367 39.3336,14.8367 41.5,12.5 Z M 47.5,33.5 C 51.2965,34.9114 51.2965,35.9114 47.5,36.5C 47.5,35.5 47.5,34.5 47.5,33.5 Z M 34.5,47.5 C 35.8333,47.5 37.1667,47.5 38.5,47.5C 37.5954,53.9632 36.2621,53.9632 34.5,47.5 Z"/>
</g>
<g>
<path style="opacity:0.71" fill="#05ca00" d="M 23.5,14.5 C 21.4508,14.7058 21.1175,15.5391 22.5,17C 29.5908,19.8756 35.5908,22.8756 40.5,26C 39.5838,26.3742 38.7504,26.8742 38,27.5C 33.1877,22.86 27.6877,20.5267 21.5,20.5C 19.2965,19.0538 16.9631,18.2204 14.5,18C 15.8554,21.2118 17.3554,24.3785 19,27.5C 19.7083,26.6195 20.5416,25.9528 21.5,25.5C 21.56,26.0431 21.8933,26.3764 22.5,26.5C 29.1486,22.4797 29.8152,23.1463 24.5,28.5C 24.9569,29.2975 25.6236,29.6309 26.5,29.5C 25.1999,30.8267 23.8665,32.16 22.5,33.5C 24.1897,35.5193 25.8564,37.5193 27.5,39.5C 27.0766,40.2646 26.7432,41.0979 26.5,42C 29.3796,47.5508 33.0463,52.5508 37.5,57C 37,57.5 36.5,58 36,58.5C 34.5422,56.04 32.7089,53.8734 30.5,52C 29.5632,51.3083 28.8966,51.475 28.5,52.5C 27.7387,51.6091 26.7387,50.9424 25.5,50.5C 18.553,51.9205 11.553,52.4205 4.5,52C 3.19551,51.196 2.52884,50.0293 2.5,48.5C 6.30907,43.5097 9.97574,38.3431 13.5,33C 11.5293,28.892 9.69592,24.7253 8,20.5C 7.33333,16.8333 7.33333,13.1667 8,9.5C 8.73869,8.42575 9.73869,7.75908 11,7.5C 15.8157,8.84699 19.9824,11.1803 23.5,14.5 Z M 21.5,20.5 C 25.2992,21.9609 25.2992,23.6276 21.5,25.5C 20.5416,25.9528 19.7083,26.6195 19,27.5C 17.3554,24.3785 15.8554,21.2118 14.5,18C 16.9631,18.2204 19.2965,19.0538 21.5,20.5 Z M 25,42 C 25.8297,40.5295 26.9956,38.2066 28.5,36C 27.5056,34.7603 26.0778,33.6204 25,32.5C 23.4918,34.4059 21.4906,36.6554 20,38.5C 18.9282,37.2717 18.25,35.8398 18,34.5C 17.25,35.75 16.1667,37.1667 15,38C 14.5,38.5 14.5,39.5 15,40C 16.9993,40.6089 19.1075,39.5245 21,38C 21.6706,39.5368 22.0079,41.0933 22.5,42.5C 21.4079,43.9192 21.5,45.5684 22.5,47C 22.5,46 22.5,45 23,44C 23.5295,43.3543 24.5,43 25,42 Z"/>
</g>
</svg>
`
const title = 'Star History'
const buttonHtml = `<button id="zoomStarHistory" data-show-dialog-id="repo-delete-menu-dialog" type="button"
data-view-component="true"
class="btn btn-sm tooltipped tooltipped-s">
<span class="Button-content">
<span class="Button-label tooltipped tooltipped-s" aria-label="${title}">${svgStr}</span>
</span>
</button>`
el.insertAdjacentHTML('afterbegin', buttonHtml)
const button = document.getElementById('zoomStarHistory')
button.addEventListener('click', () => zoomStarHistory(imgDataURL))
}
function zoomStarHistory(imgURL) {
const { user, repo } = getUserAndRepoOfCurrentPage()
// Create/stylize overlay
const overlay = document.createElement('div')
overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'
overlay.style.width = '100%'; overlay.style.height = '100%'
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; overlay.style.display = 'flex'
overlay.style.alignItems = 'center'; overlay.style.justifyContent = 'center'
overlay.style.zIndex = '9999'
// Stylize zoomed img
const zoomedImg = new Image()
zoomedImg.title = 'View on star-history.com'; zoomedImg.src = imgURL
zoomedImg.style.maxWidth = '90%'; zoomedImg.style.maxHeight = '90%'
zoomedImg.style.cursor = 'pointer'
// Add listeners
zoomedImg.onclick = () => // view on star-history.com
safeWinOpen(`https://star-history.com/#${user}/${repo}&Date`)
overlay.onclick = () => document.body.removeChild(overlay)
// Append elements
overlay.append(zoomedImg); document.body.append(overlay)
}
// Run MAIN routine
// Register ABOUT menu command
GM_registerMenuCommand('💡 About ' + app.name, async () => {
// Show alert
const headingStyle = 'font-size: 1.15rem ; font-weight: bold',
pStyle = 'font-size: 1rem ; position: relative ; left: 3px',
pBrStyle = 'font-size: 1rem ; position: relative ; left: 9px ; bottom: 3px '
const aboutAlertID = chatgpt.alert(
app.name, // title
`<span style="${headingStyle}">🏷️ <i>Version</i>: </span>`
+ `<span style="${pStyle}">${GM_info.script.version}</span>\n`
+ `<span style="${headingStyle}">📜 <i>Source code</i>:</span>\n`
+ `<span style="${pBrStyle}"><a href="${app.urls.gitHub}" target="_blank" rel="nopener">`
+ app.urls.gitHub + '</a></span>',
[ // buttons
function checkForUpdates() { updateCheck() },
function leaveAReview() { safeWinOpen(app.urls.greasyFork + '/feedback#post-discussion') }
])
// Re-format buttons to include emojis + re-case + hide 'Dismiss'
for (const button of document.getElementById(aboutAlertID).querySelectorAll('button')) {
if (/updates/i.test(button.textContent))
button.textContent = '🚀 Check for Updates'
else if (/review/i.test(button.textContent))
button.textContent = '⭐ Leave a Review'
else if (/github/i.test(button.textContent))
button.textContent = '📜 GitHub Source'
else button.style.display = 'none' // hide Dismiss button
}
})
// Observe DOM for need to insert star history
let starHistoryAdded = false, prevURL = location.href
new MutationObserver(mutations => mutations.forEach(mutation => {
if (mutation.type == 'childList' && mutation.addedNodes.length) {
const onRepoPage = !!document.querySelector('meta[name="route-pattern"][content*="/:repository"]')
if (location.href != prevURL) { prevURL = location.href; starHistoryAdded = false }
if (onRepoPage && !starHistoryAdded) { insertStarHistory(); starHistoryAdded = true }
}
})).observe(document.documentElement, { childList: true, subtree: true })
})()