NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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.3.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 // @downloadURL https://raw.githubusercontent.com/HaleShaw/TM-GitHubMirror/master/GitHubMirror.user.js // @updateURL https://raw.githubusercontent.com/HaleShaw/TM-GitHubMirror/master/GitHubMirror.user.js // @contributionURL https://www.jianwudao.com/ // @icon https://github.githubassets.com/favicon.ico // @require https://cdn.jsdelivr.net/npm/jquery/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; } .clone { padding-left: 0 !important; width: calc(100% - 21px) !important; } `; 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' } ]; //添加对应索引即可使用 const cloneSet = [0, 1, 4]; 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 class="octicon octicon-clippy" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"> <path fill-rule="evenodd" d="M5.75 1a.75.75 0 00-.75.75v3c0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75v-3a.75.75 0 00-.75-.75h-4.5zm.75 3V2.5h3V4h-3zm-2.874-.467a.75.75 0 00-.752-1.298A1.75 1.75 0 002 3.75v9.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 13.25v-9.5a1.75 1.75 0 00-.874-1.515.75.75 0 10-.752 1.298.25.25 0 01.126.217v9.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-9.5a.25.25 0 01.126-.217z"></path> </svg>`, commandIcon: ` <svg class="octicon octicon-terminal menuLeftIcon" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"> <path fill-rule="evenodd" d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0114.25 15H1.75A1.75 1.75 0 010 13.25V2.75zm1.75-.25a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25V2.75a.25.25 0 00-.25-.25H1.75zM7.25 8a.75.75 0 01-.22.53l-2.25 2.25a.75.75 0 11-1.06-1.06L5.44 8 3.72 6.28a.75.75 0 111.06-1.06l2.25 2.25c.141.14.22.331.22.53zm1.5 1.5a.75.75 0 000 1.5h3a.75.75 0 000-1.5h-3z"></path> </svg>`, linkIcon: ` <svg class="octicon octicon-link color-text-secondary menuLeftIcon" alt="custom" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"> <path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 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); initSetting(); message = getMessage(true, true); settingHtml = getSettingHtml(); let menuButtonHtml = getMenuButtonPrefix() + getCloneList() + getBrowseList() + getMenuButtonSuffix(); $('h1.flex-wrap.break-word.text-normal').append(menuButtonHtml); if (location.pathname.split('/')[3] == 'releases') { addReleasesList(); } if (isPC()) { addDownloadZip(); } addRawList(); } /** * 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 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="SelectMenu-list" data-filter-list=""> <div class="btn-block flash-error menuBlock" id="menuButtonBlock" role="alert"> ${message.menuButton.block} </div>`; } function getSettingHtml() { const clone = GM_getValue('clone'); const depth = GM_getValue('depth'); const lang = GM_getValue('lang'); const cloneStatus = clone ? ' checked' : ''; const depthStatus = depth ? ' 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); const inputs = $('.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); } }); /** * Depth Checkbox event. */ $('#menuButtonDepth').change(function () { const status = $('#menuButtonDepth').is(':checked'); 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); } }); /** * Language Checkbox event. */ $('#menuButtonLang').change(function () { const status = $('#menuButtonLang').is(':checked'); const value = status ? 'en' : 'zh'; GM_setValue('lang', value); message = getMessage(); updateMessage(); }); /** * 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); } /** * Get the clone list. */ function getCloneList() { const href = window.location.href.split('/'); const git = href[3] + '/' + href[4] + '.git'; let menuButtonHtml = ''; const prefix = getClonePrefix(); cloneSet.forEach(id => { menuButtonHtml += getCloneHtml( prefix + mirrors[id]['url'] + '/' + git, mirrors[id]['name'] ); }); return menuButtonHtml; } function getMenuButtonSuffix() { return `</div></div></tab-container></div></details-menu></details>`; } /** * 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="padding: 0 16px;" title="${tip}"> ${icons.commandIcon} <input type="text" class="form-control input-monospace input-sm clone" value="${url}" readonly="" data-autoselect=""> <div class="input-group-button"> <clipboard-copy value="${url}" class="btn btn-sm"> ${icons.copyIcon} </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"> ${icons.linkIcon} <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 () { const href = $(this).attr('href'); $(this) .parent() .after(`<div class="Box-body" >` + 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() { $('#raw-url').each(function () { var href = $(this).attr('href'); rawSet.forEach(id => { if (id == 3 || id == 10) { getRawHtml(id, mirrors[id]['url'] + href.replace('/raw', '')); } else if (id == 6) { getRawHtml(id, mirrors[id]['url'] + href.replace('/raw/', '@')); } else { getRawHtml(id, mirrors[id]['url'] + href); } }); }); } /** * * @param {Number} id id of mirrors. * @param {String} url url. */ function getRawHtml(id, url) { var span = $('#raw-url').clone().removeAttr('id'); span.attr({ href: url, title: mirrors[id]['description'], target: '_blank' }); span.text(mirrors[id]['name']); $('#raw-url').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; } })();