npm-file-downloader

A Tampermonkey script that supports downloading single npm files

  1. // ==UserScript==
  2. // @name npm-file-downloader
  3. // @video https://youtu.be/6BqphFJ69-g
  4. // @namespace http://tampermonkey.net/
  5. // @version 2024-11-04
  6. // @description A Tampermonkey script that supports downloading single npm files
  7. // @author qer
  8. // @match https://www.npmjs.com/*
  9. // @icon https://github.com/user-attachments/assets/7ccadb13-ee47-4206-88bf-050a087988d8
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. const mainButtonCssText = `
  17. background-color: #0969da;
  18. color: white;
  19. padding: 8px 16px;
  20. border: none;
  21. border-radius: 6px;
  22. font-size: 14px;
  23. font-weight: 500;
  24. cursor: pointer;
  25. transition: background-color 0.2s ease;
  26. margin-left: 10px;
  27. box-shadow: 0 1px 3px rgba(0,0,0,0.12);
  28. `;
  29.  
  30. const downloadButtonCssText = `
  31. color: #0969da;
  32. text-decoration: none;
  33. font-size: 14px;
  34. padding: 4px 8px;
  35. border-radius: 4px;
  36. transition: background-color 0.2s ease;
  37. `;
  38.  
  39. const showDownloadLinksButton = document.createElement('button');
  40. showDownloadLinksButton.innerHTML = 'Show Download Links';
  41. showDownloadLinksButton.style.cssText = mainButtonCssText;
  42. addButtonClickEffect(showDownloadLinksButton);
  43.  
  44. showDownloadLinksButton.onclick = showDownloadLinks; // download function
  45. addShowDownloadLinksButton(); // add button to the page
  46.  
  47. function addButtonClickEffect(button) {
  48. button.addEventListener('mouseover', function () {
  49. this.style.backgroundColor = '#0557c5';
  50. });
  51. button.addEventListener('mouseout', function () {
  52. this.style.backgroundColor = '#0969da';
  53. });
  54. button.addEventListener('mousedown', function () {
  55. this.style.transform = 'scale(0.98)';
  56. });
  57. button.addEventListener('mouseup', function () {
  58. this.style.transform = 'scale(1)';
  59. });
  60. }
  61.  
  62. function addShowDownloadLinksButton() {
  63. const tabListA = document.querySelector('ul[role="tablist"]').querySelectorAll('a');
  64. if (!tabListA) return;
  65. for (const a of tabListA) {
  66. a.onclick = () => {
  67. if (a.getAttribute('href') === '?activeTab=code') {
  68. document.querySelector('#main div').childNodes[1].querySelectorAll('li')[1].appendChild(showDownloadLinksButton);
  69. } else showDownloadLinksButton.remove();
  70. };
  71. }
  72.  
  73. const link = document.querySelector('a[href="?activeTab=code"]');
  74. if (!link) return;
  75. const ariaSelected = link.getAttribute('aria-selected');
  76. console.log('ariaSelected', ariaSelected);
  77. if (ariaSelected === 'true') {
  78. document.querySelector('#main div').childNodes[1].querySelectorAll('li')[1].appendChild(showDownloadLinksButton);
  79. }
  80. }
  81.  
  82. function info() {
  83. const path = new URL(window.location.href).pathname.split('/');
  84. return { packageName: path[2], version: path[4]?.replace(/^v\//, '') || 'latest' };
  85. }
  86.  
  87. function showDownloadLinks() {
  88. const { packageName, version } = info();
  89. console.log('Package Name:', packageName);
  90. console.log('Version:', version);
  91. const domain = 'https://nfd.qer.im';
  92. // const domain = 'http://localhost:8787';
  93. let path = ''; let liElements = [];
  94. try {
  95. const section = document.querySelector('#main div').childNodes[2].querySelector('section:not([data-attribute="hidden"])');
  96. console.log(section);
  97. path = section.querySelector('h2').innerText.split('/').slice(2)
  98. .join('/') || '';
  99. liElements = section.querySelectorAll('ul li');
  100. if (!liElements || liElements.length <= 0) {
  101. throw new Error(`Can't find elements: ${path}${liElements.length}`);
  102. }
  103. } catch (e) {
  104. alert(`Failed to add download links: ${e}`);
  105. }
  106.  
  107. liElements.forEach((li, index) => {
  108. const buttonText = li.querySelector('button').textContent;
  109. const fileType = li.querySelectorAll('div')[2].textContent;
  110. // const fileSize = li.querySelectorAll('div')[3].textContent;
  111. const isFolder = !fileType || fileType.toLowerCase() === 'folder';
  112.  
  113. if (buttonText === '../') return;
  114.  
  115. const downloadButton = document.createElement('a');
  116. downloadButton.style.cssText = downloadButtonCssText;
  117.  
  118. if (isFolder) {
  119. downloadButton.textContent = '(Folder)';
  120. downloadButton.style.cssText += `
  121. color: #666;
  122. cursor: not-allowed;
  123. opacity: 0.7;
  124. `;
  125. } else {
  126. downloadButton.textContent = 'Download';
  127. downloadButton.style.cursor = 'pointer';
  128. downloadButton.href = `${domain}/api/download?package=${packageName}&path=${path}&file=${encodeURIComponent(buttonText)}&version=${version}`;
  129.  
  130. downloadButton.addEventListener('click', function (event) {
  131. event.preventDefault();
  132.  
  133. const loadingIndicator = document.createElement('span');
  134. loadingIndicator.textContent = 'Downloading...';
  135. loadingIndicator.style.marginLeft = '5px';
  136. this.parentNode.insertBefore(loadingIndicator, this.nextSibling);
  137.  
  138. this.style.pointerEvents = 'none';
  139. this.style.opacity = '0.5';
  140.  
  141. setTimeout(() => {
  142. loadingIndicator.remove();
  143. this.style.pointerEvents = 'auto';
  144. this.style.opacity = '1';
  145.  
  146. window.location.href = this.href;
  147. }, 2000);
  148. });
  149. }
  150.  
  151. const fileNameButton = li.querySelector('button');
  152. fileNameButton.parentNode.insertBefore(downloadButton, fileNameButton.nextSibling);
  153. fileNameButton.parentNode.style.display = 'flex';
  154. fileNameButton.parentNode.style.alignItems = 'center';
  155. });
  156. }
  157. }());