Greasy Fork is available in English.

VSCO image downloader

Adds download buttons to VSCO images

// ==UserScript==
// @name         VSCO image downloader
// @namespace    https://greasyfork.org/users/1004887
// @version      1.1.2
// @description  Adds download buttons to VSCO images
// @author       Peter
// @match        https://vsco.co/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const icon = `
<svg width="48" height="48" viewBox="0 0 48 48">
    <g fill="none" stroke-miterlimit="2" stroke-linecap="square">
        <g stroke="#fff" stroke-width="4">
            <path d="m24,10 v20 l-7,-7 m7,7 7,-7"/>
            <path d="m9.5,31 v7.5 h29 v-7.5"/>
        </g>
        <g stroke="#000" stroke-width="2">
            <path d="m24,10 v20 l-7,-7 m7,7 7,-7"/>
            <path d="m9.5,31 v7.5 h29 v-7.5"/>
        </g>
    </g>
</svg>`;

    // add button to images at "i.vsco.co" and "im.vsco.co" but not "assets.vsco.co"
    const imageQuery = 'img:not([src*="assets.vsco.co"])';

    const observer = new MutationObserver(records => {
        for (const record of records) {
            for (const node of record.addedNodes) {
                addDownloadButtons(node.querySelectorAll(imageQuery));
            }
        }
    });

    // because links on VSCO use pushState (instead of loading a new page)
    const originalPushState = window.history.pushState;
    window.history.pushState = function(...args) {
        originalPushState.apply(window.history, args);
        newPage();
    };
    const originalReplaceState = window.history.replaceState;
    window.history.replaceState = function(...args) {
        originalReplaceState.apply(window.history, args);
        newPage();
    };
    window.addEventListener("popstate", newPage);

    let observing = false;
    function newPage() {
        // match "/search/images", "/<username>/gallery", and "/<username>/collection"
        if (/^\/(search\/images|[^/]+\/(gallery|collection))/.test(window.location.pathname)) {
            if (!observing) observer.observe(document.body, {subtree: true, childList: true});
            observing = true;

            // add buttons to already existing images
            addDownloadButtons(document.body.querySelectorAll(imageQuery));
        } else {
            if (observing) observer.disconnect();
            observing = false;
        }
    }
    newPage();

    function addDownloadButtons(imgs) {
        for (const img of imgs) {
            const outerLink = img.closest("a");
            if (outerLink) {
                // remove outer <a> tag
                outerLink.replaceWith(...outerLink.childNodes);
            }

            const btn = document.createElement("a");
            btn.innerHTML = icon;

            btn.style.width = "48px";
            btn.style.height = "48px";
            btn.style.position = "absolute";
            btn.style.top = "0";
            btn.style.left = "0";

            if (img.src == "data:,") {
                btn.href = img.srcset.split(/[? ]/, 1)[0];
            } else {
                // necessary for profile picture
                btn.href = img.src.split("?", 1)[0];
            }
            btn.onclick = e => {e.stopPropagation()};

            img.parentElement.style.position = "relative";
            img.parentElement.appendChild(btn);
        }
    }
})();