GitHub镜像

GitHub镜像,加速访问GitHub,支持Clone、Release、Raw、Zip加速。

目前为 2022-11-09 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub镜像
  3. // @name:en GitHub Mirror
  4. // @description GitHub镜像,加速访问GitHub,支持Clone、Release、Raw、Zip加速。
  5. // @description:en GitHub mirror. Accelerate access to GitHub. Support Clone, Release, RAW and ZIP acceleration.
  6. // @namespace https://github.com/HaleShaw
  7. // @version 1.3.1
  8. // @author HaleShaw
  9. // @copyright 2021+, HaleShaw (https://github.com/HaleShaw)
  10. // @license AGPL-3.0-or-later
  11. // @homepage https://github.com/HaleShaw/TM-GitHubMirror
  12. // @supportURL https://github.com/HaleShaw/TM-GitHubMirror/issues
  13. // @contributionURL https://www.jianwudao.com/
  14. // @icon https://github.githubassets.com/favicon.ico
  15. // @require https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js
  16. // @include *://github.com/*
  17. // @compatible Chrome
  18. // @run-at document-end
  19. // @grant GM_addStyle
  20. // @grant GM_getValue
  21. // @grant GM_setValue
  22. // ==/UserScript==
  23.  
  24. // ==OpenUserJS==
  25. // @author HaleShaw
  26. // @collaborator HaleShaw
  27. // ==/OpenUserJS==
  28.  
  29. (function () {
  30. ('use strict');
  31.  
  32. const style = `
  33. /* The menu container */
  34. .menuContainer {
  35. width: 600px;
  36. }
  37.  
  38. .menuBlock {
  39. padding: 4px 0;
  40. color: #990000;
  41. }
  42.  
  43. .menuLeftIcon{
  44. margin-right:5px;
  45. }
  46.  
  47. .menuButtonLabel{
  48. margin-right: 2rem;
  49. }
  50.  
  51. .menuButtonCheck{
  52. vertical-align: text-bottom;
  53. margin: 0 3px;
  54. }
  55.  
  56. .clone {
  57. padding-left: 0 !important;
  58. width: calc(100% - 21px) !important;
  59. }
  60. `;
  61.  
  62. const mirrors = [
  63. {
  64. id: 0,
  65. name: 'CnpmJS',
  66. url: 'https://github.com.cnpmjs.org',
  67. description: 'cnpmjs.org'
  68. },
  69. {
  70. id: 1,
  71. name: 'FastGit',
  72. url: 'https://hub.fastgit.org',
  73. description: 'KevinZonda'
  74. },
  75. {
  76. id: 2,
  77. name: 'FastGit',
  78. url: 'https://download.fastgit.org',
  79. description: 'KevinZonda'
  80. },
  81. {
  82. id: 3,
  83. name: 'FastGit',
  84. url: 'https://raw.fastgit.org',
  85. description: 'KevinZonda'
  86. },
  87. {
  88. id: 4,
  89. name: 'WuYanZheShui',
  90. url: 'https://github.wuyanzheshui.workers.dev',
  91. description: 'WuYanZheShui. Maximum of 100,000 calls per day'
  92. },
  93. {
  94. id: 5,
  95. name: 'RC1844',
  96. url: 'https://github.rc1844.workers.dev',
  97. description: 'RC1844. Maximum of 100,000 calls per day'
  98. },
  99. {
  100. id: 6,
  101. name: 'jsDelivr',
  102. url: 'https://cdn.jsdelivr.net/gh',
  103. description:
  104. 'The total file size of the current branch of the project cannot exceed 50MB'
  105. },
  106. {
  107. id: 7,
  108. name: 'IAPK',
  109. url: 'https://github.iapk.cc',
  110. description: 'IAPK'
  111. },
  112. {
  113. id: 8,
  114. name: 'Ecalose',
  115. url: 'https://gh.haval.gq',
  116. description: 'Ecalose. Maximum of 100,000 calls per day'
  117. },
  118. {
  119. id: 9,
  120. name: 'IAPK',
  121. url: 'https://iapk.cc/github?url=https://github.com',
  122. description: 'IAPK'
  123. },
  124. {
  125. id: 10,
  126. name: 'Statically',
  127. url: 'https://cdn.staticaly.com/gh',
  128. description:
  129. 'Only images and source code files are supported, and the file size is limited to 30MB'
  130. }
  131. ];
  132.  
  133. //添加对应索引即可使用
  134. const cloneSet = [0, 1, 4];
  135. const browseSet = [0, 1, 4, 5, 7, 8];
  136. const downloadSet = [2, 4, 5, 8, 9];
  137. const rawSet = [3, 4, 5, 6, 8, 9, 10];
  138.  
  139. const messages = {
  140. en: {
  141. menuButton: {
  142. name: 'CloneMirror',
  143. title: 'Open List',
  144. header: 'Quickly clone and Mirror sites',
  145. block:
  146. 'Please do not login in the mirror site. I will not be responsible for any loss caused by this.'
  147. }
  148. },
  149. zh: {
  150. menuButton: {
  151. name: '克隆与镜像',
  152. title: '打开列表',
  153. header: '快速克隆与镜像站点',
  154. block: '请不要在镜像网站登录账号,若因此造成任何损失本人概不负责'
  155. }
  156. }
  157. };
  158.  
  159. const icons = {
  160. closeIcon: `
  161. <svg aria-label="Close menu" class="octicon octicon-x" width="16" height="16" role="img">
  162. <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>
  163. </svg>`,
  164. copyIcon: `
  165. <svg class="octicon octicon-clippy" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true">
  166. <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>
  167. </svg>`,
  168. commandIcon: `
  169. <svg class="octicon octicon-terminal menuLeftIcon" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true">
  170. <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>
  171. </svg>`,
  172. linkIcon: `
  173. <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">
  174. <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>
  175. </svg>`
  176. };
  177.  
  178. const clonePrefix = 'git clone ';
  179. const depthPrefix = '--depth=1 ';
  180. let message;
  181. let settingHtml;
  182.  
  183. main();
  184. $(document).on('pjax:success', function () {
  185. $('#mirror-menu').remove();
  186. main();
  187. });
  188.  
  189. function main() {
  190. GM_addStyle(style);
  191. logInfo(GM_info.script.name, GM_info.script.version);
  192. initSetting();
  193. message = getMessage(true, true);
  194. settingHtml = getSettingHtml();
  195. let menuButtonHtml = getMenuButtonPrefix() + getCloneList() + getBrowseList() + getMenuButtonSuffix();
  196. $('div.d-flex.flex-wrap.flex-items-center.wb-break-word.f3.text-normal').append(menuButtonHtml);
  197. if (location.pathname.split('/')[3] == 'releases') {
  198. addReleasesList();
  199. }
  200. if (isPC()) {
  201. addDownloadZip();
  202. }
  203. addRawList();
  204. }
  205.  
  206. /**
  207. * Initialize setting.
  208. */
  209. function initSetting() {
  210. let lang = GM_getValue('lang');
  211. let clone = GM_getValue('clone');
  212. let depth = GM_getValue('depth');
  213. if (lang == undefined) {
  214. GM_setValue('lang', 'zh');
  215. }
  216. if (clone == undefined) {
  217. GM_setValue('clone', true);
  218. }
  219. if (depth == undefined) {
  220. GM_setValue('depth', true);
  221. }
  222. }
  223.  
  224. function getMenuButtonPrefix() {
  225. return `
  226. <details class="details-reset details-overlay mr-0 mb-0" id="mirror-menu">
  227. <summary class="btn ml-2 btn-primary" id="menuButtonTitle" data-hotkey="m" title="${message.menuButton.title}" aria-haspopup="menu" role="button">
  228. <span class="css-truncate-target" id="menuButtonName" data-menu-button="">${message.menuButton.name}</span>
  229. <span class="dropdown-caret"></span>
  230. </summary>
  231.  
  232. <details-menu class="SelectMenu SelectMenu--hasFilter" role="menu">
  233. <div class="SelectMenu-modal menuContainer">
  234. <header class="SelectMenu-header">
  235. <span class="SelectMenu-title" id="menuButtonHeader">${message.menuButton.header}</span>
  236. ${settingHtml}
  237. <button class="SelectMenu-closeButton" type="button" data-toggle-for="mirror-menu">
  238. ${icons.closeIcon}
  239. </button>
  240. </header>
  241. <tab-container class="d-flex flex-column js-branches-tags-tabs" style="min-height: 0;">
  242. <div role="tabpanel" class="d-flex flex-column flex-auto" tabindex="0">
  243. <div class="SelectMenu-list" data-filter-list="">
  244. <div class="btn-block flash-error menuBlock" id="menuButtonBlock" role="alert">
  245. ${message.menuButton.block}
  246. </div>`;
  247. }
  248.  
  249. function getSettingHtml() {
  250. const clone = GM_getValue('clone');
  251. const depth = GM_getValue('depth');
  252. const lang = GM_getValue('lang');
  253. const cloneStatus = clone ? ' checked' : '';
  254. const depthStatus = depth ? ' checked' : '';
  255. const langStatus = lang == 'en' ? ' checked' : '';
  256. return `
  257. <label class="menuButtonLabel"><input id="menuButtonClone" class="menuButtonCheck" type="checkbox"${cloneStatus}>Clone</input></label>
  258. <label class="menuButtonLabel"><input id="menuButtonDepth" class="menuButtonCheck" type="checkbox"${depthStatus}>Depth</input></label>
  259. <label class="menuButtonLabel"><input id="menuButtonLang" class="menuButtonCheck" type="checkbox"${langStatus}>English</input></label>
  260. `;
  261. }
  262.  
  263. /**
  264. * Clone Checkbox event.
  265. */
  266. $('#menuButtonClone').change(function () {
  267. const status = $('#menuButtonClone').is(':checked');
  268. GM_setValue('clone', status);
  269. const inputs = $('.clone');
  270. for (let i = 0; i < inputs.length; i++) {
  271. let value = inputs[i].value;
  272. if (status) {
  273. value = clonePrefix + value;
  274. } else {
  275. value = value.replace(clonePrefix, '');
  276. }
  277. inputs[i].value = value;
  278. $(inputs[i]).next().children().attr('value', value);
  279. }
  280. });
  281.  
  282. /**
  283. * Depth Checkbox event.
  284. */
  285. $('#menuButtonDepth').change(function () {
  286. const status = $('#menuButtonDepth').is(':checked');
  287. GM_setValue('depth', status);
  288. const inputs = $('.form-control.input-monospace.input-sm.clone');
  289. const index = clonePrefix.length;
  290. for (let i = 0; i < inputs.length; i++) {
  291. let value = inputs[i].value;
  292. if (status) {
  293. const length = value.length;
  294. if (value.startsWith(clonePrefix)) {
  295. value = value.slice(0, index) + depthPrefix + value.slice(index, length);
  296. } else {
  297. value = depthPrefix + value;
  298. }
  299. } else {
  300. value = value.replace(depthPrefix, '');
  301. }
  302. inputs[i].value = value;
  303. $(inputs[i]).next().children().attr('value', value);
  304. }
  305. });
  306.  
  307. /**
  308. * Language Checkbox event.
  309. */
  310. $('#menuButtonLang').change(function () {
  311. const status = $('#menuButtonLang').is(':checked');
  312. const value = status ? 'en' : 'zh';
  313. GM_setValue('lang', value);
  314. message = getMessage();
  315. updateMessage();
  316. });
  317.  
  318. /**
  319. * Update message by target language.
  320. */
  321. function updateMessage() {
  322. $('#menuButtonTitle').attr('title', message.menuButton.title);
  323. $('#menuButtonName').html(message.menuButton.name);
  324. $('#menuButtonHeader').html(message.menuButton.header);
  325. $('#menuButtonBlock').html(message.menuButton.block);
  326. }
  327.  
  328. /**
  329. * Get the clone list.
  330. */
  331. function getCloneList() {
  332. const href = window.location.href.split('/');
  333. const git = href[3] + '/' + href[4] + '.git';
  334. let menuButtonHtml = '';
  335. const prefix = getClonePrefix();
  336. cloneSet.forEach(id => {
  337. menuButtonHtml += getCloneHtml(
  338. prefix + mirrors[id]['url'] + '/' + git,
  339. mirrors[id]['name']
  340. );
  341. });
  342. return menuButtonHtml;
  343. }
  344.  
  345. function getMenuButtonSuffix() {
  346. return `</div></div></tab-container></div></details-menu></details>`;
  347. }
  348.  
  349. /**
  350. * Get the clone command prefix.
  351. */
  352. function getClonePrefix() {
  353. let prefix = '';
  354. let clone = GM_getValue('clone');
  355. let depth = GM_getValue('depth');
  356. if (clone) {
  357. prefix += 'git clone ';
  358. }
  359. if (depth) {
  360. prefix += '--depth=1 ';
  361. }
  362. return prefix;
  363. }
  364.  
  365. /**
  366. * Get the clone button html string.
  367. * @param {String} url url.
  368. * @param {tip} tip tip.
  369. */
  370. function getCloneHtml(url, tip) {
  371. return `
  372. <div class="input-group" style="padding: 0 16px;" title="${tip}">
  373. ${icons.commandIcon}
  374. <input type="text" class="form-control input-monospace input-sm clone" value="${url}" readonly=""
  375. data-autoselect="">
  376. <div class="input-group-button">
  377. <clipboard-copy value="${url}" class="btn btn-sm">
  378. ${icons.copyIcon}
  379. </clipboard-copy>
  380. </div>
  381. </div>`;
  382. }
  383.  
  384. /**
  385. * Get the browse list.
  386. */
  387. function getBrowseList() {
  388. let menuButtonHtml = ``;
  389. const href = window.location.href.split('/');
  390. const path = window.location.pathname;
  391. browseSet.forEach(id => {
  392. menuButtonHtml += getBrowseHtml(
  393. mirrors[id]['url'] + path,
  394. mirrors[id]['name'],
  395. mirrors[id]['description']
  396. );
  397. });
  398. if (href.length == 5 || path.includes('/tree/') || path.includes('/blob/')) {
  399. var html = mirrors[5]['url'] + path.replace('/tree/', '@').replace('/blob/', '@');
  400. if (!path.includes('/blob/')) {
  401. html += '/';
  402. }
  403. menuButtonHtml += getBrowseHtml(html, mirrors[5]['name'], mirrors[5]['description']);
  404. }
  405. if (location.hostname != 'github.com') {
  406. menuButtonHtml += getBrowseHtml(`https://github.com${path}`, '返回GitHub');
  407. }
  408. return menuButtonHtml;
  409. }
  410.  
  411. /**
  412. * Get browse html string.
  413. * @param {String} url url.
  414. * @param {String} name name.
  415. * @param {String} tip tip.
  416. * @returns
  417. */
  418. function getBrowseHtml(url, name, tip = '') {
  419. return `
  420. <a class="SelectMenu-item" href="${url}" target="_blank" title="${tip}" role="menuitemradio" aria-checked="false" rel="nofollow">
  421. ${icons.linkIcon}
  422. <span class="css-truncate css-truncate-overflow" style="width: 520px; overflow: hidden; word-break:keep-all; white-space:nowrap; text-overflow:ellipsis;">${url}</span>
  423. <span class="css-truncate css-truncate-overflow" style="width: 80px; text-align: right;">${name}</span>
  424. </a>`;
  425. }
  426.  
  427. /**
  428. * Add Release list.
  429. */
  430. function addReleasesList() {
  431. let length = 0;
  432. let timeout = 0;
  433. let interval = setInterval(() => {
  434. timeout += 1;
  435. length = $('.Box--condensed').find('[href]').length;
  436.  
  437. // Stop the circulator when the element is found or after 10 seconds(100*100 ms).
  438. if (timeout == 100 || length > 0) {
  439. clearInterval(interval);
  440. $('.Box--condensed')
  441. .find('[href]')
  442. .each(function () {
  443. const href = $(this).attr('href');
  444. $(this)
  445. .parent()
  446. .after(`<div class="Box-body" >` + getReleaseDownloadHtml(href) + `</div>`);
  447. $(this).parent().removeClass('Box-body');
  448. });
  449. }
  450. }, 100);
  451. }
  452.  
  453. /**
  454. * Get Release download button html string.
  455. * @param {String} href href.
  456. * @returns html.
  457. */
  458. function getReleaseDownloadHtml(href) {
  459. let html = '';
  460. downloadSet.forEach(id => {
  461. html += `<a class="flex-1 btn btn-outline get-repo-btn" rel="nofollow" href="${
  462. mirrors[id]['url'] + href
  463. }" title="${mirrors[id]['description']}">${mirrors[id]['name']}</a>`;
  464. });
  465. return html;
  466. }
  467.  
  468. /**
  469. * Add download zip button.
  470. */
  471. function addDownloadZip() {
  472. $("a[data-open-app='link']").each(function () {
  473. var li = $(`<li class="Box-row p-0"></li>`);
  474. const downloadHref = $(this).attr('href');
  475. var aElement = $(this)
  476. .clone()
  477. .removeAttr('data-hydro-click data-hydro-click-hmac data-ga-click');
  478. aElement.addClass('Box-row Box-row--hover-gray');
  479. downloadSet.forEach(id => {
  480. let tempA = aElement.clone();
  481. tempA.attr({
  482. href: mirrors[id]['url'] + downloadHref,
  483. title: mirrors[id]['description']
  484. });
  485. tempA.html(
  486. tempA.html().replace('Download ZIP', `Download ZIP(${mirrors[id]['name']})`)
  487. );
  488. li = li.clone().append(tempA);
  489. });
  490. $(this).parent().after(li);
  491. });
  492. }
  493.  
  494. /**
  495. * Add Raw list.
  496. */
  497. function addRawList() {
  498. $('#raw-url').each(function () {
  499. var href = $(this).attr('href');
  500. rawSet.forEach(id => {
  501. if (id == 3 || id == 10) {
  502. getRawHtml(id, mirrors[id]['url'] + href.replace('/raw', ''));
  503. } else if (id == 6) {
  504. getRawHtml(id, mirrors[id]['url'] + href.replace('/raw/', '@'));
  505. } else {
  506. getRawHtml(id, mirrors[id]['url'] + href);
  507. }
  508. });
  509. });
  510. }
  511.  
  512. /**
  513. *
  514. * @param {Number} id id of mirrors.
  515. * @param {String} url url.
  516. */
  517. function getRawHtml(id, url) {
  518. var span = $('#raw-url').clone().removeAttr('id');
  519. span.attr({
  520. href: url,
  521. title: mirrors[id]['description'],
  522. target: '_blank'
  523. });
  524. span.text(mirrors[id]['name']);
  525. $('#raw-url').before(span);
  526. }
  527.  
  528. /**
  529. * Get message by setting.
  530. */
  531. function getMessage() {
  532. return 'zh' == GM_getValue('lang') ? messages.zh : messages.en;
  533. }
  534.  
  535. /**
  536. * Log the title and version at the front of the console.
  537. * @param {String} title title.
  538. * @param {String} version script version.
  539. */
  540. function logInfo(title, version) {
  541. const titleStyle = 'color:white;background-color:#606060';
  542. const versionStyle = 'color:white;background-color:#1475b2';
  543. const logTitle = ' ' + title + ' ';
  544. const logVersion = ' ' + version + ' ';
  545. console.log('%c' + logTitle + '%c' + logVersion, titleStyle, versionStyle);
  546. }
  547.  
  548. /**
  549. * Check if the visitor is PC.
  550. */
  551. function isPC() {
  552. var userAgentInfo = navigator.userAgent;
  553. var agents = ['Android', 'iPhone', 'SymbianOS', 'Windows Phone', 'iPad', 'iPod'];
  554. var isPC = true;
  555. const len = agents.length;
  556. for (var v = 0; v < len; v++) {
  557. if (userAgentInfo.indexOf(agents[v]) > 0) {
  558. isPC = false;
  559. break;
  560. }
  561. }
  562. return isPC;
  563. }
  564. })();