GreasyFork: download script button

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name          GreasyFork: download script button
// @description   If you have a script manager and you want to download some script without installing it, this script will help
// @author        Konf
// @version       2.3.4
// @namespace     https://greasyfork.org/users/424058
// @icon          https://i.imgur.com/OIGiyQc.png
// @match         https://greasyfork.org/*/scripts/*
// @match         https://sleazyfork.org/*/scripts/*
// @match         https://web.archive.org/web/*/https://greasyfork.org/*/scripts/*
// @match         https://web.archive.org/web/*/https://sleazyfork.org/*/scripts/*
// @connect       update.greasyfork.org
// @connect       update.sleazyfork.org
// @compatible    Chrome
// @compatible    Opera
// @compatible    Firefox
// @run-at        document-end
// @grant         GM_addStyle
// @grant         GM.xmlHttpRequest
// @noframes
// ==/UserScript==

/* jshint esversion: 11 */

(function() {
  'use strict';

  const i18n = {
    download: 'download',
    downloadWithoutInstalling: 'downloadWithoutInstalling',
    failedToDownload: 'failedToDownload',
  };

  const translate = (function() {
    const userLang = location.pathname.split('/')[1];
    const strings = {
      'en': {
        [i18n.download]: 'Download ⇩',
        [i18n.downloadWithoutInstalling]: 'Download without installing',
        [i18n.failedToDownload]:
          'Failed to download the script. There might be more info in the browser console',
      },
      'ru': {
        [i18n.download]: 'Скачать ⇩',
        [i18n.downloadWithoutInstalling]: 'Скачать не устанавливая',
        [i18n.failedToDownload]:
          'Не удалось скачать скрипт. Больше информации может быть в консоли браузера',
      },
      'zh-CN': {
        [i18n.download]: '下载 ⇩',
        [i18n.downloadWithoutInstalling]: '下载此脚本',
        [i18n.failedToDownload]: '无法下载此脚本',
      },
    };

    return id => (strings[userLang] || strings.en)[id] || strings.en[id];
  }());

  const installArea = document.querySelector('div#install-area');
  const installBtns = installArea?.querySelectorAll(':scope > a.install-link');
  const installHelpLinks = document.querySelectorAll('a.install-help-link');
  const suggestion = document.querySelector('div#script-feedback-suggestion');
  const libraryRequire = document.querySelector('div#script-content > p > code');
  const libraryVersion = document.querySelector(
    '#script-stats > dd.script-show-version > span'
  );

  // if a script/style is detected
  if (
    installArea &&
    installBtns &&
    (installBtns.length > 0) &&
    (installBtns.length === installHelpLinks.length)
  ) {
    for (let i = 0; i < installBtns.length; i++) {
      mountScriptDownloadButton(installBtns[i], installArea, installHelpLinks[i]);
    }
  }
  // or maybe a library
  else if (suggestion && libraryRequire) {
    mountLibraryDownloadButton(suggestion, libraryRequire, libraryVersion);
  }

  function mountScriptDownloadButton(
    installBtn,
    installArea,
    installHelpLink,
  ) {
    if (!installBtn.href) throw new Error('script href is not found');

    // https://img.icons8.com/pastel-glyph/64/ffffff/download.png
    // array to fold the string in a code editor
    const downloadIconBase64 = [
      'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaX',
      'HeAAAABmJLR0QA/wD/AP+gvaeTAAABgUlEQVR4nO3ZTU6DUAAE4HnEk+jWG3TrHV',
      'wY3XoEt23cGleamtRtTbyPS3sCV0bXjptHRAIEsM/hZ76kCZRHGaZAGwDMzMzMbJ',
      '6CasMkMwBncXYbQvhSZZEgecEf56ocmWrDAA4L00eqEMoCBsEFqAOouQB1ADUXoA',
      '6g5gLUAdRcgDqAmgtQB1BzAeoAakkLIHlN8pPkDcnWd59IBpK3cd1VyoxJkfwo3P',
      'V5KJZAcllYtiy8H+LY3HvKjKlPgU1h+hLAuulIiMvWcWzVZ4xL/Dbv+Nsjyax8BM',
      'Sx96Wxm3jzdLwaSliVCpjezucqzmuSfKuZJkvXi0moORKqTOebL2tRwnR3PtdQwv',
      'R3PldRgmznlc8GA4DTOPscQqAqy6x1+X8+6Ke5yfNxIE9z6/TN1+XCM4inuQ165Z',
      'vHz04DF6AOoOYC1AHUXIA6gNpBz/UWJK/2muTvFn1W6lvASXyNXpdTYJcsxf69th',
      '3Y5QjYAiCA485x/tcLgCd1CDMzMzMbum8+xtkWw6QCvwAAAABJRU5ErkJggg==',
    ].join('');

    GM_addStyle([`
      .GF-DSB__script-download-button {
        position: relative;
        padding: 8px 22px;
        cursor: pointer;
        border: none;
        background: #0F750F;
        transition: box-shadow 0.2s;
      }

      .GF-DSB__script-download-button:hover,
      .GF-DSB__script-download-button:focus {
        box-shadow: 0 8px 16px 0 rgb(0 0 0 / 20%), 0 6px 20px 0 rgb(0 0 0 / 19%);
      }


      .GF-DSB__script-download-icon {
        position: absolute;
      }

      .GF-DSB__script-download-icon--download {
        width: 30px;
        height: 30px;
        top: 4px;
        left: 7px;
      }

      .GF-DSB__script-download-icon--loading,
      .GF-DSB__script-download-icon--loading:after {
        border-radius: 50%;
        width: 16px;
        height: 16px;
      }

      .GF-DSB__script-download-icon--loading {
        top: 8px;
        left: 11px;
        border-top: 3px solid rgba(255, 255, 255, 0.2);
        border-right: 3px solid rgba(255, 255, 255, 0.2);
        border-bottom: 3px solid rgba(255, 255, 255, 0.2);
        border-left: 3px solid #ffffff;
        transform: translateZ(0);
        object-position: -99999px;
        animation: GF-DSB__script-download-loading-icon 1.1s infinite linear;
      }

      @keyframes GF-DSB__script-download-loading-icon {
        0% {
          transform: rotate(0deg);
        }
        100% {
          transform: rotate(360deg);
        }
      }
    `][0]);

    const b = document.createElement('a');
    const bIcon = document.createElement('img');

    b.href = '#';
    b.title = translate(i18n.downloadWithoutInstalling);
    b.draggable = false;
    b.className = 'GF-DSB__script-download-button';

    bIcon.src = downloadIconBase64;
    bIcon.draggable = false;
    bIcon.className = (
      'GF-DSB__script-download-icon GF-DSB__script-download-icon--download'
    );

    // shadows bugfix
    installHelpLink.style.position = 'relative';

    b.appendChild(bIcon);
    installArea.insertBefore(b, installHelpLink);

    // doubleclicks countermeasure
    let isFetchingAllowed = true;

    async function clicksHandler(ev) {
      ev.preventDefault();

      setTimeout(() => b === document.activeElement && b.blur(), 250);

      if (isFetchingAllowed === false) return;

      isFetchingAllowed = false;
      bIcon.className = (
        'GF-DSB__script-download-icon GF-DSB__script-download-icon--loading'
      );

      try {
        let scriptName = installBtn.dataset.scriptName;

        if (installBtn.dataset.scriptVersion) {
          scriptName += ` ${installBtn.dataset.scriptVersion}`;
        }

        await downloadScript({
          fileExt: `.user.${installBtn.dataset.installFormat || 'txt'}`,
          href: installBtn.href,
          name: scriptName,
        });
      } catch (e) {
        console.error(e);
        alert(`${translate(i18n.failedToDownload)}: \n${e}`);
      } finally {
        setTimeout(() => {
          isFetchingAllowed = true;
          bIcon.className = (
            'GF-DSB__script-download-icon GF-DSB__script-download-icon--download'
          );
        }, 300);
      }
    }

    b.addEventListener('click', clicksHandler);
    b.addEventListener('auxclick', e => e.button === 1 && clicksHandler(e));
  }

  function mountLibraryDownloadButton(suggestion, libraryRequire, libraryVersion) {
    let [
      libraryHref,
      libraryName,
    ] = libraryRequire.innerText.match(
      /\/\/ @require (https:\/\/.+\/scripts\/\d+\/\d+\/(.*)\.js)/
    ).slice(1);

    // this probably is completely useless but whatever
    if (!libraryHref) throw new Error('library href is not found');

    libraryName = decodeURIComponent(libraryName);

    if (libraryVersion?.innerText) libraryName += ` ${libraryVersion.innerText}`;

    GM_addStyle([`
      .GF-DSB__library-download-button {
        transition: box-shadow 0.2s;
      }

      .GF-DSB__library-download-button--loading {
        animation: GF-DSB__loading-text 1s infinite linear;
      }

      @keyframes GF-DSB__loading-text {
        50% {
          opacity: 0.4;
        }
      }
    `][0]);

    const b = document.createElement('a');

    b.href = '#';
    b.draggable = false;
    b.innerText = translate(i18n.download);
    b.className = 'GF-DSB__library-download-button';

    suggestion.appendChild(b);

    // doubleclicks countermeasure
    let isFetchingAllowed = true;

    async function clicksHandler(ev) {
      ev.preventDefault();

      setTimeout(() => b === document.activeElement && b.blur(), 250);

      if (isFetchingAllowed === false) return;

      isFetchingAllowed = false;
      b.className = (
        'GF-DSB__library-download-button GF-DSB__library-download-button--loading'
      );

      try {
        await downloadScript({
          fileExt: '.js',
          href: libraryHref,
          name: libraryName,
        });
      } catch (e) {
        console.error(e);
        alert(`${translate(i18n.failedToDownload)}: \n${e}`);
      } finally {
        setTimeout(() => {
          isFetchingAllowed = true;
          b.className = 'GF-DSB__library-download-button';
        }, 300);
      }
    }

    b.addEventListener('click', clicksHandler);
    b.addEventListener('auxclick', e => e.button === 1 && clicksHandler(e));
  }

  // utils --------------------------------------------------------------------

  async function downloadScript({
    fileExt = '.txt',
    href,
    name = Date.now(),
  } = {}) {
    if (!href) throw new Error('Script href is missing');

    // "web.archive" part has been done poorly and unreliable
    if (location.hostname === 'web.archive.org') {

      // Get a "web.archive" link prefix. Full link example:
      // https://web.archive.org/web/20220827221543/https://greasyfork...
      // Prefix:
      // https://web.archive.org/web/20220827221543
      const webArchivePrefix = (
        location.href.match(/(.+)\/http(s|):\/\/(greas|sleaz)yfork\.org/)[1]
      );

      if (!webArchivePrefix) throw new Error('Failed to get script href');

      // "id_" part is needed to get a clean file from the webarchive.
      // By default there are some js metadata that will break the script.
      // See: https://archive.org/post/1044859
      // Possible alternative is to cut off these strings manually
      // hoping that there are fixed amount of them, or maybe using regex
      href = webArchivePrefix + 'id_/' + href;
    }

    const response = await new Promise((resolve, reject) => {
      GM.xmlHttpRequest({
        url: href,
        responseType: 'blob',
        onload: resolve,
        onerror: reject,
      });
    });

    if (response.status !== 200) {
      throw new Error(`Bad response: ${response.status}`);
    }

    const a = document.createElement('a');
    const blobUrl = window.URL.createObjectURL(response.response);

    a.href = blobUrl;
    a.download = `${name}${fileExt}`;
    document.body.appendChild(a); // firefox bug countermeasure
    a.click();
    a.remove();
    window.URL.revokeObjectURL(blobUrl);
  }
}());