Greasy Fork is available in English.

Greasy Fork Enhance

Enhance your experience at Greasyfork.

// ==UserScript==
// @name         Greasy Fork Enhance
// @name:zh-CN   Greasy Fork 增强
// @namespace    http://tampermonkey.net/
// @version      0.7.2
// @description  Enhance your experience at Greasyfork.
// @description:zh-CN 增进 Greasyfork 浏览体验。
// @author       PRO
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @match        https://greasyfork.org/*
// @require      https://update.greasyfork.org/scripts/470224/1317473/Tampermonkey%20Config.js
// @icon         https://raw.githubusercontent.com/greasyfork-org/greasyfork/main/public/images/blacklogo16.png
// @icon64       https://raw.githubusercontent.com/greasyfork-org/greasyfork/main/public/images/blacklogo96.png
// @license      gpl-3.0
// ==/UserScript==

(function () {
    'use strict';
    // Judge if the script should run
    const no_run = [".json", ".js"];
    let is_run = true;
    const idPrefix = "greasyfork-enhance-";
    no_run.forEach((suffix) => {
        if (window.location.pathname.endsWith(suffix)) {
            is_run = false;
        }
    });
    if (!is_run) return;
    // Config
    const config_desc = {
        "$default": {
            value: true,
            input: "current",
            processor: "not",
            formatter: "boolean",
            autoClose: false
        },
        "auto-hide-code": { name: "Auto hide code", title: "Hide long code blocks by default" },
        "auto-hide-rows": {
            name: "Min rows to hide",
            value: 10,
            input: "prompt",
            processor: "int_range-1-",
            formatter: "normal",
            title: "Minimum number of rows to hide"
        },
        "flat-layout": { name: "Flat layout", title: "Use flat layout for script list and descriptions", value: false },
        "animation": { name: "Animation", title: "Enable animation for toggling code blocks" },
        "lib-alternative-url": { name: "Alternative URLs for library", title: "Show a list of alternative URLs for a given library", value: false },
        "short-link": { name: "Short link", title: "Display a shortened link to current script" },
        "shortcut": { name: "Shortcut", title: "Enable keyboard shortcuts" },
        "search-syntax": { name: "*Search syntax", title: "Enable partial search syntax for Greasy Fork search bar" },
        "image-proxy": { name: "*Image proxy", title: "Use `wsrv.nl` as proxy for user-uploaded images", value: false },
    };
    const config = GM_config(config_desc);
    // CSS
    const dynamicStyle = {
        "flat-layout": `
        .script-list li:not(.ad-entry) { padding-right: 0; } ol.script-list > li > article { display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
        ol.script-list > li > article > h2 { width: 60%; overflow: hidden; text-overflow: ellipsis; margin-right: 0.5em; padding-right: 0.5em; border-right: 1px solid #DDDDDD; }
        .showing-all-languages .badge-js, .showing-all-languages .badge-css, .script-type { display: none; }
        ol.script-list > li > article > h2 > a.script-link { white-space: nowrap; }
        ol.script-list > li > article > h2 > span.script-description { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        ol.script-list > li > article > div.script-meta-block { width: 40%; column-gap: 0; }
        ol.script-list > li[data-script-type="library"] > article > h2 { width: 80%; }
        ol.script-list > li[data-script-type="library"] > article > div.script-meta-block { width: 20%; column-count: 1; }
        ol.script-list > li > article > div.script-meta-block > dl.inline-script-stats { margin: 0; }
        ol.script-list > li > article > div.script-meta-block > dl.inline-script-stats > dd { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        #script-info div.script-meta-block { float: right; column-count: 1; max-width: 300px; border-left: 1px solid #DDDDDD; margin-left: 1em; padding-left: 1em; }
        #additional-info { width: calc(100% - 2em - 2px); }
        @media (max-width: 600px) {
            ol.script-list > li:not([data-script-type="library"]) > article { display: block; }
            ol.script-list > li:not([data-script-type="library"]) > article > h2 { width: unset; border-right: none; }
            ol.script-list > li:not([data-script-type="library"]) > article > div.script-meta-block { column-count: 2; }
            ol.script-list > li > article > div.script-meta-block { width: unset; column-gap: 0; }
            ol.script-list > li[data-script-type="library"] > article > div.script-meta-block { width: 40%; }
        }`,
        "animation": `
        /* Toggle code animation */
        pre > code { transition: height 0.5s ease-in-out 0s; }
        /* Adapted from animate.css - https://animate.style/ */
        :root { --animate-duration: 1s; --animate-delay: 1s; --animate-repeat: 1; }
        .animate__animated { animation-duration: var(--animate-duration); animation-fill-mode: both; }
        .animate__animated.animate__fastest { animation-duration: calc(var(--animate-duration) / 3); }
        @keyframes tada {
            from { transform: scale3d(1, 1, 1); }
            10%, 20% { transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); }
            30%, 50%, 70%, 90% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); }
            40%, 60%, 80% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); }
            to { transform: scale3d(1, 1, 1); }
        }
        .animate__tada { animation-name: tada; }
        @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
        .animate__fadeIn { animation-name: fadeIn; }
        @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
        .animate__fadeOut { -webkit-animation-name: fadeOut; animation-name: fadeOut; }`
    };
    // Functions
    const $ = document.querySelector.bind(document);
    const $$ = document.querySelectorAll.bind(document);
    const body = $("body");
    function sanitify(s) {
        // Remove emojis (such a headache)
        s = s.replaceAll(/([\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2580-\u27BF]|\uD83E[\uDD10-\uDEFF]|\uFE0F)/g, "");
        // Trim spaces and newlines
        s = s.trim();
        // Replace spaces
        s = s.replaceAll(" ", "-");
        s = s.replaceAll("%20", "-");
        // No more multiple "-"
        s = s.replaceAll(/-+/g, "-");
        return s;
    }
    function process(node) { // Add anchor and assign id to given node; Add to outline. Return true if node is actually processed.
        if (node.childElementCount > 1 || node.classList.length > 0) return false; // Ignore complex nodes
        const text = node.textContent;
        if (!node.id) { // If the node has no id
            node.id = sanitify(text); // Then assign id
        }
        // Add anchors
        const anchor = node.appendChild(document.createElement('a'));
        anchor.className = 'anchor';
        anchor.href = '#' + node.id;
        const link = outline.appendChild(document.createElement("li"))
            .appendChild(document.createElement("a"));
        link.href = "#" + node.id;
        link.text = text;
        return true;
    }
    async function animate(node, animation) {
        return new Promise((resolve, reject) => {
            node.classList.add("animate__animated", "animate__" + animation);
            if (node.getAnimations().length == 0) {
                node.classList.remove("animate__animated", "animate__" + animation);
                reject("No animation available");
            }
            node.addEventListener('animationend', e => {
                e.stopPropagation();
                node.classList.remove("animate__animated", "animate__" + animation);
                resolve("Animation ended");
            }, { once: true });
        });
    }
    async function transition(node, height) {
        return new Promise((resolve, reject) => {
            node.style.height = height;
            if (node.getAnimations().length == 0) {
                resolve("No transition available");
            }
            node.addEventListener('transitionend', e => {
                e.stopPropagation();
                resolve("Transition ended");
            }, { once: true });
        });
    }
    function copyCode() {
        const code = this.parentNode.nextElementSibling;
        const text = code.textContent;
        navigator.clipboard.writeText(text).then(() => {
            this.textContent = "Copied!";
            animate(this, "tada").then(() => {
                this.textContent = "Copy code";
            }, () => {
                window.setTimeout(() => {
                    this.textContent = "Copy code";
                }, 1000);
            });
        });
    }
    function toggleCode() {
        const code = this.parentNode.nextElementSibling;
        if (code.style.height == "0px") {
            code.style.willChange = "height";
            transition(code, code.getAttribute("data-height")).then(() => {
                code.style.willChange = "";
            });
            animate(this, "fadeOut").then(() => {
                this.textContent = "Hide code";
                animate(this, "fadeIn");
            }, () => {
                this.textContent = "Hide code";
            });
        } else {
            code.style.willChange = "height";
            transition(code, "0px").then(() => {
                code.style.willChange = "";
            });
            animate(this, "fadeOut").then(() => {
                this.textContent = "Show code";
                animate(this, "fadeIn");
            }, () => {
                this.textContent = "Show code";
            });
        }
    }
    function create_toolbar() {
        const toolbar = document.createElement("div");
        const copy = toolbar.appendChild(document.createElement("a"));
        const toggle = toolbar.appendChild(document.createElement("a"));
        copy.textContent = "Copy code";
        copy.className = "code-operation";
        copy.title = "Copy code to clipboard";
        copy.addEventListener("click", copyCode);
        toggle.textContent = "Hide code";
        toggle.classList.add("code-operation", "animate__fastest");
        toggle.title = "Toggle code display";
        toggle.addEventListener("click", toggleCode);
        // Css
        toolbar.className = "code-toolbar";
        return toolbar;
    }
    function injectCSS(id, css) {
        const style = document.head.appendChild(document.createElement("style"));
        style.id = idPrefix + id;
        style.textContent = css;
    }
    function cssHelper(id, enable) {
        const current = document.getElementById(idPrefix + id);
        if (current) {
            current.disabled = !enable;
        } else if (enable) {
            injectCSS(id, dynamicStyle[id]);
        }
    }
    // Basic css
    injectCSS("basic", `
    html { scroll-behavior: smooth; }
    a.anchor::before { content: "#"; }
    a.anchor { opacity: 0; text-decoration: none; padding: 0px 0.5em; transition: all 0.25s ease-in-out; }
    h1:hover>a.anchor, h2:hover>a.anchor, h3:hover>a.anchor,
    h4:hover>a.anchor, h5:hover>a.anchor, h6:hover>a.anchor { opacity: 1; transition: all 0.25s ease-in-out; }
    a.button { margin: 0.5em 0 0 0; display: flex; align-items: center; justify-content: center; text-decoration: none; color: black; background-color: #a42121ab; border-radius: 50%; width: 2em; height: 2em; font-size: 1.8em; font-weight: bold; }
    div.code-toolbar { display: flex; gap: 1em; }
    a.code-operation { cursor: pointer; font-style: italic; }
    div.lum-lightbox { z-index: 2; }
    div#float-buttons { position: fixed; bottom: 1em; right: 1em; display: flex; flex-direction: column; user-select: none; z-index: 1; }
    aside.panel { display: none; }
    .dynamic-opacity { transition: opacity 0.2s ease-in-out; opacity: 0.2; }
    .dynamic-opacity:hover { opacity: 0.8; }
    input[type=file] { border-style: dashed; border-radius: 0.5em; border-color: gray; padding: 0.5em; background: rgba(169, 169, 169, 0.4); transition-property: border-color, background; transition-duration: 0.25s; transition-timing-function: ease-in-out; }
    input[type=file]:hover { border-color: black; background: rgba(169, 169, 169, 0.6); }
    input[type=file]::file-selector-button { border: 1px solid; border-radius: 0.3em; transition: background 0.25s ease-in-out; background: rgba(169, 169, 169, 0.7); }
    input[type=file]::file-selector-button:hover { background: rgba(169, 169, 169, 1); }
    table { border: 1px solid #8d8d8d; border-collapse: collapse; width: auto; }
    table td, table th { padding: 0.5em 0.75em; vertical-align: middle; border: 1px solid #8d8d8d; }
    @media (any-hover: none) { .dynamic-opacity { opacity: 0.8; } .dynamic-opacity:hover { opacity: 0.8; } }
    @media screen and (min-width: 767px) {
        aside.panel { display: contents; line-height: 1.5; }
        ul.outline { position: sticky; float: right; padding: 0 0 0 0.5em; margin: 0 0.5em -99vh; max-height: 80vh; border: 1px solid #BBBBBB; border-left: 2px solid #F2E5E5; box-shadow: 0 0 5px #ddd; background: linear-gradient(to right, #fcf1f1, #FFF 1em); list-style: none; width: 10.5%; color: gray; border-radius: 5px; overflow-y: scroll; z-index: 1; }
        ul.outline > li { overflow: hidden; text-overflow: ellipsis; }
        ul.outline > li > a { color: gray; white-space: nowrap; text-decoration: none; }
    }
    pre > code { overflow: hidden; display: block; }
    ul { padding-left: 1.5em; }`);
    // Aside panel & Anchors
    let outline;
    const is_script = /^\/[^\/]+\/scripts/;
    const is_specific_script = /^\/[^\/]+\/scripts\/\d+/;
    const is_disccussion = /^\/[^\/]+\/discussions/;
    const path = window.location.pathname;
    if ((!is_script.test(path) && !is_disccussion.test(path)) || is_specific_script.test(path)) {
        const panel = body.insertBefore(document.createElement("aside"), $("body > div.width-constraint"));
        panel.className = "panel";
        const reference_node = $("body > div.width-constraint > section");
        outline = panel.appendChild(document.createElement("ul"));
        outline.classList.add("outline");
        outline.classList.add("dynamic-opacity");
        outline.style.top = reference_node ? getComputedStyle(reference_node).marginTop : "1em";
        outline.style.marginTop = outline.style.top;
        let flag = false;
        $$("body > div.width-constraint h1, h2, h3, h4, h5, h6").forEach((node) => {
            flag = process(node) || flag; // Not `flag || process(node)`!
        });
        if (!flag) {
            panel.remove();
        }
    }
    // Navigate to hash
    const hash = window.location.hash.slice(1);
    if (hash) {
        const ele = document.getElementById(decodeURIComponent(hash));
        if (ele) {
            ele.scrollIntoView();
        }
    }
    // Buttons
    const buttons = body.appendChild(document.createElement("div"));
    buttons.id = "float-buttons";
    const to_top = buttons.appendChild(document.createElement("a"));
    to_top.classList.add("button");
    to_top.classList.add("dynamic-opacity");
    to_top.href = "#top";
    to_top.text = "↑";
    // Double click to get to top
    body.addEventListener("dblclick", (e) => {
        if (e.target === body) {
            to_top.click();
        }
    });
    // Fix current tab link
    const tab = $("ul#script-links > li.current");
    if (tab) {
        const link = tab.appendChild(document.createElement("a"));
        link.href = window.location.pathname;
        link.appendChild(tab.firstChild);
    }
    const parts = window.location.pathname.split("/");
    if (parts.length <= 2 || (parts.length == 3 && parts[2] === '')) {
        const banner = $("header#main-header div#site-name");
        const img = banner.querySelector("img");
        const text = banner.querySelector("#site-name-text > h1");
        const link1 = document.createElement("a");
        link1.href = window.location.pathname;
        img.parentNode.replaceChild(link1, img);
        link1.appendChild(img);
        const link2 = document.createElement("a");
        link2.href = window.location.pathname;
        link2.textContent = text.textContent;
        text.textContent = "";
        text.appendChild(link2);
    }
    // Toolbar for code blocks
    const code_blocks = document.getElementsByTagName("pre");
    for (const code_block of code_blocks) {
        if (code_block.firstChild.tagName === "CODE") {
            const height = getComputedStyle(code_block.firstChild).getPropertyValue("height");
            code_block.firstChild.style.height = height;
            code_block.firstChild.setAttribute("data-height", height);
            code_block.insertAdjacentElement("afterbegin", create_toolbar());
        }
    }
    // Auto hide code blocks
    function autoHide() {
        if (!config["auto-hide-code"]) {
            for (const code_block of code_blocks) {
                const toggle = code_block.firstChild.lastChild;
                if (toggle.textContent === "Show code") {
                    toggle.click(); // Click the toggle button
                }
            }
        } else {
            for (const code_block of code_blocks) {
                const m = code_block.lastChild.textContent.match(/\n/g);
                const rows = m ? m.length : 0;
                const toggle = code_block.firstChild.lastChild;
                const hidden = toggle.textContent === "Show code";
                if (rows >= config["auto-hide-rows"] && !hidden || rows < config["auto-hide-rows"] && hidden) {
                    code_block.firstChild.lastChild.click(); // Click the toggle button
                }
            }
        }
    }
    document.addEventListener("readystatechange", (e) => {
        if (e.target.readyState === "complete") {
            autoHide();
        }
    }, { once: true });
    // Alternative URLs for library
    function alternativeURLs(enable) {
        if ($(".remove-attachments") || !$("div#script-content") || $("div#script-content > div#install-area")) return; // Not a library
        const id = idPrefix + "lib-alternative-url";
        const current = document.getElementById(id);
        if (current && !enable) {
            current.remove();
        } else if (!current && enable) {
            const description = $("div#script-content > p");
            const trim = "// @require ";
            const text = description?.querySelector("code")?.textContent;
            if (!text || !text.startsWith(trim)) return; // Found no URL
            const url = text.slice(trim.length);
            const parts = url.split("/");
            const scriptId = parts[4];
            const scriptVersion = parts[5];
            const fileName = parts[6];
            const URLs = [
                [`// @require https://update.greasyfork.org/scripts/${scriptId}/${fileName}`, "Latest version"],
                [`// @require https://greasyfork.org/scripts/${scriptId}/code/${fileName}?version=${scriptVersion}`, "Current version (Legacy)"],
                [`// @require https://greasyfork.org/scripts/${scriptId}/code/${fileName}`, "Latest version (Legacy)"],
            ];

            const detail = document.createElement("p").appendChild(document.createElement("details"));
            description.after(detail.parentElement);
            detail.parentElement.id = id;
            detail.appendChild(document.createElement("summary")).textContent = "Alternative URLs";
            const list = detail.appendChild(document.createElement("ul"));
            for (const [url, text] of URLs) {
                const link = list.appendChild(document.createElement("li")).appendChild(document.createElement("code"));
                link.textContent = url;
                link.title = text;
            }
        }
    }
    alternativeURLs(config["lib-alternative-url"]);
    // Short link
    function shortLink(enable) {
        const description = $("div#script-content");
        const url = window.location.href;
        const scriptId = url.match(/\/scripts\/(\d+)/)?.[1];
        if (!scriptId || !description) return;
        const id = idPrefix + "short-link";
        const current = document.getElementById(id);
        if (current && !enable) {
            current.remove();
        } else if (!current && enable) {
            const short = `https://greasyfork.org/scripts/${scriptId}`;
            const p = description.insertAdjacentElement("beforebegin", document.createElement("p"));
            p.id = id;
            p.textContent = "Short link: ";
            const link = p.appendChild(document.createElement("a"));
            link.href = short;
            link.textContent = short;
            const copy = p.appendChild(document.createElement("a"));
            copy.textContent = "(Copy)";
            copy.style.marginLeft = "1em";
            copy.style.cursor = "pointer";
            copy.title = "Copy short link to clipboard";
            copy.addEventListener("click", () => {
                if (copy.textContent === "(Copied!)") return;
                navigator.clipboard.writeText(short).then(() => {
                    copy.textContent = "(Copied!)";
                    window.setTimeout(() => {
                        copy.textContent = "(Copy)";
                    }, 1000);
                });
            });
        }
    }
    shortLink(config["short-link"]);
    // Shortcut
    function shortcut(enable) {
        const textAreas = $$("textarea");
        let enabled = false;
        function onKeyup(e) {
            const form = this.form;
            if (!form) return;
            // Ctrl + Enter to submit
            if (e.ctrlKey && e.key === "Enter") {
                form.submit();
            }
        }
        if (!enabled && enable) {
            for (const textarea of textAreas) {
                textarea.addEventListener("keyup", onKeyup);
            }
            enabled = true;
        } else if (enabled && !enable) {
            for (const textarea of textAreas) {
                textarea.removeEventListener("keyup", onKeyup);
            }
            enabled = false;
        }
    }
    shortcut(config["shortcut"]);
    // Initialize css
    for (const prop in dynamicStyle) {
        cssHelper(prop, config[prop]);
    }
    // Dynamically respond to config changes
    const callbacks = {
        "auto-hide-code": autoHide,
        "auto-hide-rows": autoHide,
        "flat-layout": (after) => {
            const meta_orig = $("#script-info > #script-content > .script-meta-block");
            const meta_mod = $("#script-info > .script-meta-block");
            if (after && meta_orig) {
                const links = $("#script-info > #script-links");
                links.after(meta_orig);
            } else if (!after && meta_mod) {
                const additional = $("#script-info > #script-content > #additional-info");
                additional.before(meta_mod);
            }
        },
        "lib-alternative-url": alternativeURLs,
        "short-link": shortLink
    };
    callbacks["flat-layout"](config["flat-layout"]);
    window.addEventListener(GM_config_event, e => {
        if (e.detail.type === "set") {
            const callback = callbacks[e.detail.prop];
            if (callback && (e.detail.before !== e.detail.after)) {
                callback(e.detail.after);
            }
            if (e.detail.prop in dynamicStyle) {
                cssHelper(e.detail.prop, e.detail.after);
            }
        }
    });
    // Search syntax
    if (config["search-syntax"]) {
        const search = $("input[type=search][name=q]");
        const submit = $("input[type=submit]");
        if (search && submit) {
            const form = search.parentElement;
            // site:site-name
            function parseSite(s) {
                const m = s.match(/\bsite:(\S*)/);
                return m?.[1] || null;
            }
            form.addEventListener("submit", (e) => {
                const site = parseSite(search.value);
                if (site) {
                    search.value = search.value.replace(/\s*\bsite:\S*/, "");
                    form.action = `/scripts/by-site/${site}?site=${site}`;
                }
            });
        }
    }
    // Image proxy
    if (config["image-proxy"]) {
        const PROXY = "https://wsrv.nl/?url=";
        const images = $$("a[href^='/rails/active_storage/blobs/redirect/'] > img[src^='https://greasyfork.']");
        for (const img of images) {
            img.src = PROXY + img.src;
            const link = img.parentElement;
            link.href = PROXY + link.href;
        }
    }
})();