If you have a script manager and you want to download some script without installing it, this script will help
- // ==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.1
- // @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/*
- // @compatible Chrome
- // @compatible Opera
- // @compatible Firefox
- // @run-at document-end
- // @grant GM_addStyle
- // @noframes
- // ==/UserScript==
- /* jshint esversion: 8 */
- (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 is 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.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',
- 'wY3XoEt23cGleamtRtTbyPS3sCV0b###tHRAIEsM/hZ76kCZRHGaZAGwDMzMzMbJ',
- '6CasMkMwBncXYbQvhSZZEgecEf56ocmWrDAA4L00eqEMoCBsEFqAOouQB1ADUXoA',
- '6g5gLUAdRcgDqAmgtQB1BzAeoAakkLIHlN8pPkDcnWd59IBpK3cd1VyoxJkfwo3P',
- 'V5KJZAcllYtiy8H+LY3HvKjKlPgU1h+hLAuulIiMvWcWzVZ4xL/Dbv+Nsjyax8BM',
- 'Sx96Wxm3jzdLwaSliVCpjezucqzmuSfKuZJkvXi0moORKqTOebL2tRwnR3PtdQwv',
- 'R3PldRgmznlc8GA4DTOPscQqAqy6x1+X8+6Ke5yfNxIE9z6/TN1+XCM4inuQ165Z',
- 'vHz04DF6AOoOYC1AHUXIA6gNpBz/UWJK/2muTvFn1W6lvASXyNXpdTYJcsxf69th',
- '3Y5QjYAiCA485x/tcL###1CDMzMzMbum8+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';
- installHelpLink.style.position = 'relative'; // shadows bugfix
- b.appendChild(bIcon);
- installArea.insertBefore(b, installHelpLink);
- // against doubleclicks
- 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);
- // against doubleclicks
- 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 --------------------------------------------------------------------
- // Is needed because you can't fetch a new format script link
- // due to different domain cors restriction...
- function convertScriptHrefToAnOldFormat(href) {
- const regex = /https:\/\/update\.(\w+\.org)\/scripts\/(\d+)\/(\d+\/)?(.+)/;
- const match = href.match(regex);
- if (!match) throw new Error("can't convert href to an old format");
- const domain = match[1];
- const scriptId = match[2];
- const version = match[3] ? `?version=${match[3]}` : '';
- const scriptName = match[4];
- return `https://${domain}/scripts/${scriptId}/code/${scriptName}${version}`;
- }
- async function downloadScript({
- fileExt = '.txt',
- href,
- name = Date.now(),
- } = {}) {
- if (!href) throw new Error('Script href is missing');
- const fetchErrors = [];
- let linksToTry = [];
- let url;
- // "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
- linksToTry.push(webArchivePrefix + 'id_/' + href);
- } else {
- // Consider first link as a main attempt. Second one is
- // needed just for some unknown edge case scenarios. See:
- // https://greasyfork.org/scripts/420872/discussions/216921
- linksToTry = [
- convertScriptHrefToAnOldFormat(href),
- href,
- ];
- }
- for (const scriptHref of linksToTry) {
- try {
- const response = await fetch(scriptHref);
- if (response.status !== 200) {
- throw new Error(`Bad response: ${response.status}`);
- }
- url = window.URL.createObjectURL(await response.blob());
- break;
- } catch (e) {
- fetchErrors.push(e);
- }
- }
- if (!url) {
- fetchErrors.forEach(e => console.error(e));
- throw new Error('Failed to fetch. See console');
- }
- const a = document.createElement('a');
- a.href = url;
- a.download = `${name}${fileExt}`;
- document.body.appendChild(a); // is needed due to firefox bug
- a.click();
- a.remove();
- window.URL.revokeObjectURL(url);
- }
- }());