Greasy Fork is available in English.

Greasy Fork 增强

增进 Greasyfork 浏览体验。

// ==UserScript==
// @name         Greasy Fork Enhance
// @name:zh-CN   Greasy Fork 增强
// @namespace    http://tampermonkey.net/
// @version      0.8.8
// @description  Enhance your experience at Greasyfork.
// @description:zh-CN 增进 Greasyfork 浏览体验。
// @match        https://greasyfork.org/*
// @author       PRO
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_addValueChangeListener
// @require      https://update.greasyfork.org/scripts/470224/1459364/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-";
    const name = GM_info.script.name;
    no_run.forEach((suffix) => {
        if (window.location.pathname.endsWith(suffix)) {
            is_run = false;
        }
    });
    if (!is_run) return;
    // Config
    const configDesc = {
        $default: {
            autoClose: false,
        },
        filterAndSearch: {
            name: "🔎 Filter and Search",
            type: "folder",
            items: {
                shortcut: {
                    name: "Shortcut",
                    title: "Enable keyboard shortcuts",
                    type: "bool",
                    value: true,
                },
                regexFilter: {
                    name: "Regex filter",
                    title: "Use regex to filter out matching scripts",
                    value: "",
                },
                searchSyntax: {
                    name: "*Search syntax",
                    title: "Enable partial search syntax for Greasy Fork search bar",
                    type: "bool",
                    value: true,
                },
            },
        },
        codeblocks: {
            name: "📝 Code blocks",
            type: "folder",
            items: {
                autoHideCode: {
                    name: "Auto hide code",
                    title: "Hide long code blocks by default",
                    type: "bool",
                    value: true,
                },
                autoHideRows: {
                    name: "Min rows to hide",
                    title: "Minimum number of rows to hide",
                    type: "int",
                    value: 10,
                    processor: "int_range-1-",
                },
                tabSize: {
                    name: "Tab size",
                    title: "Set Tab indentation size",
                    type: "int",
                    value: 4,
                    processor: "int_range-0-",
                },
                animation: {
                    name: "Animation",
                    title: "Enable animation for toggling code blocks",
                    type: "bool",
                    value: true,
                },
                metadata: {
                    name: "Metadata",
                    title: "Parses certain script metadata and displays it on the script code page",
                    type: "bool",
                    value: false,
                },
            }
        },
        display: {
            name: "🎨 Display",
            type: "folder",
            items: {
                hideButtons: {
                    name: "Hide buttons",
                    title: "Hide floating buttons added by this script",
                    type: "bool",
                    value: false,
                },
                stickyPagination: {
                    name: "Sticky pagination",
                    title: "Make pagination bar sticky",
                    type: "bool",
                    value: true,
                },
                flatLayout: {
                    name: "Flat layout",
                    title: "Use flat layout for script list and descriptions",
                    type: "bool",
                    value: false,
                },
                showVersion: {
                    name: "Show version",
                    title: "Show version number in script list",
                    type: "bool",
                    value: false,
                },
                alwaysShowNotification: {
                    name: "Always show notification",
                    title: "Always show the notification widget",
                    type: "bool",
                    value: false,
                }
            },
        },
        other: {
            name: "🔧 Other",
            type: "folder",
            items: {
                shortLink: {
                    name: "Short link",
                    title: "Display a shortened link to current script",
                    type: "bool",
                    value: true,
                },
                libAlternativeUrl: {
                    name: "Alternative URLs for library",
                    title: "Show a list of alternative URLs for a given library",
                    type: "bool",
                    value: false,
                },
                imageProxy: {
                    name: "*Image proxy",
                    title: "Use `wsrv.nl` as proxy for user-uploaded images",
                    type: "bool",
                    value: false,
                },
                lazyImage: {
                    name: "*Lazy image",
                    title: "Load user images lazily",
                    type: "bool",
                    value: false,
                },
                debug: {
                    name: "Debug",
                    title: "Enable debug mode",
                    type: "bool",
                    value: false,
                },
            }
        }
    };
    const config = new GM_config(configDesc);
    const configProxy = config.proxy;
    // CSS
    const dynamicStyle = {
        "codeblocks.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; }
        `,
        "display.hideButtons": `div#float-buttons { display: none; }`,
        "display.stickyPagination": `.sidebarred-main-content > .pagination { position: sticky; bottom: 0; backdrop-filter: blur(5px); padding: 0.5em; }`,
        "display.flatLayout": `
            .script-list > li {
                &:not(.ad-entry) { padding-right: 0; }
                article {
                    display: flex; flex-direction: row; justify-content: space-between; align-items: center;
                    > .script-meta-block {
                        width: 40%; column-gap: 0;
                        > .inline-script-stats {
                            margin: 0;
                            > dd { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
                        }
                    }
                    > h2 {
                        width: 60%; overflow: hidden; text-overflow: ellipsis; margin-right: 0.5em; padding-right: 0.5em; border-right: 1px solid #88888888;
                        > .script-link { white-space: nowrap; }
                        > .script-description { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
                    }
                }
                &[data-script-type="library"] > article {
                    > h2 { width: 80%; }
                    > .script-meta-block { width: 20%; column-count: 1; }
                }
            }
            @media (max-width: 600px) {
                .script-list > li {
                    &[data-script-type="library"] > article > div.script-meta-block { width: 40%; }
                    &:not([data-script-type="library"]) > article {
                        display: block;
                        > h2 { width: unset; border-right: none; }
                        > .script-meta-block { column-count: 2; }
                    }
                    > article > div.script-meta-block { width: unset; column-gap: 0; }
                }
            }
            .showing-all-languages .badge-js, .showing-all-languages .badge-css, .script-type { display: none; }
            #script-info .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); }
        `,
        "display.showVersion": `.script-list > li[data-script-version]::before { content: "@" attr(data-script-version); position: absolute; translate: 0 -1em; color: grey; font-size: smaller; }`,
    };
    // Functions
    const $ = document.querySelector.bind(document);
    const $$ = document.querySelectorAll.bind(document);
    const body = $("body");
    function log(...args) {
        if (configProxy["other.debug"]) {
            console.log(`[${name}]`, ...args);
        }
    }
    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(outline, 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 createToolbar() {
        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; }
    #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; }
    .script-list > .regex-filtered { display: none; }
    #greasyfork-enhance-regex-filter-tip { float: right; color: grey; }
    @media screen and (max-width: 800px) { #greasyfork-enhance-regex-filter-tip { display: none; } }`);
    // Aside panel & Anchors
    const isScript = /^\/[^\/]+\/scripts/;
    const isSpecificScript = /^\/[^\/]+\/scripts\/\d+/;
    const isDisccussion = /^\/[^\/]+\/discussions/;
    const path = window.location.pathname;
    if ((!isScript.test(path) && !isDisccussion.test(path)) || isSpecificScript.test(path)) {
        const panel = body.insertBefore(document.createElement("aside"), $("body > div.width-constraint"));
        panel.className = "panel";
        const reference_node = $("body > div.width-constraint > section");
        const 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(outline, 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 goToTop = buttons.appendChild(document.createElement("a"));
    goToTop.classList.add("button");
    goToTop.classList.add("dynamic-opacity");
    goToTop.href = "#top";
    goToTop.text = "↑";
    // Double click to get to top
    body.addEventListener("dblclick", (e) => {
        if (e.target === body) {
            goToTop.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 codeBlocks = document.getElementsByTagName("pre");
    for (const codeBlock of codeBlocks) {
        if (codeBlock.firstChild.tagName === "CODE") {
            const height = getComputedStyle(codeBlock.firstChild).getPropertyValue("height");
            codeBlock.firstChild.style.height = height;
            codeBlock.firstChild.setAttribute("data-height", height);
            codeBlock.insertAdjacentElement("afterbegin", createToolbar());
        }
    }

    // Filter and Search
    // Shortcut
    function submitOnCtrlEnter(e) {
        const form = this.form;
        if (!form) return;
        // Ctrl + Enter to submit
        if (e.ctrlKey && e.key === "Enter") {
            form.submit();
        }
    }
    function handleShortcut(e) {
        const ele = document.activeElement;
        // Ignore key combinations
        if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) {
            return;
        }
        // Do not interfere with input elements
        if (ele.tagName === "INPUT" || ele.tagName === "TEXTAREA" || ele.getAttribute("contenteditable") === "true") {
            if (e.key === "Escape") {
                e.preventDefault();
                ele.blur(); // Escape to blur
            }
            return;
        }
        // Do not interfere with input methods
        if (e.isComposing || e.keyCode === 229) {
            return;
        }
        // Focus on search bar
        switch (e.key) {
            case "Enter": {
                const input = $("input[type=search]") || $("input[type=text]") || $("textarea");
                if (input) {
                    e.preventDefault();
                    input.focus();
                }
                break;
            }
            case "ArrowLeft":
                $("a.previous_page")?.click();
                break;
            case "ArrowRight":
                $("a.next_page")?.click();
                break;
        }
    }
    let shortcutEnabled = false;
    function shortcut(enable) {
        const textAreas = $$("textarea");
        if (!shortcutEnabled && enable) {
            for (const textarea of textAreas) {
                textarea.addEventListener("keyup", submitOnCtrlEnter);
            }
            document.addEventListener("keydown", handleShortcut);
            shortcutEnabled = true;
        } else if (shortcutEnabled && !enable) {
            for (const textarea of textAreas) {
                textarea.removeEventListener("keyup", submitOnCtrlEnter);
            }
            document.removeEventListener("keydown", handleShortcut);
            shortcutEnabled = false;
        }
    }
    shortcut(configProxy["filterAndSearch.shortcut"]);
    // Regex filter
    const regexFilterTip = $(".sidebarred > .sidebarred-main-content > .script-list#browse-script-list")
        ?.previousElementSibling?.appendChild?.(document.createElement("span"));
    if (regexFilterTip) {
        regexFilterTip.id = idPrefix + "regex-filter-tip";
        regexFilterTip.title = `[${name}] Number of scripts filtered by regex`;
    }
    function setRegexFilterTip(content) {
        if (regexFilterTip) {
            regexFilterTip.textContent = content;
        }
    }
    function regexFilterOne(regex, script) {
        const info = script.querySelector("article > h2");
        if (!info) return;
        const name = info.querySelector(".script-link").textContent;
        const result = regex.test(name);
        script.classList.toggle("regex-filtered", result);
        if (result) {
            log("Filtered:", name);
        }
        return result;
    }
    function regexFilter(regexStr) {
        const debug = configProxy["other.debug"];
        const scripts = $$(".script-list > li");
        if (regexStr === "" || scripts.length === 0) {
            scripts.forEach(script => script.classList.remove("regex-filtered"));
            setRegexFilterTip("");
            return;
        }
        const regex = new RegExp(regexStr, "i");
        let count = 0;
        debug && console.groupCollapsed(`[${name}] Regex filtered scripts`);
        scripts.forEach(script => {
            if (regexFilterOne(regex, script)) {
                count++;
            }
        });
        setRegexFilterTip(`Filtered: ${count}/${scripts.length}`);
        debug && console.groupEnd();
    }
    regexFilter(configProxy["filterAndSearch.regexFilter"]);
    // Search syntax
    const types = {
        "script": "scripts",
        "lib": "scripts/libraries",
        "library": "scripts/libraries",
        // "code": "scripts/code-search", // It uses a different search parameter `c` instead of `q`
        "user": "users"
    };
    const langs = {
        "js": "",
        "javascript": "",
        "css": "css",
        "any": "all",
        "all": "all"
    };
    const sorts = {
        "rel": "",
        "relevant": "",
        "relevance": "",
        "day": "daily_installs",
        "daily": "daily_installs",
        "daily_install": "daily_installs",
        "daily_installs": "daily_installs",
        "total": "total_installs",
        "total_install": "total_installs",
        "total_installs": "total_installs",
        "score": "ratings",
        "rate": "ratings",
        "rating": "ratings",
        "ratings": "ratings",
        "created": "created",
        "created_at": "created",
        "updated": "updated",
        "updated_at": "updated",
        "name": "name",
        "title": "name",
    };
    if (configProxy["filterAndSearch.searchSyntax"]) {
        function parseString(input) {
            // Regular expression to match key:value pairs, allowing for non-word characters in values
            const regex = /\b(\w+:[^\s]+)\b/g;
            // Extract all key:value pairs
            const pairs = input.match(regex) || [];
            // Remove the pairs from the input string
            const cleanedString = input.replace(regex, '').replace(/\s{2,}/g, ' ').trim();

            // Convert pairs to an object
            const parsedPairs = pairs.reduce((acc, pair) => {
                const [key, value] = pair.split(':');
                acc[key.toLowerCase()] = value.toLowerCase(); // Case-insensitive
                return acc;
            }, {});

            return { cleanedString, parsedPairs };
        }
        function processSearch(search) {
            const form = search.form;
            if (form.method !== "get") {
                return;
            }
            form.addEventListener("submit", (e) => {
                const { cleanedString, parsedPairs } = parseString(search.value);
                if (cleanedString === search.value) return;
                search.value = cleanedString;
                if (!parsedPairs) return;
                e.preventDefault();
                const url = new URL(form.action, window.location.href);
                url.searchParams.set("q", cleanedString);
                if (parsedPairs["site"]) { // site:site-name
                    url.pathname = `/scripts/by-site/${parsedPairs["site"]}`;
                } else if (parsedPairs["type"]) { // type:type, including "script", "lib"/"library", "code", "user"
                    const typeUrl = types[parsedPairs["type"]];
                    if (typeUrl) {
                        url.pathname = `/${typeUrl}`;
                    }
                }
                if (parsedPairs["lang"]) { // lang:language
                    const lang = langs[parsedPairs["lang"]];
                    if (lang === "") {
                        url.searchParams.delete("language");
                    } else if (lang) {
                        url.searchParams.set("language", lang);
                    }
                }
                if (parsedPairs["sort"]) { // sort:sort-by
                    const sort = sorts[parsedPairs["sort"]];
                    if (sort === "" || sort === "daily_installs" && cleanedString === "") {
                        url.searchParams.delete("sort");
                    } else if (sort) {
                        url.searchParams.set("sort", sort);
                    }
                }
                window.location.href = url.href;
            });
        }
        const searches = $$("input[type=search][name=q]");
        for (const search of searches) {
            processSearch(search);
        }
    }

    // Code blocks
    // Auto hide code blocks
    function autoHide() {
        if (!configProxy["codeblocks.autoHideCode"]) {
            for (const code_block of codeBlocks) {
                const toggle = code_block.firstChild.lastChild;
                if (!toggle) continue;
                if (toggle.textContent === "Show code") {
                    toggle.click(); // Click the toggle button
                }
            }
        } else {
            for (const codeBlock of codeBlocks) {
                const m = codeBlock.lastChild.textContent.match(/\n/g);
                const rows = m ? m.length : 0;
                const toggle = codeBlock.firstChild.lastChild;
                if (!toggle) continue;
                const hidden = toggle.textContent === "Show code";
                if (rows >= configProxy["codeblocks.autoHideRows"] && !hidden || rows < configProxy["codeblocks.autoHideRows"] && hidden) {
                    codeBlock.firstChild.lastChild.click(); // Click the toggle button
                }
            }
        }
    }
    document.addEventListener("readystatechange", (e) => {
        if (e.target.readyState === "complete") {
            autoHide();
        }
    }, { once: true });
    // Tab size
    function tabSize(value) {
        const style = $("style#" + idPrefix + "tab-size") ?? document.head.appendChild(document.createElement("style"));
        style.id = idPrefix + "tab-size";
        style.textContent = `pre { tab-size: ${value}; }`;
    }
    tabSize(configProxy["codeblocks.tabSize"]);
    // Metadata
    function extractUserScriptMetadata(code) {
        const result = {};
        const userScriptRegex = /\/\/\s*=+\s*UserScript\s*=+\s*([\s\S]*?)\s*=+\s*\/UserScript\s*=+\s*/;
        const match = code.match(userScriptRegex);
        if (match) {// If the UserScript block is found
            const content = match[1];// Extract the content within the UserScript block
            const lines = content.split('\n'); // Split the content by newline

            lines.forEach(line => {
                // Regular expression to match "// @name value" pattern
                const matchLine = line.trim().match(/^\/\/\s*@(\S+)\s+(.+)$/);
                if (matchLine) {
                    const name = matchLine[1]; // Extract the name
                    const value = matchLine[2]; // Extract the value
                    switch (typeof result[name]) {
                        case "undefined": // First occurrence
                            result[name] = value;
                            break;
                        case "string": // Second occurrence
                            result[name] = [result[name], value];
                            break;
                        case "object": // Third or more occurrence
                            result[name].push(value);
                            break;
                    }
                }
            });
        }
        return result;
    }
    function metadata(enable) {
        const id = idPrefix + "metadata";
        const current = document.getElementById(id);
        if (current && !enable) {
            current.remove();
        } else if (!current && enable) {
            const scriptCodeBlock = document.querySelector(".code-container > pre.prettyprint.lang-js");
            const description = $("div#script-content");
            if (!window.location.pathname.endsWith("/code") || !scriptCodeBlock || !description) return;
            const metaBlock = document.createElement("ul");
            description.prepend(metaBlock);
            metaBlock.id = id;
            const script = scriptCodeBlock.querySelector("ol") ? Array.from(scriptCodeBlock.querySelectorAll("ol > li")).map(li => li.textContent).join("\n") : scriptCodeBlock.textContent;
            const metadata = extractUserScriptMetadata(script);
            const commonHosts = {
                GreasyFork: /^https?:\/\/update\.greasyfork\.org\/scripts\/\d+\/(?<ver>\d+)\/(?<name>.+?)\.js$/,
                JsDelivr: /^https?:\/\/cdn\.jsdelivr\.net\/(?<reg>\w+)\/(@[^/]+\/)?(?<name>[^@]+)@(?<ver>[^/]+)/,
                Cloudflare: /^https?:\/\/cdnjs\.cloudflare\.com\/ajax\/libs\/(?<name>[^/]+)\/(?<ver>[^/]+)/,
            };
            const commonRegistries = {
                npm: "NPM",
                gh: "GitHub",
            };
            // We're interested in `@grant`, `@connect`, `@require`, `@resource`
            const interestedMetadata = {};
            const interestedKeys = {
                grant: {
                    brief: "Required permissions",
                    display: (value) => {
                        const valueCode = document.createElement("code");
                        valueCode.textContent = value;
                        if (value !== "none") {
                            const valueLink = document.createElement("a");
                            valueLink.appendChild(valueCode);
                            valueLink.href = `https://www.tampermonkey.net/documentation.php#api:${valueCode.textContent}`;
                            valueLink.title = `See documentation about ${valueCode.textContent}`;
                            return valueLink;
                        } else {
                            return valueCode;
                        }
                    }
                },
                connect: {
                    brief: "Allowed URLs to connect",
                    display: (value) => {
                        const valueCode = document.createElement("code");
                        valueCode.textContent = value;
                        return valueCode;
                    }
                },
                require: {
                    brief: "External libraries",
                    display: (value) => {
                        const valueLink = document.createElement("a");
                        valueLink.href = value;
                        valueLink.textContent = value;
                        for (const [host, regex] of Object.entries(commonHosts)) {
                            const match = value.match(regex);
                            if (match) {
                                const { name, ver, reg } = match.groups;
                                const optionalRegistry = commonRegistries[reg] ? `${commonRegistries[reg]} on ` : "";
                                valueLink.textContent = `${decodeURIComponent(name)}@${ver} (${optionalRegistry}${host})`;
                                break;
                            }
                        }
                        return valueLink;
                    }
                },
                resource: {
                    brief: "External resources",
                    display: (value) => {
                        const valueCode = document.createElement("code");
                        const [name, link] = value.split(" ");
                        const valueLink = document.createElement("a");
                        valueLink.appendChild(valueCode);
                        valueLink.href = link.trim();
                        valueCode.textContent = name.trim();
                        return valueLink;
                    }
                }
            };
            for (const key in interestedKeys) {
                const values = metadata[key] ?? [];
                interestedMetadata[key] = Array.isArray(values) ? values : [values];
            }
            log("Interested Metadata:", interestedMetadata);
            // Display
            for (const [key, values] of Object.entries(interestedMetadata)) {
                const keyInfo = interestedKeys[key];
                const li = metaBlock.appendChild(document.createElement("li"));
                const keyLink = li.appendChild(document.createElement("a"));
                keyLink.href = `https://www.tampermonkey.net/documentation.php#meta:${key}`;
                keyLink.title = keyInfo.brief;
                keyLink.textContent = `@${key}`;
                const separator = li.appendChild(document.createElement("span"));
                separator.textContent = ": ";
                for (const value of values) {
                    li.appendChild(keyInfo.display(value));
                    const separator = li.appendChild(document.createElement("span"));
                    separator.textContent = ", ";
                }
                if (values.length > 0) {
                    li.lastChild.remove(); // Remove the last separator
                } else {
                    li.appendChild(document.createTextNode("none"));
                }
            }
        }
    }
    metadata(configProxy["codeblocks.metadata"]);

    // Display
    // Flat layout
    function flatLayout(enable) {
        const meta_orig = $("#script-info > #script-content .script-meta-block");
        const meta_mod = $("#script-info > .script-meta-block");
        if (enable && meta_orig) {
            const header = $("#script-info > header");
            header.before(meta_orig);
        } else if (!enable && meta_mod) {
            const additional = $("#script-info > #script-content #additional-info");
            additional.before(meta_mod);
        }
    }
    flatLayout(configProxy["display.flatLayout"]);
    // Always show notification
    function alwaysShowNotification(enable) {
        const nav = $("#nav-user-info");
        const profile = nav?.querySelector(".user-profile-link");
        const existing = nav.querySelector(".notification-widget");
        if (!nav || !profile || existing && existing.textContent !== "0") return; // There's unread notification or user is not logged in
        if (enable && !existing) {
            const notification = nav.insertBefore(document.createElement("a"), profile);
            notification.className = "notification-widget";
            notification.textContent = "0";
            notification.href = profile.querySelector("a").href + "/notifications";
        } else if (!enable && existing) {
            existing.remove();
        }
    }
    alwaysShowNotification(configProxy["display.alwaysShowNotification"]);

    // Other
    // 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(configProxy["other.shortLink"]);
    // 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(configProxy["other.libAlternativeUrl"]);
    // Image proxy
    if (configProxy["other.imageProxy"]) {
        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;
        }
    }
    // Lazy image
    if (configProxy["other.lazyImage"]) {
        const images = $$(".user-content img");
        for (const image of images) {
            image.loading = "lazy";
        }
    }

    // Initialize css
    for (const prop in dynamicStyle) {
        cssHelper(prop, configProxy[prop]);
    }
    // Dynamically respond to config changes
    const callbacks = {
        "filterAndSearch.shortcut": shortcut,
        "filterAndSearch.regexFilter": regexFilter,
        "codeblocks.autoHideCode": autoHide,
        "codeblocks.autoHideRows": autoHide,
        "codeblocks.tabSize": tabSize,
        "codeblocks.metadata": metadata,
        "display.flatLayout": flatLayout,
        "display.alwaysShowNotification": alwaysShowNotification,
        "other.shortLink": shortLink,
        "other.libAlternativeUrl": alternativeURLs,
    };
    config.addEventListener("set", e => {
        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);
        }
    });
})();