您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Load some stats for repo list, and display star counts for GitHub repositories. Please config github token first.
// ==UserScript== // @name Github repos stats // @namespace Violentmonkey Scripts // @description Load some stats for repo list, and display star counts for GitHub repositories. Please config github token first. // @thank https://github.com/sir-kokabi/github-sorter // @match https://github.com/* // @version 1.2 // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // // @require https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1 // @license MIT // ==/UserScript== // 设置缓存过期时间为 10天 : 1 小时(3600000 毫秒)*24*10 const CACHE_EXPIRATION = 3600000 * 24 * 10; function myFetch(url, options = {}) { return new Promise((resolve, reject) => { GM_xmlhttpRequest( { method: options.method || 'GET', url: url, headers: options.headers, onload: function (response) { resolve( { status: response.status, json: () => JSON.parse(response.responseText) }); }, onerror: reject }); }); } (async function main() { 'use strict'; // 注册菜单命令 GM_registerMenuCommand("Do Work: Query and Show Stats", showStats); const currentPageRepo = (function extractRepoFromGitHubUrl(url) { // 1. 检查 URL 是否是 GitHub 链接 if (!url.includes('github.com')) { return null; } // 2. 使用正则表达式提取 repo 名 const match = url.match(/github\.com\/([^/]+)\/([^/#?]+)/); if (match) { return match[1] +'/'+ match[2] } return null; })(location.href); function showStats() { const githubToken = GM_getValue("githubToken", ""); if (!githubToken) { console.warn("GitHub token not set. Please set it in the script settings."); return; } const url = window.location.href; const selector = getSelector(url); if (!selector) return; inject(selector, githubToken); } // end showStats const Tools = { // Promise.all 运行太多的 promises? 分批运行,第一批都成功,才运行下一批 // https://gist.github.com/scil/15d63220521808ba7839f423e4d8a784 runPromisesInBatches:async function(promises, batchSize = 50,startIndex = 0) { let results = []; let errorIndex=null; while (startIndex < promises.length) { const batch = promises.slice(startIndex, startIndex + batchSize); try { const batchResults = await Promise.all(batch); results.push(...batchResults); // console.log(batchResults) startIndex += batchSize; } catch (error) { errorIndex = startIndex console.error(`Error processing batch starting at index ${startIndex}:`, error); // 处理错误,例如记录错误信息或停止执行 break; // 停止执行,避免后续批次继续执行 } } return [results, errorIndex]; } , isGitHubRepo:function(url) { const githubRegex = /^https:\/\/github\.com\/[^/]+\/[^/#]+$/; return githubRegex.test(url); }, roundNumber:function(number) { if (number < 1000) return number; const suffixes = ['', 'k', 'M', 'B', 'T']; const suffixIndex = Math.floor(Math.log10(number) / 3); const scaledNumber = number / Math.pow(10, suffixIndex * 3); const formattedNumber = scaledNumber % 1 === 0 ? scaledNumber.toFixed(0) : scaledNumber.toFixed(1); return `${formattedNumber}${suffixes[suffixIndex]}`; }, // 缓存包装函数 getCachedValue:function(key, defaultValue, expirationTime) { const cachedData = GM_getValue(key); if (cachedData) { const { value, timestamp } = JSON.parse(cachedData); if (Date.now() - timestamp < expirationTime) { return value; } } return defaultValue; }, setCachedValue:function(key, value) { const data = JSON.stringify( { value: value, timestamp: Date.now() }); GM_setValue(key, data); }, } function getSelector(url) { const selectors = [ { pattern: /https?:\/\/github.com\/[^\/]+\/[^\/]+\/*$/, // selector: "#readme", selector: '.markdown-body', }, { pattern: /https?:\/\/github.com\/.*\/[Rr][Ee][Aa][Dd][Mm][Ee]\.md$/i, selector: "article", }, { pattern: /https?:\/\/github.com\/[^\/]+\/[^\/]+\/(issues|pull)\/\d+\/*$/, selector: ".comment-body", }, { pattern: /https?:\/\/github.com\/[^\/]+\/[^\/]+\/wiki\/*$/, selector: "#wiki-body", }, ]; const selector = selectors.find(( { pattern }) => pattern.test(url))?.selector; return selector; } async function inject(selector, githubToken) { const allLinks = document.querySelectorAll(`${selector} a`); const injectPromises = []; allLinks.forEach((link) => { if (Tools.isGitHubRepo(link.href) && !link.querySelector('strong#github-stars-14151312')) { injectPromises.push(injectStars(link, githubToken)); } }); // await Promise.all(injectPromises); const results = await Tools.runPromisesInBatches(injectPromises,10,0); if(results[1]) { console.warn('停止在了 ', results[1]) } const uls = Array.from(document.querySelectorAll(`${selector} ul`)).filter(ul => ul.querySelectorAll(':scope > li').length >= 2); if (!uls) return; for (const ul of uls) { sortLis(ul); } function sortLis(ul) { const lis = Array.from(ul.querySelectorAll(":scope > li")); lis.sort((a, b) => { const aStars = getHighestStars(a); const bStars = getHighestStars(b); return bStars - aStars; }); for (const li of lis) { ul.appendChild(li); } } function getHighestStars(liElement) { const clonedLiElement = liElement.cloneNode(true); const ulElements = clonedLiElement.querySelectorAll("ul"); for (const ulElement of ulElements) { ulElement.remove(); } const starsElements = clonedLiElement.querySelectorAll("strong#github-stars-14151312"); let highestStars = 0; for (const starsElement of starsElements) { const stars = parseInt(starsElement.getAttribute("stars")); if (stars > highestStars) { highestStars = stars; } } return highestStars; } async function injectStars(link, githubToken) { const stats = await getStars(link.href, githubToken) if (!stats) return; const strong = document.createElement("strong"); strong.id = "github-stars-14151312"; strong.setAttribute("stars", stats.stars); strong.style.color = "#fff"; strong.style.fontSize = "12px"; strong.innerText = `★ ${Tools.roundNumber(stats.stars)}`; strong.style.backgroundColor = "#093812"; strong.style.paddingRight = "5px"; strong.style.paddingLeft = "5px"; strong.style.textAlign = "center"; strong.style.paddingBottom = "1px"; strong.style.borderRadius = "5px"; strong.style.marginLeft = "5px"; link.appendChild(strong); } } function getStars(githubRepoURL, githubToken) { const repoName = githubRepoURL.match(/github\.com\/([^/]+\/[^/]+)/)[1]; const cacheKey = `github_stats_${currentPageRepo}_${repoName}`; // 尝试从缓存获取星标数 const statsC = Tools.getCachedValue(cacheKey, null, CACHE_EXPIRATION); if (statsC !== null) { return statsC; } return myFetch(`https://api.github.com/repos/${repoName}`, { headers: { Authorization: `Token ${githubToken}` }, }).then((response) => { const data = response.json(); const stats = {stars: data.stargazers_count, forks_count: data.forks_count, open_issues_count: data.open_issues_count, created_at: data.created_at, pushed_at: data.pushed_at, archived: data.archived , disabled: data.disabled , }; // 缓存星标数 Tools.setCachedValue(cacheKey, stats); return stats; }).catch((error) => { console.error(`query stats for ${repoName} `,error) });; } })(); // setGitHubToken (async function setGitHubToken() { 'use strict'; async function setGitHubToken() { const githubToken = GM_getValue("githubToken", ""); const token = prompt(githubToken || "Please enter your GitHub Personal Access Token:"); if (token) { // 验证 token myFetch(`https://api.github.com/user`, { headers: { Authorization: `Bearer ${token}` }, }).then((response) => { if (response.status !== 200) { console.warn("Invalid GitHub token. Please update it in the script settings."); return; } console.log('valid github token') GM_setValue("githubToken", token); alert("GitHub token has been set. Refresh the page to see the changes."); }).catch((error) => { alert(error) }); } } GM_registerMenuCommand("Config: Set GitHub Token and test it", setGitHubToken); })(); // printGithubStatsCache // clearGithubStatsCache (function githubStatsCache() { 'use strict'; GM_registerMenuCommand("Tool: Print Stats Cache", printGithubStatsCache); GM_registerMenuCommand("Tool: Delete Stats Cache", clearGithubStatsCache); function printGithubStatsCache(){ const keys = GM_listValues(); console.groupCollapsed('printGithubStatsCache') console.log('current cache number is ', keys.length) keys.forEach(key => { console.log(key,':',GM_getValue(key)) }); console.groupEnd('printGithubStatsCache') } function clearGithubStatsCache(){ let pre = prompt("输入缓存前缀,默认是 github_stats_ 包含本脚本创建的所有统计缓存。特定repo页面上生成的统计缓存,前缀格式是 github_stats_<owner>/<name> "); if(!pre) pre='github_stats_' const keys = GM_listValues(); let n = 0; console.groupCollapsed('printGithubStatsCache for '+pre) keys.forEach(key => { if(key.startsWith(pre)){ console.log(key,':',GM_getValue(key)); n++; } }); console.log('cache number is ', n) console.groupEnd('printGithubStatsCache for '+pre) const sure = prompt("相关缓存已打印。确认要删除吗?请输入1"); if(sure!=='1') return keys.forEach(key => { if(key.startsWith(pre)) GM_deleteValue(key); }); } })(); // testGithubApi 没用 token (function testGithubApi() { 'use strict'; const shortcut = 'c-g c-g'; // Register shortcut VM.shortcut.register(shortcut, testGithubApi); const name = '_Test: GithubApi without token'; // Register menu command const menuName = `${name} (${VM.shortcut.reprShortcut(shortcut)})`; GM_registerMenuCommand(menuName, testGithubApi); function testGithubApi(){ const repo = 'Zaid-Ajaj/Awesome' ; // 'vuejs/awesome-vue' const ur = ["https://api.github.com"]; ur.push("repos", repo) const url = ur.join("/") console.debug('testGithubApi ' + url) GM_xmlhttpRequest( { url, headers: { "Accept": "application/vnd.github.v3+json", }, onload: function (xhr) { console.debug(xhr.responseText); } }); } // end testGithubApi })();