Greasy Fork镜像 还支持 简体中文。

GitHub Commit File Downloader

Allows you to download individual files or all files as ZIP directly from commit pages.

  1. // ==UserScript==
  2. // @name GitHub Commit File Downloader
  3. // @description Allows you to download individual files or all files as ZIP directly from commit pages.
  4. // @icon https://github.githubassets.com/favicons/favicon-dark.svg
  5. // @version 1.1
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/userscripts/
  8. // @supportURL https://github.com/afkarxyz/userscripts/issues
  9. // @license MIT
  10. // @match https://github.com/*
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. const fileZipIconIndividual = '<svg aria-hidden="true" focusable="false" class="octicon octicon-file-zip" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" display="inline-block" overflow="visible" style="vertical-align: text-bottom;"><g><path d="M1,1.8C1,0.8,1.8,0,2.8,0h7.6c0.5,0,0.9,0.2,1.2,0.5l2.9,2.9C14.8,3.8,15,4.2,15,4.7v9.6c0,1-0.8,1.8-1.8,1.8H2.8c-1,0-1.8-0.8-1.8-1.8V1.8z M10.5,1.6c0,0-0.1-0.1-0.2-0.1H2.8c-0.1,0-0.2,0.1-0.2,0.2v12.5c0,0.1,0.1,0.2,0.2,0.2h10.5c0.1,0,0.2-0.1,0.2-0.2V4.7c0-0.1,0-0.1-0.1-0.2" /><path d="M8.7,9.2V5c0-0.4-0.4-0.7-0.7-0.7S7.3,4.7,7.3,5v4.1L5.5,7.5c-0.2-0.4-0.7-0.4-1,0c-0.2,0.2-0.2,0.7,0,1l3,3c0.2,0.2,0.7,0.2,1,0l0,0l3-3c0.2-0.2,0.2-0.7,0-1c-0.2-0.2-0.7-0.2-1,0L8.7,9.2z" /></g></svg>'; const fileZipIconAll = '<svg aria-hidden="true" focusable="false" class="octicon octicon-file-zip" viewBox="0 0 16 16" width="16" height="16" fill="currentColor" display="inline-block" overflow="visible" style="vertical-align: text-bottom;"><path d="M3.5 1.75v11.5c0 .09.048.173.126.217a.75.75 0 0 1-.752 1.298A1.748 1.748 0 0 1 2 13.25V1.75C2 .784 2.784 0 3.75 0h5.586c.464 0 .909.185 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v8.586A1.75 1.75 0 0 1 12.25 15h-.5a.75.75 0 0 1 0-1.5h.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177L9.513 1.573a.25.25 0 0 0-.177-.073H7.25a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5h-3a.25.25 0 0 0-.25.25Zm3.75 8.75h.5c.966 0 1.75.784 1.75 1.75v3a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1-.75-.75v-3c0-.966.784-1.75 1.75-1.75ZM6 5.25a.75.75 0 0 1 .75-.75h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 6 5.25Zm.75 2.25h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM8 6.75A.75.75 0 0 1 8.75 6h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 8 6.75ZM8.75 3h.5a.75.75 0 0 1 0 1.5h-.5a.75.75 0 0 1 0-1.5ZM8 9.75A.75.75 0 0 1 8.75 9h.5a.75.75 0 0 1 0 1.5h-.5A.75.75 0 0 1 8 9.75Zm-1 2.5v2.25h1v-2.25a.25.25 0 0 0-.25-.25h-.5a.25.25 0 0 0-.25.25Z"></path></svg>';
  18.  
  19. function createIndividualDownloadButtons() {
  20. const fileRows = document.querySelectorAll('li[id][role="treeitem"]');
  21. fileRows.forEach(row => {
  22. const filename = row.id.replace(/\u200E/g, '');
  23. if (row.hasAttribute('aria-expanded') || row.querySelector('.gh-file-download')) return;
  24. if (!filename || !filename.includes('.')) return;
  25.  
  26. const fileIconDiv = row.querySelector('.PRIVATE_TreeView-item-visual');
  27. if (!fileIconDiv) return;
  28. fileIconDiv.innerHTML = fileZipIconIndividual;
  29. fileIconDiv.style.cursor = 'pointer';
  30. fileIconDiv.className += ' gh-file-download';
  31. fileIconDiv.style.transition = 'transform 0.15s ease-in-out';
  32. fileIconDiv.addEventListener('mouseenter', () => fileIconDiv.style.transform = 'scale(1.1)');
  33. fileIconDiv.addEventListener('mouseleave', () => fileIconDiv.style.transform = 'scale(1)');
  34.  
  35. fileIconDiv.onclick = async (e) => {
  36. e.stopPropagation();
  37. const [_, user, repo, __, commit] = location.pathname.split('/');
  38. const rawUrl = `https://raw.githubusercontent.com/${user}/${repo}/${commit}/${filename}`;
  39. try {
  40. const res = await fetch(rawUrl);
  41. const blob = await res.blob();
  42. const url = URL.createObjectURL(blob);
  43. const a = document.createElement('a');
  44. a.href = url;
  45. a.download = filename.split('/').pop();
  46. document.body.appendChild(a);
  47. a.click();
  48. a.remove();
  49. URL.revokeObjectURL(url);
  50. } catch (e) {
  51. alert(`Failed to download ${filename}`);
  52. console.error(e);
  53. }
  54. };
  55. });
  56. }
  57.  
  58. function createDownloadAllZipButton() {
  59. const titleEl = document.querySelector('h1[data-component="PH_Title"]');
  60. if (!titleEl || titleEl.querySelector('.gh-download-all-zip')) return;
  61.  
  62. const btn = document.createElement('button');
  63. btn.innerHTML = fileZipIconAll;
  64. btn.className = 'gh-download-all-zip';
  65. btn.style.marginLeft = '12px';
  66. btn.style.cursor = 'pointer';
  67. btn.style.border = 'none';
  68. btn.style.background = 'none';
  69. btn.style.display = 'inline-flex';
  70. btn.style.alignItems = 'center';
  71. btn.style.justifyContent = 'center';
  72. btn.style.padding = '0';
  73. btn.style.transition = 'transform 0.15s ease-in-out'; btn.addEventListener('mouseenter', () => btn.style.transform = 'scale(1.2)');
  74. btn.addEventListener('mouseleave', () => btn.style.transform = 'scale(1)');
  75.  
  76. btn.onclick = async () => {
  77. const [_, user, repo, __, commit] = location.pathname.split('/');
  78. const directUrl = `https://github.com/${user}/${repo}/archive/${commit}.zip`;
  79. const a = document.createElement('a');
  80. a.href = directUrl;
  81. a.download = `${repo}-${commit.slice(0, 7)}.zip`;
  82. a.target = '_blank';
  83. document.body.appendChild(a);
  84. a.click();
  85. a.remove();
  86. };
  87.  
  88. titleEl.appendChild(btn);
  89. }
  90.  
  91. function handleRouteChange() {
  92. if (!location.pathname.match(/^\/[^\/]+\/[^\/]+\/commit\/[a-f0-9]+$/)) return;
  93. createIndividualDownloadButtons();
  94. createDownloadAllZipButton();
  95. }
  96.  
  97. const observer = new MutationObserver(() => {
  98. handleRouteChange();
  99. });
  100. observer.observe(document.body, { childList: true, subtree: true }); (function() {
  101. const origPushState = history.pushState;
  102. const origReplaceState = history.replaceState;
  103. let lastPath = location.pathname;
  104.  
  105. function checkPathChange() {
  106. if (location.pathname !== lastPath) {
  107. lastPath = location.pathname;
  108. setTimeout(handleRouteChange, 100);
  109. }
  110. }
  111.  
  112. history.pushState = function(...args) {
  113. origPushState.apply(this, args);
  114. checkPathChange();
  115. };
  116.  
  117. history.replaceState = function(...args) {
  118. origReplaceState.apply(this, args);
  119. checkPathChange();
  120. };
  121.  
  122. window.addEventListener('popstate', checkPathChange);
  123. })();
  124.  
  125. handleRouteChange();
  126. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址