Greasy Fork is available in English.
GitHub镜像,加速访问GitHub,支持Clone、Release、Raw、Zip加速。
// ==UserScript== // @name GitHub镜像 // @name:en GitHub Mirror // @description GitHub镜像,加速访问GitHub,支持Clone、Release、Raw、Zip加速。 // @description:en GitHub mirror. Accelerate access to GitHub. Support Clone, Release, RAW and ZIP acceleration. // @namespace https://github.com/HaleShaw // @version 1.4.0 // @author HaleShaw // @copyright 2021+, HaleShaw (https://github.com/HaleShaw) // @license AGPL-3.0-or-later // @homepage https://github.com/HaleShaw/TM-GitHubMirror // @supportURL https://github.com/HaleShaw/TM-GitHubMirror/issues // @contributionURL https://www.jianwudao.com/ // @icon https://github.githubassets.com/favicon.ico // @require https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js // @include *://github.com/* // @compatible Chrome // @run-at document-end // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // ==/UserScript== // ==OpenUserJS== // @author HaleShaw // @collaborator HaleShaw // ==/OpenUserJS== (function () { ("use strict"); const style = ` /* The menu container */ .menuContainer { width: 600px; } .menuBlock { padding: 4px 0; color: #990000; } .menuLeftIcon{ margin-right:5px; } .menuButtonLabel{ margin-right: 2rem; } .menuButtonCheck{ vertical-align: text-bottom; margin: 0 3px; } .SelectMenu-list { padding: 0 16px; } .SelectMenu-list > a.SelectMenu-item { padding-left: 0; padding-right: 0; margin-top: 4px; } .clone { padding-left: 3px 12px !important; } .Box-body.download-box { border-bottom: none; width: 100%; text-align: right; padding: unset; } .Box-body.download-box > a { font-size: 11px; margin: 0 3px; padding: 0 6px; } `; const mirrors = [ { id: 0, name: "CnpmJS", url: "https://github.com.cnpmjs.org", description: "cnpmjs.org", }, { id: 1, name: "FastGit", url: "https://hub.fastgit.org", description: "KevinZonda", }, { id: 2, name: "FastGit", url: "https://download.fastgit.org", description: "KevinZonda", }, { id: 3, name: "FastGit", url: "https://raw.fastgit.org", description: "KevinZonda", }, { id: 4, name: "WuYanZheShui", url: "https://github.wuyanzheshui.workers.dev", description: "WuYanZheShui. Maximum of 100,000 calls per day", }, { id: 5, name: "RC1844", url: "https://github.rc1844.workers.dev", description: "RC1844. Maximum of 100,000 calls per day", }, { id: 6, name: "jsDelivr", url: "https://cdn.jsdelivr.net/gh", description: "The total file size of the current branch of the project cannot exceed 50MB", }, { id: 7, name: "IAPK", url: "https://github.iapk.cc", description: "IAPK", }, { id: 8, name: "Ecalose", url: "https://gh.haval.gq", description: "Ecalose. Maximum of 100,000 calls per day", }, { id: 9, name: "IAPK", url: "https://iapk.cc/github?url=https://github.com", description: "IAPK", }, { id: 10, name: "Statically", url: "https://cdn.staticaly.com/gh", description: "Only images and source code files are supported, and the file size is limited to 30MB", }, { id: 11, name: "Github 原生", url: "ssh://[email protected]:443/", description: "Github 官方提供的 443 端口的 SSH,适用于限制访问 22 端口的网络环境", }, { id: 12, name: "FastGit", url: "[email protected]:", description: "FastGit ##", }, ]; //添加对应索引即可使用 const cloneSet = [0, 1, 4]; const sshSet = [11, 12]; const browseSet = [0, 1, 4, 5, 7, 8]; const downloadSet = [2, 4, 5, 8, 9]; const rawSet = [3, 4, 5, 6, 8, 9, 10]; const messages = { en: { menuButton: { name: "CloneMirror", title: "Open List", header: "Quickly clone and Mirror sites", block: "Please do not login in the mirror site. I will not be responsible for any loss caused by this.", }, }, zh: { menuButton: { name: "克隆与镜像", title: "打开列表", header: "快速克隆与镜像站点", block: "请不要在镜像网站登录账号,若因此造成任何损失本人概不负责", }, }, }; const icons = { closeIcon: ` <svg aria-label="Close menu" class="octicon octicon-x" width="16" height="16" role="img"> <path fill-rule="evenodd" d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"></path> </svg>`, copyIcon: ` <svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-copy js-clipboard-copy-icon d-inline-block"> <path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path> </svg>`, copiedIcon: ` <svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-check js-clipboard-check-icon color-fg-success d-inline-block d-sm-none"> <path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path> </svg>`, }; const clonePrefix = "git clone "; const depthPrefix = "--depth=1 "; let message; let settingHtml; main(); $(document).on("pjax:success", function () { $("#mirror-menu").remove(); main(); }); function main() { GM_addStyle(style); logInfo(GM_info.script.name, GM_info.script.version); const prefix = getClonePrefix(); addMenu(prefix); setTimeout(() => { addHttpsClone(prefix); addSSHClone(prefix); addRawList(); }, 1000); if (isPC()) { addDownloadZip(); } // The Release page loads element dynamically. const callback = (mutationsList, observer) => { if (location.pathname.indexOf("/releases") === -1) return; for (const mutation of mutationsList) { for (const target of mutation.addedNodes) { if (target.nodeType !== 1) return; if ( target.tagName === "DIV" && target.dataset.viewComponent === "true" && target.classList[0] === "Box" ) addReleasesList(); } } }; const observer = new MutationObserver(callback); observer.observe(document, { childList: true, subtree: true }); } /** * Initialize setting. */ function initSetting() { let lang = GM_getValue("lang"); let clone = GM_getValue("clone"); let depth = GM_getValue("depth"); if (lang == undefined) { GM_setValue("lang", "zh"); } if (clone == undefined) { GM_setValue("clone", true); } if (depth == undefined) { GM_setValue("depth", true); } } function addMenu(prefix) { initSetting(); message = getMessage(true, true); settingHtml = getSettingHtml(); let menuButtonHtml = getMenuButtonPrefix() + getCloneList(prefix) + getBrowseList() + getMenuButtonSuffix(); $("div.d-flex.flex-wrap.flex-items-center.wb-break-word.f3.text-normal").append(menuButtonHtml); } function getMenuButtonPrefix() { return ` <details class="details-reset details-overlay mr-0 mb-0" id="mirror-menu"> <summary class="btn ml-2 btn-primary" id="menuButtonTitle" data-hotkey="m" title="${message.menuButton.title}" aria-haspopup="menu" role="button"> <span class="css-truncate-target" id="menuButtonName" data-menu-button="">${message.menuButton.name}</span> <span class="dropdown-caret"></span> </summary> <details-menu class="SelectMenu SelectMenu--hasFilter" role="menu"> <div class="SelectMenu-modal menuContainer"> <header class="SelectMenu-header"> <span class="SelectMenu-title" id="menuButtonHeader">${message.menuButton.header}</span> ${settingHtml} <button class="SelectMenu-closeButton" type="button" data-toggle-for="mirror-menu"> ${icons.closeIcon} </button> </header> <tab-container class="d-flex flex-column js-branches-tags-tabs" style="min-height: 0;"> <div role="tabpanel" class="d-flex flex-column flex-auto" tabindex="0"> <div class="btn-block flash-error menuBlock" id="menuButtonBlock" role="alert"> ${message.menuButton.block} </div> <div class="SelectMenu-list" data-filter-list="">`; } function getSettingHtml() { const clone = GM_getValue("clone"); const depth = GM_getValue("depth"); const lang = GM_getValue("lang"); const cloneStatus = clone && clone != "undefined" ? " checked" : ""; const depthStatus = depth && depth != "undefined" ? " checked" : ""; const langStatus = lang == "en" ? " checked" : ""; return ` <label class="menuButtonLabel"><input id="menuButtonClone" class="menuButtonCheck" type="checkbox"${cloneStatus}>Clone</input></label> <label class="menuButtonLabel"><input id="menuButtonDepth" class="menuButtonCheck" type="checkbox"${depthStatus}>Depth</input></label> <label class="menuButtonLabel"><input id="menuButtonLang" class="menuButtonCheck" type="checkbox"${langStatus}>English</input></label> `; } /** * Clone Checkbox event. */ $("#menuButtonClone").change(function () { const status = $("#menuButtonClone").is(":checked"); GM_setValue("clone", status); if (!status) { document.getElementById("menuButtonDepth").checked = false; depthChanged(status); } cloneChanged(status); }); /** * Depth Checkbox event. */ $("#menuButtonDepth").change(function () { const status = $("#menuButtonDepth").is(":checked"); depthChanged(status); console.log(status); let cloneStatus = $("#menuButtonClone").is(":checked"); if (status && !cloneStatus) { cloneStatus = true; document.getElementById("menuButtonClone").checked = true; GM_setValue("clone", cloneStatus); cloneChanged(cloneStatus); } }); /** * Language Checkbox event. */ $("#menuButtonLang").change(function () { const status = $("#menuButtonLang").is(":checked"); const value = status ? "en" : "zh"; GM_setValue("lang", value); message = getMessage(); updateMessage(); }); function cloneChanged(status) { const inputs = $("input.clone"); for (let i = 0; i < inputs.length; i++) { let value = inputs[i].value; if (status) { value = clonePrefix + value; } else { value = value.replace(clonePrefix, ""); } inputs[i].value = value; $(inputs[i]).next().children().attr("value", value); } } function depthChanged(status) { GM_setValue("depth", status); const inputs = $(".form-control.input-monospace.input-sm.clone"); const index = clonePrefix.length; for (let i = 0; i < inputs.length; i++) { let value = inputs[i].value; if (status) { const length = value.length; if (value.startsWith(clonePrefix)) { value = value.slice(0, index) + depthPrefix + value.slice(index, length); } else { value = depthPrefix + value; } } else { value = value.replace(depthPrefix, ""); } inputs[i].value = value; $(inputs[i]).next().children().attr("value", value); } } /** * Update message by target language. */ function updateMessage() { $("#menuButtonTitle").attr("title", message.menuButton.title); $("#menuButtonName").html(message.menuButton.name); $("#menuButtonHeader").html(message.menuButton.header); $("#menuButtonBlock").html(message.menuButton.block); } function addHttpsClone(prefix) { let httpsGroup = document.querySelector('[role="tabpanel"]:nth-child(2) div.input-group'); if (!httpsGroup) { return; } let inputs = httpsGroup.querySelectorAll("input.clone"); if (inputs.length > 0) { return; } updateDefaultClone(prefix, httpsGroup); httpsGroup.insertAdjacentHTML("afterend", getCloneList(prefix)); } /** * Get the clone list. */ function getCloneList(prefix) { const href = window.location.href.split("/"); const git = href[3] + "/" + href[4] + ".git"; let menuButtonHtml = ""; cloneSet.forEach(id => { menuButtonHtml += getCloneHtml(prefix + mirrors[id]["url"] + "/" + git, mirrors[id]["name"]); }); return menuButtonHtml; } function updateDefaultClone(prefix, parent) { let input = parent.querySelector("input"); gitStr = input.value; gitNew = prefix + gitStr; let button = parent.querySelector("clipboard-copy"); input.setAttribute("value", gitNew); input.setAttribute("aria-label", gitNew); input.className += " clone"; button.setAttribute("value", gitNew); } function getMenuButtonSuffix() { return `</div></div></tab-container></div></details-menu></details>`; } function addSSHClone(prefix) { let sshGroup = document.querySelector('[role="tabpanel"]:nth-child(3) div.input-group'); if (!sshGroup) { return; } let inputs = sshGroup.querySelectorAll("input.clone"); if (inputs.length > 0) { return; } let defaultSsh = sshGroup.firstElementChild; const sshStr = defaultSsh.value; let hrefSplit = sshStr.split(":"); let groupHtml = ""; if (hrefSplit[0] != "[email protected]") { return; } defaultSsh.value = prefix + sshStr; defaultSsh.setAttribute("aria-label", prefix + sshStr); defaultSsh.className += " clone"; let button = sshGroup.querySelector("clipboard-copy"); button.setAttribute("value", prefix + sshStr); sshSet.forEach(id => { groupHtml += getCloneHtml(prefix + mirrors[id]["url"] + hrefSplit[1], mirrors[id]["name"]); }); sshGroup.insertAdjacentHTML("afterend", groupHtml); } /** * Get the clone command prefix. */ function getClonePrefix() { let prefix = ""; let clone = GM_getValue("clone"); let depth = GM_getValue("depth"); if (clone) { prefix += "git clone "; } if (depth) { prefix += "--depth=1 "; } return prefix; } /** * Get the clone button html string. * @param {String} url url. * @param {tip} tip tip. */ function getCloneHtml(url, tip) { return ` <div class="input-group" style="margin-top: 4px;" title="${tip}"> <input type="text" class="clone form-control input-monospace input-sm color-bg-subtle" data-autoselect="" value="${url}" aria-label="${url}" readonly=""> <div class="input-group-button"> <clipboard-copy value="${url}" class="btn btn-sm js-clipboard-copy tooltipped-no-delay ClipboardButton" tabindex="0" role="button"> ${icons.copyIcon}${icons.copiedIcon} </clipboard-copy> </div> </div>`; } /** * Get the browse list. */ function getBrowseList() { let menuButtonHtml = ``; const href = window.location.href.split("/"); const path = window.location.pathname; browseSet.forEach(id => { menuButtonHtml += getBrowseHtml( mirrors[id]["url"] + path, mirrors[id]["name"], mirrors[id]["description"] ); }); if (href.length == 5 || path.includes("/tree/") || path.includes("/blob/")) { var html = mirrors[5]["url"] + path.replace("/tree/", "@").replace("/blob/", "@"); if (!path.includes("/blob/")) { html += "/"; } menuButtonHtml += getBrowseHtml(html, mirrors[5]["name"], mirrors[5]["description"]); } if (location.hostname != "github.com") { menuButtonHtml += getBrowseHtml(`https://github.com${path}`, "返回GitHub"); } return menuButtonHtml; } /** * Get browse html string. * @param {String} url url. * @param {String} name name. * @param {String} tip tip. * @returns */ function getBrowseHtml(url, name, tip = "") { return ` <a class="SelectMenu-item" href="${url}" target="_blank" title="${tip}" role="menuitemradio" aria-checked="false" rel="nofollow"> <span class="css-truncate css-truncate-overflow" style="width: 520px; overflow: hidden; word-break:keep-all; white-space:nowrap; text-overflow:ellipsis;">${url}</span> <span class="css-truncate css-truncate-overflow" style="width: 80px; text-align: right;">${name}</span> </a>`; } /** * Add Release list. */ function addReleasesList() { $(".Box--condensed") .find("[href]") .each(function () { if ($(this).parent().parent().find(".download-box").length == 0) { const href = $(this).attr("href"); $(this) .parent() .after(`<div class="Box-body download-box" >` + getReleaseDownloadHtml(href) + `</div>`); $(this).parent().removeClass("Box-body"); } }); } /** * Get Release download button html string. * @param {String} href href. * @returns html. */ function getReleaseDownloadHtml(href) { let html = ""; downloadSet.forEach(id => { html += `<a class="flex-1 btn btn-outline get-repo-btn" rel="nofollow" href="${mirrors[id]["url"] + href }" title="${mirrors[id]["description"]}">${mirrors[id]["name"]}</a>`; }); return html; } /** * Add download zip button. */ function addDownloadZip() { $("a[data-open-app='link']").each(function () { var li = $(`<li class="Box-row p-0"></li>`); const downloadHref = $(this).attr("href"); var aElement = $(this) .clone() .removeAttr("data-hydro-click data-hydro-click-hmac data-ga-click"); aElement.addClass("Box-row Box-row--hover-gray"); downloadSet.forEach(id => { let tempA = aElement.clone(); tempA.attr({ href: mirrors[id]["url"] + downloadHref, title: mirrors[id]["description"], }); tempA.html(tempA.html().replace("Download ZIP", `Download ZIP(${mirrors[id]["name"]})`)); li = li.clone().append(tempA); }); $(this).parent().after(li); }); } /** * Add Raw list. */ function addRawList() { let rawButton = $('#raw-url, a[data-testid="raw-button"]'); if (rawButton.length == 0) { return; } const href = rawButton.attr("href"); rawSet.forEach(id => { if (id == 3 || id == 10) { addRawButton(id, mirrors[id]["url"] + href.replace("/raw", ""), rawButton); } else if (id == 6) { addRawButton(id, mirrors[id]["url"] + href.replace("/raw/", "@"), rawButton); } else { addRawButton(id, mirrors[id]["url"] + href, rawButton); } }); } /** * Add the Raw Button. * @param {Number} id id of mirrors. * @param {String} url url. * @param {Object} rawButton the raw button. */ function addRawButton(id, url, rawButton) { var span = rawButton.clone().removeAttr("id"); span.attr({ href: url, title: mirrors[id]["description"], target: "_blank", }); span.text(mirrors[id]["name"]); rawButton.before(span); } /** * Get message by setting. */ function getMessage() { return "zh" == GM_getValue("lang") ? messages.zh : messages.en; } /** * Log the title and version at the front of the console. * @param {String} title title. * @param {String} version script version. */ function logInfo(title, version) { const titleStyle = "color:white;background-color:#606060"; const versionStyle = "color:white;background-color:#1475b2"; const logTitle = " " + title + " "; const logVersion = " " + version + " "; console.log("%c" + logTitle + "%c" + logVersion, titleStyle, versionStyle); } /** * Check if the visitor is PC. */ function isPC() { var userAgentInfo = navigator.userAgent; var agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"]; var isPC = true; const len = agents.length; for (var v = 0; v < len; v++) { if (userAgentInfo.indexOf(agents[v]) > 0) { isPC = false; break; } } return isPC; } })();