GreasyFork: download script button

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

目前为 2024-01-29 提交的版本。查看 最新版本

  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.8
  6. // @namespace https://greasyfork.org/users/424058
  7. // @icon https://web.archive.org/web/20240128171124im_/https://greasyfork.org/vite/assets/blacklogo96-sWE0jP07.png
  8. // @match https://greasyfork.org/*/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. libraryName = decodeURIComponent(libraryName);
  228.  
  229. if (libraryVersion?.innerText) libraryName += ` ${libraryVersion.innerText}`;
  230.  
  231. GM_addStyle([`
  232. .GF-DSB__library-download-button {
  233. transition: box-shadow 0.2s;
  234. }
  235.  
  236. .GF-DSB__library-download-button--loading {
  237. animation: GF-DSB__loading-text 1s infinite linear;
  238. }
  239.  
  240. @keyframes GF-DSB__loading-text {
  241. 50% {
  242. opacity: 0.4;
  243. }
  244. }
  245. `][0]);
  246.  
  247. const b = document.createElement('a');
  248.  
  249. b.href = '#';
  250. b.draggable = false;
  251. b.innerText = translate(i18n.download);
  252. b.className = 'GF-DSB__library-download-button';
  253.  
  254. suggestion.appendChild(b);
  255.  
  256. // against doubleclicks
  257. let isFetchingAllowed = true;
  258.  
  259. async function clicksHandler(ev) {
  260. ev.preventDefault();
  261.  
  262. setTimeout(() => b === document.activeElement && b.blur(), 250);
  263.  
  264. if (isFetchingAllowed === false) return;
  265.  
  266. isFetchingAllowed = false;
  267. b.className =
  268. 'GF-DSB__library-download-button GF-DSB__library-download-button--loading';
  269.  
  270. try {
  271. await downloadScript({
  272. fileExt: '.js',
  273. href: libraryHref,
  274. name: libraryName,
  275. });
  276. } catch (e) {
  277. console.error(e);
  278. alert(`${translate(i18n.failedToDownload)}: \n${e}`);
  279. } finally {
  280. setTimeout(() => {
  281. isFetchingAllowed = true;
  282. b.className = 'GF-DSB__library-download-button';
  283. }, 300);
  284. }
  285. }
  286.  
  287. b.addEventListener('click', clicksHandler);
  288. b.addEventListener('auxclick', e => e.button === 1 && clicksHandler(e));
  289. }
  290.  
  291. // utils --------------------------------------------------------------------
  292.  
  293. // Is needed because you can't fetch a new format script link
  294. // due to different domain cors restriction...
  295. function convertScriptHrefToAnOldFormat(href) {
  296. const regex = /https:\/\/update\.(\w+\.org)\/scripts\/(\d+)\/(\d+\/)?(.+)/;
  297. const match = href.match(regex);
  298.  
  299. if (!match) throw new Error("can't convert href to an old format");
  300.  
  301. const domain = match[1];
  302. const scriptId = match[2];
  303. const version = match[3] ? `?version=${match[3]}` : '';
  304. const scriptName = match[4];
  305.  
  306. return `https://${domain}/scripts/${scriptId}/code/${scriptName}${version}`;
  307. }
  308.  
  309. async function downloadScript({
  310. fileExt = '.txt',
  311. href,
  312. name = Date.now(),
  313. } = {}) {
  314. if (!href) throw new Error('Script href is missing');
  315.  
  316. const fetchErrors = [];
  317. let url;
  318.  
  319. // Consider first attempt as a main one. Second one is
  320. // needed just for some unknown edge case scenarios. See link:
  321. // https://greasyfork.org/scripts/420872/discussions/216921
  322. for (const scriptHref of [
  323. convertScriptHrefToAnOldFormat(href),
  324. href,
  325. ]) {
  326. try {
  327. const response = await fetch(scriptHref);
  328.  
  329. if (response.status !== 200) {
  330. throw new Error(`Bad response: ${response.status}`);
  331. }
  332.  
  333. url = window.URL.createObjectURL(await response.blob());
  334.  
  335. break;
  336. } catch (e) {
  337. fetchErrors.push(e);
  338. }
  339. }
  340.  
  341. if (!url) {
  342. fetchErrors.forEach(e => console.error(e));
  343.  
  344. throw new Error('Failed to fetch. See console');
  345. }
  346.  
  347. const a = document.createElement('a');
  348.  
  349. a.href = url;
  350. a.download = `${name}${fileExt}`;
  351. document.body.appendChild(a); // is needed due to firefox bug
  352. a.click();
  353. a.remove();
  354.  
  355. window.URL.revokeObjectURL(url);
  356. }
  357. }());