GreasyFork: download script button

If you have a script manager and you want to download some script without installing it, this script will help

As of 2023-12-19. See the latest version.

  1. // ==UserScript==
  2. // @name GreasyFork: download script button
  3. // @description If you have a script manager and you want to download some script without installing it, this script will help
  4. // @author Konf
  5. // @version 2.2.5
  6. // @namespace https://greatest.deepsurf.us/users/424058
  7. // @icon https://greatest.deepsurf.us/vite/assets/blacklogo96-e0c2c761.png
  8. // @match https://greatest.deepsurf.us/*/scripts/*
  9. // @match https://sleazyfork.org/*/scripts/*
  10. // @compatible Chrome
  11. // @compatible Opera
  12. // @compatible Firefox
  13. // @run-at document-end
  14. // @grant GM_addStyle
  15. // @noframes
  16. // ==/UserScript==
  17.  
  18. /* jshint esversion: 8 */
  19.  
  20. (function() {
  21. 'use strict';
  22.  
  23. const i18n = {
  24. download: 'download',
  25. downloadWithoutInstalling: 'downloadWithoutInstalling',
  26. failedToDownload: 'failedToDownload',
  27. };
  28.  
  29. const translate = (function() {
  30. const userLang = location.pathname.split('/')[1];
  31. const strings = {
  32. 'en': {
  33. [i18n.download]: 'Download ⇩',
  34. [i18n.downloadWithoutInstalling]: 'Download without installing',
  35. [i18n.failedToDownload]:
  36. 'Failed to download the script. There is might be more info in the browser console',
  37. },
  38. 'ru': {
  39. [i18n.download]: 'Скачать ⇩',
  40. [i18n.downloadWithoutInstalling]: 'Скачать не устанавливая',
  41. [i18n.failedToDownload]:
  42. 'Не удалось скачать скрипт. Больше информации может быть в консоли браузера',
  43. },
  44. 'zh-CN': {
  45. [i18n.download]: '下载 ⇩',
  46. [i18n.downloadWithoutInstalling]: '下载此脚本',
  47. [i18n.failedToDownload]: '无法下载此脚本',
  48. },
  49. };
  50.  
  51. return id => (strings[userLang] || strings.en)[id] || strings.en[id];
  52. }());
  53.  
  54. const installBtns = document.querySelectorAll('a.install-link');
  55. const installArea = document.querySelector('div#install-area');
  56. const installHelpLinks = document.querySelectorAll('a.install-help-link');
  57. const suggestion = document.querySelector('div#script-feedback-suggestion');
  58. const libraryRequire = document.querySelector('div#script-content > p > code');
  59. const libraryVersion = document.querySelector(
  60. '#script-stats > dd.script-show-version > span'
  61. );
  62.  
  63. // if a script/style is detected
  64. if (
  65. installArea &&
  66. (installBtns.length > 0) &&
  67. (installBtns.length === installHelpLinks.length)
  68. ) {
  69. for (let i = 0; i < installBtns.length; i++) {
  70. mountScriptDownloadButton(installBtns[i], installArea, installHelpLinks[i]);
  71. }
  72. }
  73. // or maybe a library
  74. else if (suggestion && libraryRequire) {
  75. mountLibraryDownloadButton(suggestion, libraryRequire, libraryVersion);
  76. }
  77.  
  78. function mountScriptDownloadButton(
  79. installBtn,
  80. installArea,
  81. installHelpLink,
  82. ) {
  83. if (!installBtn.href) throw new Error('script href is not found');
  84.  
  85. // https://img.icons8.com/pastel-glyph/64/ffffff/download.png
  86. // array to fold the string in a code editor
  87. const downloadIconBase64 = [
  88. 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaX',
  89. 'HeAAAABmJLR0QA/wD/AP+gvaeTAAABgUlEQVR4nO3ZTU6DUAAE4HnEk+jWG3TrHV',
  90. 'wY3XoEt23cGleamtRtTbyPS3sCV0bXjptHRAIEsM/hZ76kCZRHGaZAGwDMzMzMbJ',
  91. '6CasMkMwBncXYbQvhSZZEgecEf56ocmWrDAA4L00eqEMoCBsEFqAOouQB1ADUXoA',
  92. '6g5gLUAdRcgDqAmgtQB1BzAeoAakkLIHlN8pPkDcnWd59IBpK3cd1VyoxJkfwo3P',
  93. 'V5KJZAcllYtiy8H+LY3HvKjKlPgU1h+hLAuulIiMvWcWzVZ4xL/Dbv+Nsjyax8BM',
  94. 'Sx96Wxm3jzdLwaSliVCpjezucqzmuSfKuZJkvXi0moORKqTOebL2tRwnR3PtdQwv',
  95. 'R3PldRgmznlc8GA4DTOPscQqAqy6x1+X8+6Ke5yfNxIE9z6/TN1+XCM4inuQ165Z',
  96. 'vHz04DF6AOoOYC1AHUXIA6gNpBz/UWJK/2muTvFn1W6lvASXyNXpdTYJcsxf69th',
  97. '3Y5QjYAiCA485x/tcLgCd1CDMzMzMbum8+xtkWw6QCvwAAAABJRU5ErkJggg==',
  98. ].join('');
  99.  
  100. GM_addStyle([`
  101. .GF-DSB__script-download-button {
  102. position: relative;
  103. padding: 8px 22px;
  104. cursor: pointer;
  105. border: none;
  106. background: #0F750F;
  107. transition: box-shadow 0.2s;
  108. }
  109.  
  110. .GF-DSB__script-download-button:hover,
  111. .GF-DSB__script-download-button:focus {
  112. box-shadow: 0 8px 16px 0 rgb(0 0 0 / 20%), 0 6px 20px 0 rgb(0 0 0 / 19%);
  113. }
  114.  
  115.  
  116. .GF-DSB__script-download-icon {
  117. position: absolute;
  118. }
  119.  
  120. .GF-DSB__script-download-icon--download {
  121. width: 30px;
  122. height: 30px;
  123. top: 4px;
  124. left: 7px;
  125. }
  126.  
  127. .GF-DSB__script-download-icon--loading,
  128. .GF-DSB__script-download-icon--loading:after {
  129. border-radius: 50%;
  130. width: 16px;
  131. height: 16px;
  132. }
  133.  
  134. .GF-DSB__script-download-icon--loading {
  135. top: 8px;
  136. left: 11px;
  137. border-top: 3px solid rgba(255, 255, 255, 0.2);
  138. border-right: 3px solid rgba(255, 255, 255, 0.2);
  139. border-bottom: 3px solid rgba(255, 255, 255, 0.2);
  140. border-left: 3px solid #ffffff;
  141. transform: translateZ(0);
  142. object-position: -99999px;
  143. animation: GF-DSB__script-download-loading-icon 1.1s infinite linear;
  144. }
  145.  
  146. @keyframes GF-DSB__script-download-loading-icon {
  147. 0% {
  148. transform: rotate(0deg);
  149. }
  150. 100% {
  151. transform: rotate(360deg);
  152. }
  153. }
  154. `][0]);
  155.  
  156. const b = document.createElement('a');
  157. const bIcon = document.createElement('img');
  158.  
  159. b.href = '#';
  160. b.title = translate(i18n.downloadWithoutInstalling);
  161. b.draggable = false;
  162. b.className = 'GF-DSB__script-download-button';
  163.  
  164. bIcon.src = downloadIconBase64;
  165. bIcon.draggable = false;
  166. bIcon.className =
  167. 'GF-DSB__script-download-icon GF-DSB__script-download-icon--download';
  168.  
  169. installHelpLink.style.position = 'relative'; // shadows bugfix
  170.  
  171. b.appendChild(bIcon);
  172. installArea.insertBefore(b, installHelpLink);
  173.  
  174. // against doubleclicks
  175. let isFetchingAllowed = true;
  176.  
  177. async function clicksHandler(ev) {
  178. ev.preventDefault();
  179.  
  180. setTimeout(() => b === document.activeElement && b.blur(), 250);
  181.  
  182. if (isFetchingAllowed === false) return;
  183.  
  184. isFetchingAllowed = false;
  185. bIcon.className =
  186. 'GF-DSB__script-download-icon GF-DSB__script-download-icon--loading';
  187.  
  188. try {
  189. let scriptName = installBtn.dataset.scriptName;
  190.  
  191. if (installBtn.dataset.scriptVersion) {
  192. scriptName += ` ${installBtn.dataset.scriptVersion}`;
  193. }
  194.  
  195. await downloadScript({
  196. fileExt: `.user.${installBtn.dataset.installFormat || 'txt'}`,
  197. href: installBtn.href,
  198. name: scriptName,
  199. });
  200. } catch (e) {
  201. console.error(e);
  202. alert(`${translate(i18n.failedToDownload)}: \n${e}`);
  203. } finally {
  204. setTimeout(() => {
  205. isFetchingAllowed = true;
  206. bIcon.className =
  207. 'GF-DSB__script-download-icon GF-DSB__script-download-icon--download';
  208. }, 300);
  209. }
  210. }
  211.  
  212. b.addEventListener('click', clicksHandler);
  213. b.addEventListener('auxclick', e => e.button === 1 && clicksHandler(e));
  214. }
  215.  
  216. function mountLibraryDownloadButton(suggestion, libraryRequire, libraryVersion) {
  217. let [
  218. libraryHref,
  219. libraryName,
  220. ] = libraryRequire.innerText.match(
  221. /\/\/ @require (https:\/\/.+\/scripts\/\d+\/\d+\/(.*)\.js)/
  222. ).slice(1);
  223.  
  224. // this probably is completely useless but whatever
  225. if (!libraryHref) throw new Error('library href is not found');
  226.  
  227. if (libraryVersion?.innerText) libraryName += ` ${libraryVersion.innerText}`;
  228.  
  229. GM_addStyle([`
  230. .GF-DSB__library-download-button {
  231. transition: box-shadow 0.2s;
  232. }
  233.  
  234. .GF-DSB__library-download-button--loading {
  235. animation: GF-DSB__loading-text 1s infinite linear;
  236. }
  237.  
  238. @keyframes GF-DSB__loading-text {
  239. 50% {
  240. opacity: 0.4;
  241. }
  242. }
  243. `][0]);
  244.  
  245. const b = document.createElement('a');
  246.  
  247. b.href = '#';
  248. b.draggable = false;
  249. b.innerText = translate(i18n.download);
  250. b.className = 'GF-DSB__library-download-button';
  251.  
  252. suggestion.appendChild(b);
  253.  
  254. // against doubleclicks
  255. let isFetchingAllowed = true;
  256.  
  257. async function clicksHandler(ev) {
  258. ev.preventDefault();
  259.  
  260. setTimeout(() => b === document.activeElement && b.blur(), 250);
  261.  
  262. if (isFetchingAllowed === false) return;
  263.  
  264. isFetchingAllowed = false;
  265. b.className =
  266. 'GF-DSB__library-download-button GF-DSB__library-download-button--loading';
  267.  
  268. try {
  269. await downloadScript({
  270. fileExt: '.js',
  271. href: libraryHref,
  272. name: libraryName,
  273. });
  274. } catch (e) {
  275. console.error(e);
  276. alert(`${translate(i18n.failedToDownload)}: \n${e}`);
  277. } finally {
  278. setTimeout(() => {
  279. isFetchingAllowed = true;
  280. b.className = 'GF-DSB__library-download-button';
  281. }, 300);
  282. }
  283. }
  284.  
  285. b.addEventListener('click', clicksHandler);
  286. b.addEventListener('auxclick', e => e.button === 1 && clicksHandler(e));
  287. }
  288.  
  289. // utils --------------------------------------------------------------------
  290.  
  291. // Is needed because you can't fetch a new format script link
  292. // due to different domain cors restriction...
  293. function convertScriptHrefToAnOldFormat(href) {
  294. const regex = /https:\/\/update\.(\w+\.org)\/scripts\/(\d+)\/(\d+\/)?(.+)/;
  295. const match = href.match(regex);
  296.  
  297. if (!match) throw new Error("can't convert href to an old format");
  298.  
  299. const domain = match[1];
  300. const scriptId = match[2];
  301. const version = match[3] ? `?version=${match[3]}` : '';
  302. const scriptName = match[4];
  303.  
  304. return `https://${domain}/scripts/${scriptId}/code/${scriptName}${version}`;
  305. }
  306.  
  307. async function downloadScript({
  308. fileExt = '.txt',
  309. href,
  310. name = Date.now(),
  311. } = {}) {
  312. if (!href) throw new Error('Script href is missing');
  313.  
  314. const fetchErrors = [];
  315. let url;
  316.  
  317. // Consider first attempt as a main one. Second one is
  318. // needed just for some unknown edge case scenarios. See link:
  319. // https://greatest.deepsurf.us/scripts/420872/discussions/216921
  320. for (const scriptHref of [
  321. convertScriptHrefToAnOldFormat(href),
  322. href,
  323. ]) {
  324. try {
  325. const response = await fetch(scriptHref);
  326.  
  327. if (response.status !== 200) {
  328. throw new Error(`Bad response: ${response.status}`);
  329. }
  330.  
  331. url = window.URL.createObjectURL(await response.blob());
  332. } catch (e) {
  333. fetchErrors.push(e);
  334. }
  335. }
  336.  
  337. if (!url) {
  338. fetchErrors.forEach(e => console.error(e));
  339.  
  340. throw new Error('Failed to fetch. See console');
  341. }
  342.  
  343. const a = document.createElement('a');
  344.  
  345. a.href = url;
  346. a.download = `${name}${fileExt}`;
  347. document.body.appendChild(a); // is needed due to firefox bug
  348. a.click();
  349. a.remove();
  350.  
  351. window.URL.revokeObjectURL(url);
  352. }
  353. }());