GreasyFork: download script button

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

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

QingJ © 2025

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