GitHub File Downloader

Allows you to download individual files directly from GitHub repository pages.

  1. // ==UserScript==
  2. // @name GitHub File Downloader
  3. // @description Allows you to download individual files directly from GitHub repository pages.
  4. // @icon https://github.githubassets.com/favicons/favicon-dark.svg
  5. // @version 1.3
  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 style = document.createElement('style');
  18. style.textContent = `
  19. .github-icon-replacer-hover {
  20. cursor: pointer;
  21. transition: transform 0.1s ease;
  22. }
  23. .github-icon-replacer-hover:hover {
  24. transform: scale(1.1);
  25. }
  26. `;
  27. document.head.appendChild(style);
  28.  
  29. async function downloadFile(url, fileName) {
  30. try {
  31. const response = await fetch(url);
  32. const blob = await response.blob();
  33. const link = document.createElement('a');
  34. link.href = window.URL.createObjectURL(blob);
  35. link.download = fileName;
  36. link.style.display = 'none';
  37. document.body.appendChild(link);
  38. link.click();
  39. document.body.removeChild(link);
  40. } catch (error) {
  41. console.error('Download failed:', error);
  42. }
  43. }
  44.  
  45. function replaceIcons() {
  46. const directoryRows = document.querySelectorAll('tr.react-directory-row');
  47. directoryRows.forEach(row => {
  48. const svgIcons = row.querySelectorAll('.react-directory-filename-column svg.color-fg-muted');
  49. svgIcons.forEach(svg => {
  50. if (!svg.dataset.replaced) {
  51. const wrapper = document.createElement('div');
  52. wrapper.style.display = 'inline-block';
  53. svg.innerHTML = `
  54. <g>
  55. <path d="M14.5,3.4l-2.9-2.9C11.2,0.2,10.8,0,10.3,0H3.8C2.8,0,2,0.8,2,1.8v12.5c0,1,0.8,1.8,1.8,1.8h9.5c1,0,1.8-0.8,1.8-1.8V4.7
  56. C15,4.2,14.8,3.8,14.5,3.4z M10.5,1.6L10.5,1.6l2.9,2.9l0,0h-2.7c-0.1,0-0.2-0.1-0.2-0.2V1.6z M13.5,14.2c0,0.1-0.1,0.2-0.2,0.2
  57. H3.8c-0.1,0-0.2-0.1-0.2-0.2V1.8c0-0.1,0.1-0.2,0.2-0.2H9v2.8C9,5.2,9.8,6,10.8,6h2.8V14.2z"/>
  58. <path d="M9.1,10.6V7.3c0-0.3-0.3-0.6-0.6-0.6S7.9,7,7.9,7.3v3.3L6.5,9.3C6.3,9,5.9,9,5.7,9.3c-0.2,0.2-0.2,0.6,0,0.8l2.4,2.4
  59. c0.2,0.2,0.6,0.2,0.8,0h0l2.4-2.4c0.2-0.2,0.2-0.6,0-0.8c-0.2-0.2-0.6-0.2-0.8,0L9.1,10.6z"/>
  60. </g>
  61. `;
  62. svg.setAttribute('viewBox', '0 0 16 16');
  63. svg.classList.add('github-icon-replacer-hover');
  64. svg.dataset.replaced = 'true';
  65.  
  66. const fileLink = row.querySelector('a[href]');
  67. if (fileLink) {
  68. const downloadUrl = fileLink.href
  69. .replace('github.com', 'raw.githubusercontent.com')
  70. .replace('/blob/', '/');
  71. const fileName = fileLink.textContent.trim();
  72.  
  73. svg.addEventListener('click', (e) => {
  74. e.preventDefault();
  75. e.stopPropagation();
  76. downloadFile(downloadUrl, fileName);
  77. });
  78. }
  79. }
  80. });
  81. });
  82. }
  83.  
  84. const observer = new MutationObserver((mutations) => {
  85. for (let mutation of mutations) {
  86. if (mutation.type === 'childList') {
  87. replaceIcons();
  88. }
  89. }
  90. });
  91.  
  92. observer.observe(document.body, { childList: true, subtree: true });
  93.  
  94. replaceIcons();
  95. })();