🏠 Home 

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