Greasy Fork++

Adds various features and improves the Greasy Fork experience

// ==UserScript==
// @name               Greasy Fork++
// @namespace          https://github.com/iFelix18
// @version            3.2.52
// @author             CY Fung <https://greasyfork.org/users/371179> & Davide <iFelix18@protonmail.com>
// @icon               https://www.google.com/s2/favicons?domain=https://greasyfork.org
// @description        Adds various features and improves the Greasy Fork experience
// @description:de     Fügt verschiedene Funktionen hinzu und verbessert das Greasy Fork-Erlebnis
// @description:es     Agrega varias funciones y mejora la experiencia de Greasy Fork
// @description:fr     Ajoute diverses fonctionnalités et améliore l'expérience Greasy Fork
// @description:it     Aggiunge varie funzionalità e migliora l'esperienza di Greasy Fork
// @description:ru     Добавляет различные функции и улучшает работу с Greasy Fork
// @description:zh-CN  添加各种功能并改善 Greasy Fork 体验
// @description:zh-TW  加入多種功能並改善Greasy Fork的體驗
// @description:ja     Greasy Forkの体験を向上させる様々な機能を追加
// @description:ko     Greasy Fork 경험을 향상시키고 다양한 기능을 추가
// @copyright          2023, CY Fung (https://greasyfork.org/users/371179); 2021, Davide (https://github.com/iFelix18)
// @license            MIT
// @require            https://fastly.jsdelivr.net/gh/sizzlemctwizzle/GM_config@06f2015c04db3aaab9717298394ca4f025802873/gm_config.min.js
// @require            https://fastly.jsdelivr.net/npm/@violentmonkey/shortcut@1.4.1/dist/index.min.js
// @require            https://fastly.jsdelivr.net/gh/cyfung1031/userscript-supports@3fa07109efca28a21094488431363862ccd52d7c/library/WinComm.min.js
// @match              *://greasyfork.org/*
// @match              *://sleazyfork.org/*
// @connect            greasyfork.org
// @compatible         chrome
// @compatible         edge
// @compatible         firefox
// @compatible         safari
// @compatible         brave
// @grant              GM.deleteValue
// @grant              GM.getValue
// @grant              GM.notification
// @grant              GM.registerMenuCommand
// @grant              GM.setValue
// @grant              unsafeWindow
// @run-at             document-start
// @inject-into        content
// ==/UserScript==

/* global GM_config, VM, GM, WinComm */

/**
 * @typedef { typeof import("./library/WinComm.js")  } WinComm
 */

// console.log(GM)

/** @type {WinComm} */
const WinComm = this.WinComm;

//  -------- UU Fucntion - original code: https://fastly.jsdelivr.net/npm/@ifelix18/utils@6.5.0/lib/index.min.js  --------
// optimized by CY Fung to remove $ dependency and observe creation
const UU = (function () {
    const scriptName = GM.info.script.name; // not name_i18n
    const scriptVersion = GM.info.script.version;
    const authorMatch = /^(.*?)\s<\S[^\s@]*@\S[^\s.]*\.\S+>$/.exec(GM.info.script.author);
    const author = authorMatch ? authorMatch[1] : GM.info.script.author;
    let scriptId = scriptName.toLowerCase().replace(/\s/g, "-");
    let loggingEnabled = false;

    const log = (message) => {
        if (loggingEnabled) {
            console.log(`${scriptName}:`, message);
        }
    };

    const error = (message) => {
        console.error(`${scriptName}:`, message);
    };

    const warn = (message) => {
        console.warn(`${scriptName}:`, message);
    };

    const alert = (message) => {
        window.alert(`${scriptName}: ${message}`);
    };

    /** @param {string} text */
    const short = (text, length) => {
        const s = text.split(" ");
        const l = Number(length);
        return s.length > l
            ? `${s.slice(0, l).join(" ")} [...]`
            : text;
    };

    const addStyle = (css) => {
        const head = document.head || document.querySelector("head");
        const style = document.createElement("style");
        style.textContent = css;
        head.appendChild(style);
    };

    const init = async (options = {}) => {
        scriptId = options.id || scriptId;
        loggingEnabled = typeof options.logging === "boolean" ? options.logging : false;
        console.info(
            `%c${scriptName}\n%cv${scriptVersion}${author ? ` by ${author}` : ""} is running!`,
            "color:red;font-weight:700;font-size:18px;text-transform:uppercase",
            ""
        );
    };

    return {
        init,
        log,
        error,
        warn,
        alert,
        short,
        addStyle
    };
})();

//  -------- UU Fucntion - original code: https://fastly.jsdelivr.net/npm/@ifelix18/utils@6.5.0/lib/index.min.js  --------


const mWindow = (() => {


    const fields = {
        hideBlacklistedScripts: {
            label: 'Hide blacklisted scripts:<br><span>Choose which lists to activate in the section below, press <b>Ctrl + Alt + B</b> to show Blacklisted scripts</span>',
            section: ['Features'],
            labelPos: 'right',
            type: 'checkbox',
            default: true
        },
        hideHiddenScript: {
            label: 'Hide scripts:<br><span>Add a button to hide the script<br>See and edit the list of hidden scripts below, press <b>Ctrl + Alt + H</b> to show Hidden script',
            labelPos: 'right',
            type: 'checkbox',
            default: true
        },
        showInstallButton: {
            label: 'Install button:<br><span>Add to the scripts list a button to install the script directly</span>',
            labelPos: 'right',
            type: 'checkbox',
            default: true
        },
        showTotalInstalls: {
            label: 'Installations:<br><span>Shows the number of daily and total installations on the user profile</span>',
            labelPos: 'right',
            type: 'checkbox',
            default: true
        },
        milestoneNotification: {
            label: 'Milestone notifications:<br><span>Get notified whenever your total installs got over any of these milestone<br>Separate milestones with a comma, leave blank to turn off notifications</span>',
            labelPos: 'left',
            type: 'text',
            title: 'Separate milestones with a comma!',
            size: 150,
            default: '10, 100, 500, 1000, 2500, 5000, 10000, 100000, 1000000'
        },
        nonLatins: {
            label: 'Non-Latin:<br><span>This list blocks all scripts with non-Latin characters in the title/description</span>',
            section: ['Lists'],
            labelPos: 'right',
            type: 'checkbox',
            default: false // not true
        },
        blacklist: {
            label: 'Blacklist:<br><span>A "non-opinionable" list that blocks all scripts with specific words in the title/description, references to "bots", "cheats" and some online game sites, and other "bullshit"</span>',
            labelPos: 'right',
            type: 'checkbox',
            default: true
        },
        customBlacklist: {
            label: 'Custom Blacklist:<br><span>Personal blacklist defined by a set of unwanted words<br>Separate unwanted words with a comma (example: YouTube, Facebook, pizza), leave blank to disable this list</span>',
            labelPos: 'left',
            type: 'text',
            title: 'Separate unwanted words with a comma!',
            size: 150,
            default: ''
        },
        hiddenList: {
            label: 'Hidden Scripts:<br><span>Block individual undesired scripts by their unique IDs<br>Separate IDs with a comma</span>',
            labelPos: 'left',
            type: 'textarea',
            title: 'Separate IDs with a comma!',
            default: '',
            save: false
        },
        logging: {
            label: 'Logging',
            section: ['Developer options'],
            labelPos: 'right',
            type: 'checkbox',
            default: false
        },
        debugging: {
            label: 'Debugging',
            labelPos: 'right',
            type: 'checkbox',
            default: false
        }
    }

    const logo = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAASFBMVEVHcEwBAQEDAwMAAAACAgIBAQEAAAAREREDAwMBAQH///8WFhYuLi7U1NSdnZ1bW1vExMTq6uqtra309PRERETf399ycnKGhoaVOQEOAAAACnRSTlMAg87/rjLgE1rzhWrqxgAABexJREFUaN61WouSpCAMVPEJKCqi//+nF4IKKig6e1SduzfupEkT8oIkiRlVVdRpnmdlQ0hTZnme1kVV4Zvk96Fla8nH0ZSI8rP0Ks2uwi1Ilv4EURW5K5xS0slhMb/BkD0hrMk/q1HVeSP6QVILMFIY8wagn6ojTV5Xn8RnbFZaoAPQc9bR3gXQ/yaWvYYA8VfKKeXACZVnAE1V9o4on/izWPsb/q9Ji3j5OcrjhiCXohsAQso6lh6QL9qOEd6GAAbKYAInAFAiiqYC5LMeLIaFKeppR3h/BiAkj6CpLuEPmbbHngUBhFZsdAGiaUL5xLBzRrAAZBlk5wpnVJEohHTbuZoAD0uhMUu+uY/bLZHaryBCH4vQCuugbnSoYf5sk+llKWaEETT/Qu2TecmSHaF1KPT6gmkM4hNLLkIR2l/guAZK1fQrS3kVXmChEX5mKb0xICH/gKXrQtf2pbhlyfqFoL/1LUOVEbFwcsuSs5GfAcjJ8dVkknbafpYUfUXSQYWqRP81THcs8fbVMmTVaQU6ENNOdyxNgGRYmFsp2/mQaFiKzGeC1IcVmAjrDjq4LAF9RgdF13CAo3cTDRcAP2OOCjX6UAwCPpbWyGZsCWTMAM0YTGF2Eg0XAD8bramue9jocGVpi5y7LbUUVRO0dRINF2D9bN/PBSqgAizt8gHByJAUddEyTqa7rYF57oZkkgiYj48lYeVTuuh4Hw1A8pWhxr68snQYioOxHSm6A2gq1wuZz68suUMKELst8oCLfAew+rzMecmOLO251wYwa4CDmd4B8GyPM1YDlyXeUp8Gx412A9Chy6vP9cXO0kW+5e6N104vH68sXeW/jwzptss8OihFf1UAY2dVkgDCdQz8dfiv1m3sZek62rcIsJlr/5uADv1bhNqzxrcIb3VIkzz06m9YykMAM39kidIoAG+5R7icHlm6BViUVDqSZknpfd8NZh2MO1Xz+JKlcYsfZeK3UqjBTDRexn680PVoSxMFBiCST6RJJmXzg2FTegaPzyRWRWu9cERAHW4o6jANmPU0Ewwqe36wa8j1wyQLADHyk1FphM760H1sBY/+PtS5ECQTvucHynoapYPiZJKFDoSNnFxZYl0QYG2gQExtcJFN8LNl1voHOA++5yQelh5yVPhRopma8M3OALMO8p0GhgDT+lgKDatBhhvN5gcuRWaZJeQ8CzVBLmBLd2tgdrLND9xFxh9CW8JABYRSNQVYugJYK8rB2bn5gWOmaM4dzmXQVjvuidMzS3YfpEm9uPnBtp5yNFRJLRUTb9OaiN1x+06uk0q4+cG+U+SqCeoKLmMwrYkp1pYWRbUvgoDjDZng7EScG3/wSxAyK7+/Xvrgl974JZ1gp69r1Bc7LvUlXhEIsSxh4lWU5Ecdwixh6lhlhPwvlkyZlpIvCFEspW4B8h9YWguQYOZynzZEsJTvRWBPxwDABnKuXWJY2ovAKu8H9h7gkSXblqqFIB8AHlhyekbGUk2PYUbXtvgAXGnYjfWwNA+QcDHN3+x2Q2rngENgiSeeAUZfjDMVHkSn1m2GGBVwCh0d8NlfhJ4owiyE+VjiPV0WKQ7tHCxD1h6DeQ7PAMKWvUcERtt2PDakkio9f/1pkdcsxMOSLq7ldD5LAJf3BeCaCfQmDl57s/Xak4sHEJiPjOcdN4f61+n8CDDQaX/iIk8KcrOTDqCC4Km3tdw9AeBM1+dq1IqRE0stI8LbWk6K7AmAjYPeX/jEdF/qJtgpX+pDzfH9eCVunFyt1UEQUt8dUHwE2BE6b2f8A8I1WMxqGLQfyqu7I8zmOwBh08TJrfy36+ANw1XcQdrHEXOeWeTf5edRJ7JV+t/o+UKTc+hRxx8oF+lLaxKCvTmw1vcRshcAbGFZ8eFUv4kF4NnHewn5pM91sauv7z9gumDPPNgoobBq54/XHraLGyAZXPLqaFrnzIMpKoeR/3BxY7t6woWY2hYqZZ0u2DOPeZzZr1dP7OUZbk4MVE+wecrmqcn+5vLMevsneP3ncfwDNtu0vRpuz80AAAAASUVORK5CYII='

    const locales = { /* cSpell: disable */
        de: {
            downgrade: 'Auf zurückstufen',
            hide: '❌ Dieses skript ausblenden',
            install: 'Installieren',
            notHide: '✔️ Dieses skript nicht ausblenden',
            milestone: 'Herzlichen Glückwunsch, Ihre Skripte haben den Meilenstein von insgesamt $1 Installationen überschritten!',
            reinstall: 'Erneut installieren',
            update: 'Auf aktualisieren'
        },
        en: {
            downgrade: 'Downgrade to',
            hide: '❌ Hide this script',
            install: 'Install',
            notHide: '✔️ Not hide this script',
            milestone: 'Congrats, your scripts got over the milestone of $1 total installs!',
            reinstall: 'Reinstall',
            update: 'Update to'
        },
        es: {
            downgrade: 'Degradar a',
            hide: '❌ Ocultar este script',
            install: 'Instalar',
            notHide: '✔️ No ocultar este script',
            milestone: '¡Felicidades, sus scripts superaron el hito de $1 instalaciones totales!',
            reinstall: 'Reinstalar',
            update: 'Actualizar a'
        },
        fr: {
            downgrade: 'Revenir à',
            hide: '❌ Cacher ce script',
            install: 'Installer',
            notHide: '✔️ Ne pas cacher ce script',
            milestone: 'Félicitations, vos scripts ont franchi le cap des $1 installations au total!',
            reinstall: 'Réinstaller',
            update: 'Mettre à'
        },
        it: {
            downgrade: 'Riporta a',
            hide: '❌ Nascondi questo script',
            install: 'Installa',
            notHide: '✔️ Non nascondere questo script',
            milestone: 'Congratulazioni, i tuoi script hanno superato il traguardo di $1 installazioni totali!',
            reinstall: 'Reinstalla',
            update: 'Aggiorna a'
        },
        ru: {
            downgrade: 'Откатить до',
            hide: '❌ Скрыть этот скрипт',
            install: 'Установить',
            notHide: '✔️ Не скрывать этот сценарий',
            milestone: 'Поздравляем, ваши скрипты преодолели рубеж в $1 установок!',
            reinstall: 'Переустановить',
            update: 'Обновить до'
        },
        'zh-CN': {
            downgrade: '降级到',
            hide: '❌ 隐藏此脚本',
            install: '安装',
            notHide: '✔️ 不隐藏此脚本',
            milestone: '恭喜,您的脚本超过了 $1 次总安装的里程碑!',
            reinstall: '重新安装',
            update: '更新到'
        },
        'zh-TW': {
            downgrade: '降級至',
            hide: '❌ 隱藏此腳本',
            install: '安裝',
            notHide: '✔️ 不隱藏此腳本',
            milestone: '恭喜,您的腳本安裝總數已超過 $1!',
            reinstall: '重新安裝',
            update: '更新至'
        },
        'ja': {
            downgrade: 'ダウングレードする',
            hide: '❌ このスクリプトを隠す',
            install: 'インストール',
            notHide: '✔️ このスクリプトを隠さない',
            milestone: 'おめでとうございます、あなたのスクリプトの合計インストール回数が $1 を超えました!',
            reinstall: '再インストール',
            update: '更新する'
        },
        'ko': {
            downgrade: '다운그레이드하기',
            hide: '❌ 이 스크립트 숨기기',
            install: '설치',
            notHide: '✔️ 이 스크립트 숨기지 않기',
            milestone: '축하합니다, 스크립트의 총 설치 횟수가 $1을 넘었습니다!',
            reinstall: '재설치',
            update: '업데이트하기'
        }

    };

    const blacklist = [
        '\\bagar((\\.)?io)?\\b', '\\bagma((\\.)?io)?\\b', '\\baimbot\\b', '\\barras((\\.)?io)?\\b', '\\bbot(s)?\\b',
        '\\bbubble((\\.)?am)?\\b', '\\bcheat(s)?\\b', '\\bdiep((\\.)?io)?\\b', '\\bfreebitco((\\.)?in)?\\b', '\\bgota((\\.)?io)?\\b',
        '\\bhack(s)?\\b', '\\bkrunker((\\.)?io)?\\b', '\\blostworld((\\.)?io)?\\b', '\\bmoomoo((\\.)?io)?\\b', '\\broblox(\\.com)?\\b',
        '\\bshell\\sshockers\\b', '\\bshellshock((\\.)?io)?\\b', '\\bshellshockers\\b', '\\bskribbl((\\.)?io)?\\b', '\\bslither((\\.)?io)?\\b',
        '\\bsurviv((\\.)?io)?\\b', '\\btaming((\\.)?io)?\\b', '\\bvenge((\\.)?io)?\\b', '\\bvertix((\\.)?io)?\\b', '\\bzombs((\\.)?io)?\\b',
        // '\\p{Extended_Pictographic}'
    ];


    const settingsCSS = `

        /*
        #greasyfork-plus label::before {
        content:'';
        display:block;
        position:absolute;
        left:0;
        right:0;
        top:0;
        bottom:0;
        z-index:1;
        }
        #greasyfork-plus label {
        position:relative;
        z-index:0;
        }
        */

        html {
        color: #222;
        background: #f9f9f9;
        }

        #greasyfork-plus{
            --config-var-display: flex;
        }
        #greasyfork-plus * {
            font-family:Open Sans,sans-serif,Segoe UI Emoji !important;
            font-size:12px
        }
        #greasyfork-plus .section_header[class] {
            background-color:#670000;
            background-image:linear-gradient(#670000,#900);
            border:1px solid transparent;
            color:#fff
        }
        #greasyfork-plus .field_label[class]{
            margin-bottom:4px
        }
        #greasyfork-plus .field_label[class] span{
            font-size:95%;
            font-style:italic;
            opacity:.8;
        }
        #greasyfork-plus .field_label[class] b{
            color:#670000
        }
        #greasyfork-plus_logging_var[class],
        #greasyfork-plus_debugging_var[class] {
            --config-var-display: inline-flex;
        }
        #greasyfork-plus #greasyfork-plus_logging_var label.field_label[class],
        #greasyfork-plus #greasyfork-plus_debugging_var label.field_label[class] {
            margin-bottom:0;
            align-self: center;
        }
        #greasyfork-plus .config_var[class]{
            display:var(--config-var-display);
            position: relative;
        }
        #greasyfork-plus_customBlacklist_var[class],
        #greasyfork-plus_hiddenList_var[class],
        #greasyfork-plus_milestoneNotification_var[class]{
            flex-direction:column;
            margin-left:21px;
        }

        #greasyfork-plus_customBlacklist_var[class]::before,
        #greasyfork-plus_hiddenList_var[class]::before,
        #greasyfork-plus_milestoneNotification_var[class]::before{
            /* content: "◉"; */
            content: "◎";
            position: absolute;
            left: auto;
            top: auto;
            margin-left: -16px;
        }
        #greasyfork-plus_field_customBlacklist[class],
        #greasyfork-plus_field_milestoneNotification[class]{
            flex:1;
        }
        #greasyfork-plus_field_hiddenList[class]{
            box-sizing:border-box;
            overflow:hidden;
            resize:none;
            width:100%
        }

        body > #greasyfork-plus_wrapper:only-child {
            box-sizing: border-box;
            overflow: auto;
            max-height: calc(100vh - 72px);
            padding: 12px;
            /* overflow: auto; */
            scrollbar-gutter: both-edges;
            background: rgba(127,127,127,0.05);
            border: 1px solid rgba(127,127,127,0.5);
        }

        #greasyfork-plus_wrapper > #greasyfork-plus_buttons_holder:last-child {
            position: fixed;
            bottom: 0;
            right: 0;
            margin: 0 12px 6px 0;
        }

        #greasyfork-plus .saveclose_buttons[class] {
            padding: 4px 14px;
            margin: 6px;
        }
        #greasyfork-plus .section_header_holder#greasyfork-plus_section_2[class] {
            position: fixed;
            left: 0;
            bottom: 0;
            margin: 8px;
        }
        #greasyfork-plus .section_header#greasyfork-plus_section_header_2[class] {
            background: #000;
            color: #eee;
        }

        #greasyfork-plus_header[class]{
            font-size: 16pt;
            font-weight: bold;
        }

    `;

    const pageCSS = `

        .script-list li.blacklisted{
            display:none;
            background:#321919;
            color:#e8e6e3
        }
        .script-list li.hidden{
            display:none;
            background:#321932;
            color:#e8e6e3
        }
        .script-list li.blacklisted a:not(.install-link),.script-list li.hidden a:not(.install-link){
            color:#ff8484
        }
        #script-info.hidden,#script-info.hidden .user-content{
            background:#321932;
            color:#e8e6e3
        }
        #script-info.hidden a:not(.install-link):not(.install-help-link){
            color:#ff8484
        }
        #script-info.hidden code{
            background-color:transparent
        }
        html {
            --block-btn-color:#111;
            --block-btn-bgcolor:#eee;
        }
        #script-info.hidden, #script-info.hidden .user-content {
            --block-btn-color:#eee;
            --block-btn-bgcolor:#111;
        }

        [style-54998]{
            float:right;
            font-size: 70%;
            text-decoration:none;
        }

        [style-16377]{
            cursor:pointer;
            font-size:70%;
            white-space:nowrap;
            border: 1px solid #888;
            background: var(--block-btn-bgcolor, #eee);
            color: var(--block-btn-color);
            border-radius: 4px;
            padding: 0px 6px;
            margin: 0 8px;
        }
        [style-77329] {
            cursor: pointer;
            margin-left: 1ex;
            white-space: nowrap;
            float: right;
            border: 1px solid #888;
            background: var(--block-btn-bgcolor, #eee);
            color: var(--block-btn-color);
            border-radius: 4px;
            padding: 0px 6px;
        }

        a#hyperlink-35389,
        a#hyperlink-40361,
        a#hyperlink-35389:visited,
        a#hyperlink-40361:visited,
        a#hyperlink-35389:hover,
        a#hyperlink-40361:hover,
        a#hyperlink-35389:focus,
        a#hyperlink-40361:focus,
        a#hyperlink-35389:active,
        a#hyperlink-40361:active {

            border: none !important;
            outline: none !important;
            box-shadow: none !important;
            appearance: none !important;
            background: none !important;
            color:inherit !important;
        }

        a#hyperlink-35389{
            opacity: var(--hyperlink-blacklisted-option-opacity);

        }
        a#hyperlink-40361{
            opacity: var(--hyperlink-hidden-option-opacity);
        }


        html {

            --hyperlink-blacklisted-option-opacity: 0.5;
            --hyperlink-hidden-option-opacity: 0.5;
        }


        .list-option.list-current[class] > a[href] {

            text-decoration:none;
        }

        html {
            --blacklisted-display: none;
            --hidden-display: none;
        }

        [blacklisted-shown] {
            --blacklisted-display: list-item;
            --hyperlink-blacklisted-option-opacity: 1;
        }
        [hidden-shown] {
            --hidden-display: list-item;
            --hyperlink-hidden-option-opacity: 1;
        }

        .script-list li.blacklisted{
            display: var(--blacklisted-display);

        }

        .script-list li.hidden{
            display: var(--hidden-display);

        }

        .install-link.install-status-checking,
        .install-link.install-status-checking:visited,
        .install-link.install-status-checking:active,
        .install-link.install-status-checking:hover,
        .install-help-link.install-status-checking {
            background-color: #405458;
        }

        div.previewable{
            display: flex;
            flex-direction: column;
        }
        .script-version-ainfo-span {
            align-self:end;
            font-size: 90%;
            padding: 4px 8px;
            margin: 0;
        }
        [style*="display:"] + .script-version-ainfo-span{
            display: none;
        }


        /* Greasy Fork Enhance - Flat Layout  */

        [greasyfork-enhance-k37*="|flat-layout|"] ol.script-list > li > article > h2 {
            width: 0;
            flex-grow: 1;
            flex-basis: 60%;
        }

        [greasyfork-enhance-k37*="|flat-layout|"] ol.script-list > li > article > div.script-meta-block {
            width: auto;
            flex-basis: 40%;
            flex-shrink: 0;
            flex-grow: 0;
        }

        [greasyfork-enhance-k37*="|flat-layout|"] .script-list li:not(.ad-entry) {
            padding: 1em;
            margin: 0;
        }

        [greasyfork-enhance-k37*="|flat-layout|"] .script-list li:not(.ad-entry) article {
            padding: 0;
            margin: 0;
        }

        [greasyfork-enhance-k37*="|flat-layout|"]  #script-info div.script-meta-block + #additional-info {

            max-width: calc( 100% - 340px );
            min-height: 300px;
            box-sizing: border-box;
        }

         [greasyfork-enhance-k37*="|basic|"] ul.outline {
            margin-bottom: -99vh;

         }

         .discussion-list .hidden {
            display: none;
         }


    `

    const window = {};

    /** @param {typeof WinComm.createInstance} createInstance */
    function contentScriptText(shObject, createInstance) {

        // avoid setupEthicalAdsFallback looping
        if (typeof window.ethicalads === "undefined") {
            const p = Promise.resolve([]);
            window.ethicalads = { wait: p };
        }

        /*
         *

        return new Promise((resolve, reject) => {
          const external = unsafeWindow.external;
          console.log(334, external)
          const scriptHandler = GM.info.scriptHandler;
          if (external && external.Violentmonkey && (scriptHandler || 'Violentmonkey') === 'Violentmonkey' ) {
            external.Violentmonkey.isInstalled(name, namespace).then((data) => resolve(data));
            return;
          }

          if (external && external.Tampermonkey && (scriptHandler || 'Tampermonkey') === 'Tampermonkey') {
            external.Tampermonkey.isInstalled(name, namespace, (data) => {
              (data.installed) ? resolve(data.version) : resolve();
            });
            return;
          }

          resolve();
        });

        */

        if (document.querySelector('#greasyfork-enhance-basic')) {



            const setScriptOnDisabled = async (style) => {

                try {
                    const pd = Object.getOwnPropertyDescriptor(style.constructor.prototype, 'disabled');
                    const { get, set } = pd;
                    Object.defineProperty(style, 'disabled', {
                        get() {
                            return get.call(this);
                        },
                        set(nv) {
                            let r = set.call(this, nv);
                            Promise.resolve().then(chHead);
                            return r;
                        }
                    })
                } catch (e) {

                }
            };

            document.addEventListener('style-s48', function (evt) {
                const target = (evt || 0).target || 0;
                if (!target) return;
                setScriptOnDisabled(target)

            }, true);


            const isScriptEnabled = (style) => {

                if (style instanceof HTMLStyleElement) {
                    if (!style.hasAttribute('s48')) {
                        style.setAttribute('s48', '');
                        style.dispatchEvent(new CustomEvent('style-s48'));
                        // setScriptOnDisabled(style);
                    }
                    return style.disabled !== true;
                }
                return false;
            }
            const chHead = () => {
                let p = [];
                if (isScriptEnabled(document.getElementById('greasyfork-enhance-basic')))
                    p.push('basic');
                if (isScriptEnabled(document.getElementById('greasyfork-enhance-flat-layout')))
                    p.push('flat-layout');
                if (isScriptEnabled(document.getElementById('greasyfork-enhance-animation')))
                    p.push('animation');
                if (p.length >= 1)
                    document.documentElement.setAttribute('greasyfork-enhance-k37', `|${p.join('|')}|`);
                else
                    document.documentElement.removeAttribute('greasyfork-enhance-k37');
            }
            const moHead = new MutationObserver(chHead);
            moHead.observe(document.head, { subtree: false, childList: true });
            chHead();

            /*
            const outline = document.querySelector('aside.panel > ul.outline');
            if(outline) {
              const div = document.createElement('div');
              //outline.replaceWith(div);
              //div.appendChild(outline)
            }
            */

            //         Promise.resolve().then(()=>{
            //           let outline = document.querySelector('[greasyfork-enhance-k37*="|basic|"] header + aside.panel ul.outline');
            //           if(outline){
            //             let aside = outline.closest('aside.panel');
            //           let header = aside.parentNode.querySelector('header');
            //             let p = header.getBoundingClientRect().height;

            //             document.body.parentNode.insertBefore(aside, document.body);
            //             // outline.style.top='0'
            //             p+=(parseFloat(getComputedStyle(outline).marginTop.replace('px',''))||0)
            //             outline.style.marginTop= p.toFixed(2)+'px';
            //           }

            //         })

        }



        const { scriptHandler, scriptName, scriptVersion, scriptNamespace, communicationId } = shObject;

        const wincomm = createInstance(communicationId);

        const external = window.external;

        if (external[scriptHandler]) 1;
        else if (external && external.Violentmonkey && (scriptHandler || 'Violentmonkey') === 'Violentmonkey') scriptHandler = 'Violentmonkey';
        else if (external && external.Tampermonkey && (scriptHandler || 'Tampermonkey') === 'Tampermonkey') scriptHandler = 'Tampermonkey';

        const manager = external[scriptHandler];

        if (!manager) {

            wincomm.send('userScriptManagerNotDetected', {
                code: 1
            });
            return;

        }

        const promiseWrap = (x) => {
            // bug in FireFox + Violentmonkey
            if (typeof (x || 0) === 'object' && typeof x.then === 'function') return x; else return Promise.resolve(x);
        };


        const pnIsInstalled2 = (type, scriptName, scriptNamespace) => new Promise((resolve, reject) => {
            const resultPr = promiseWrap(manager.isInstalled(scriptName, scriptNamespace));
            resultPr.then((result) => resolve({
                type,
                result: typeof result === 'string' ? { version: result } : result
            })).catch(reject);
        }).catch(console.warn);


        const pnIsInstalled3 = (type, scriptName, scriptNamespace) => new Promise((resolve, reject) => {
            try {
                manager.isInstalled(scriptName, scriptNamespace, (result) => {
                    resolve({
                        type,
                        result: typeof result === 'string' ? { version: result } : result
                    });
                });
            } catch (e) {
                reject(e);
            }
        }).catch(console.warn);



        const enableScriptInstallChecker = (r) => {

            const { type, result } = r;
            let version = result.version;
            // console.log(type, result, version)
            if (version !== scriptVersion) return;

            const pnIsInstalled = type < 25 ? pnIsInstalled2 : pnIsInstalled3;

            wincomm.hook('_$GreasyFork$Msg$OnScriptInstallCheck', {

                'installedVersion.req': (d, evt) => {
                    pnIsInstalled(type, d.data.name, d.data.namespace).then((r) => {
                        if (r && 'result' in r) {
                            wincomm.response(evt, 'installedVersion.res', {
                                version: r.result ? (r.result.version || '') : ''
                            });
                        }
                    })
                }

            });

            wincomm.send('ready', { type });

            // console.log('enableScriptInstallChecker', r)


        }

        const kl = manager.isInstalled.length;

        if (!(kl === 2 || kl === 3)) return;
        const puds = kl === 2 ? [
            pnIsInstalled2(21, scriptName, scriptNamespace), // scriptName is GM.info.script.name not GM.info.script.name_i18n
            pnIsInstalled2(20, scriptName, '')
        ] : [
            pnIsInstalled3(31, scriptName, scriptNamespace),
            pnIsInstalled3(30, scriptName, '')
        ];

        Promise.all(puds).then((rs) => {
            const [r1, r0] = rs;
            if (r0 && r0.result && r0.result.version) enableScriptInstallChecker(r0); // '3.1.4'
            else if (r1 && r1.result && r1.result.version) enableScriptInstallChecker(r1);
        });



        // console.log(327, shObject, scriptHandler);

    }



    return { fields, logo, locales, blacklist, settingsCSS, pageCSS, contentScriptText }



})();

(async () => {

    let rafPromise = null;

    const getRafPromise = () => rafPromise || (rafPromise = new Promise(resolve => {
        requestAnimationFrame(hRes => {
            rafPromise = null;
            resolve(hRes);
        });
    }));

    const isVaildURL = (url) => {
        if (!url || typeof url !== 'string' || url.length < 23) return;
        let obj = null;
        try {
            obj = new URL(url);
        } catch (e) {
            return false;
        }
        if (obj && obj.host === obj.hostname && !obj.port && (obj.protocol || '').startsWith('http') && obj.pathname) {
            return true;
        }
        return false;
    };

    const installLinkPointerDownHandler = function (e) {
        if (!e || !e.isTrusted) return;
        const button = e.target || this;
        if (button.hasAttribute('acnmd')) return;
        const href = button.href;
        if (!href || !isVaildURL(href)) return;
        if (/\.js[^-.\w\d\s:\/\\]*$/.test(href)) {
            0 && fetch(href, {
                method: "GET",
                cache: 'reload',
                redirect: "follow"
            }).then(() => {
                console.debug('code url reloaded', href);
            }).catch((e) => {
                console.debug(e);
            });
            const m = /^(https\:\/\/(greasyfork|sleazyfork)\.org\/[_-\w\/]*scripts\/(\d+)[-\w%]*)(\/|$)/.exec(location.href)
            if (m && m[1]) {
                const href = `${m[1]}/code`
                0 && fetch(href, {
                    method: "GET",
                    cache: 'reload',
                    redirect: "follow"
                }).then(() => {
                    console.debug('code url reloaded', href);
                }).catch((e) => {
                    console.debug(e);
                });
            }

            if (m && m[3] && href.includes('.user.js')) {
                const href = `https://${location.hostname}/scripts/${m[3]}-fetching/code/${crypto.randomUUID()}.user.js?version_=${Date.now()}`
                0 && fetch(href, {
                    method: "GET",
                    cache: 'reload',
                    redirect: "follow"
                }).then(() => {
                    console.debug('code url reloaded', href);
                }).catch((e) => {
                    console.debug(e);
                });
            }


        }

        button.setAttribute('acnmd', '');
    };

    const setupInstallLink = (button) => {
        if (!button || button.className !== 'install-link' || button.nodeName !== "A" || !button.href) return button;
        button.addEventListener('pointerdown', installLinkPointerDownHandler);
        return button;
    };

    function fixValue(key, def, test) {
        return GM.getValue(key, def).then((v) => test(v) || GM.deleteValue(key))
    }

    function numberArr(arrVal) {
        if (!arrVal || typeof arrVal.length !== 'number') return [];
        return arrVal.filter(e => typeof e === 'number' && !isNaN(e))
    }

    const isScriptFirstUse = await GM.getValue('firstUse', true);
    await Promise.all([
        fixValue('hiddenList', [], v => v && typeof v === 'object' && typeof v.length === 'number' && (v.length === 0 || typeof v[0] === 'number')),
        fixValue('lastMilestone', 0, v => v && typeof v === 'number' && v >= 0)
    ])

    function createRE(t, ...opt) {
        try {
            return new RegExp(t, ...opt);
        } catch (e) { }
        return null;
    }

    const useHashedScriptName = true;
    const fixLibraryScriptCodeLink = true;
    const addAdditionInfoLengthHint = true;

    const id = 'greasyfork-plus';
    const title = `${GM.info.script.name} v${GM.info.script.version} Settings`;
    const fields = mWindow.fields;
    const logo = mWindow.logo;
    const nonLatins = /[^\p{Script=Latin}\p{Script=Common}\p{Script=Inherited}]/gu;
    const blacklist = createRE((mWindow.blacklist || []).filter(e => !!e).join('|'), 'giu');
    const hiddenList = numberArr(await GM.getValue('hiddenList', []));
    const lang = document.documentElement.lang;
    const locales = mWindow.locales;

    const _isBlackList = (text) => {
        if (!text || typeof text !== 'string') return false;
        if (text.includes('hack') && (text.includes('EXPERIMENT_FLAGS') || text.includes('yt.'))) return false;
        return blacklist.test(text);
    }
    const isBlackList = (name, description) => {
        // To be reviewed
        if (!blacklist) return false;
        return _isBlackList(name) || _isBlackList(description);
    }

    function hiddenListStrToArr(str) {
        if (!str || typeof str !== 'string') str = '';
        return [...new Set(str ? numberArr(str.split(',').map(e => parseInt(e))) : [])];
    }

    const gmc = new GM_config({
        id,
        title,
        fields,
        css: mWindow.settingsCSS,
        events: {
            init: () => {
                gmc.initializedResolve && gmc.initializedResolve();
                gmc.initializedResolve = null;

            },
            /** @param {Document} document */
            open: async (document) => {
                const textarea = document.querySelector(`#${id}_field_hiddenList`);

                const hiddenSet = new Set(numberArr(await GM.getValue('hiddenList', [])));
                if (hiddenSet.size !== 0) {
                    const unsavedHiddenList = hiddenListStrToArr(gmc.get('hiddenList'));
                    const unsavedHiddenSet = new Set(unsavedHiddenList);

                    const hasDifferentItems = [...hiddenSet].some(item => !unsavedHiddenSet.has(item)) || [...unsavedHiddenSet].some(item => !hiddenSet.has(item));

                    if (hasDifferentItems) {

                        gmc.fields.hiddenList.value = [...hiddenSet].sort((a, b) => a - b).join(', ');

                        gmc.close();
                        gmc.open();

                    }


                }

                const resize = (target) => {
                    target.style.height = '';
                    target.style.height = `${target.scrollHeight}px`;
                };

                if (textarea) {
                    resize(textarea);
                    textarea.addEventListener('input', (event) => resize(event.target));

                }

                document.body.addEventListener('mousedown', (event) => {
                    if (event.detail > 1 && !event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && !event.defaultPrevented) {
                        event.preventDefault();
                        event.stopPropagation();
                        event.stopImmediatePropagation();
                    }
                }, true);
            },
            save: async (forgotten) => {

                if (gmc.isOpen) {
                    await GM.setValue('hiddenList', hiddenListStrToArr(forgotten.hiddenList));

                    UU.alert('settings saved');
                    gmc.close();
                    setTimeout(() => window.location.reload(false), 500);
                }
            }
        }
    });
    gmc.initialized = new Promise(r => (gmc.initializedResolve = r));
    await gmc.initialized.then();
    const customBlacklistRE = createRE((gmc.get('customBlacklist') || '').replace(/\s/g, '').split(',').join('|'), 'giu');

    if (typeof GM.registerMenuCommand === 'function') {
        GM.registerMenuCommand('Configure', () => gmc.open());
        GM.registerMenuCommand('Reset Everything', () => {
            Promise.all([
                GM.deleteValue('hiddenList'),
                GM.deleteValue('lastMilestone'),
                GM.deleteValue('firstUse')
            ]).then(() => {
                setTimeout(() => window.location.reload(false), 50);
            })
        });
    }

    UU.init({ id, logging: gmc.get('logging') });
    UU.log(nonLatins);
    UU.log(blacklist);
    UU.log(hiddenList);

    const _VM = (typeof VM !== 'undefined' ? VM : null) || {
        shortcut: {
            register: () => { }
        }
    };


    function fixLibraryCodeURL(code_url) {
        if (/\/scripts\/(\d+)(\-[^\/]+)\/code\//.test(code_url)) {
            code_url = code_url.replace(/\/scripts\/(\d+)(\-[^\/]+)\/code\//, '/scripts/$1/code/');
            let qm = code_url.indexOf('?');
            let s1 = code_url.substring(0, qm);
            let s2 = code_url.substring(qm + 1);
            if (qm > 0) {
                code_url = `${decodeURI(s1)}?${s2}`;
            }
        }
        return code_url;
    }

    function setClickToSelect(elm) {
        elm.addEventListener('click', function () {
            if (window.getSelection() + "" === "") {
                if (typeof this.select === 'function') {
                    this.select();
                } else {
                    const range = document.createRange();  // Create a range object
                    range.selectNode(this);        // Select the text within the element
                    const selection = window.getSelection(); // Get the selection object
                    selection.removeAllRanges();  // First clear any existing selections
                    selection.addRange(range);    // Add the new range to the selection
                }
            }
        });
        elm.addEventListener('drag', function (evt) {
            evt.preventDefault();
        });
        elm.addEventListener('drop', function (evt) {
            evt.preventDefault();
        });
        elm.addEventListener('dragstart', function (evt) {
            evt.preventDefault();
        });
    }

    const copyText = typeof (((window.navigator || 0).clipboard || 0).writeText) === 'function' ? (text) => {
        navigator.clipboard.writeText(text).then(function () {
            //
        }).catch(function (err) {
            alert("Unable to Copy");
        });
    } : (text) => {
        const textToCopy = document.createElement('strong');
        textToCopy.style.position = 'fixed';
        textToCopy.style.opacity = '0';
        textToCopy.style.top = '-900vh';
        textToCopy.textContent = text;
        document.body.appendChild(textToCopy);

        const range = document.createRange();  // Create a range object
        range.selectNode(textToCopy);        // Select the text within the element

        const selection = window.getSelection(); // Get the selection object
        selection.removeAllRanges();  // First clear any existing selections
        selection.addRange(range);    // Add the new range to the selection

        try {
            document.execCommand('copy');  // Try to copy the selected text
        } catch (err) {
            alert("Unable to Copy");
        }

        selection.removeAllRanges();  // Remove the selection range after copying
        textToCopy.remove();
    };


    let avoidDuplication = 0;
    const avoidDuplicationF = () => {
        const p = avoidDuplication;
        avoidDuplication = Date.now();
        if (avoidDuplication - p < 30) return false;
        return true;
    }
    // https://violentmonkey.github.io/vm-shortcut/
    const shortcuts = [
        ['ctrlcmd-alt-keys', () => avoidDuplicationF() && gmc.open()],
        ['ctrlcmd-alt-keyb', () => avoidDuplicationF() && toggleListDisplayingItem('blacklisted')],
        ['ctrlcmd-alt-keyh', () => avoidDuplicationF() && toggleListDisplayingItem('hidden')]
    ]
    for (const [scKey, scFn] of shortcuts) {
        _VM.shortcut.register(scKey, scFn);
    }

    const addSettingsToMenu = () => {
        const nav = document.querySelector('#site-nav > nav')
        if (!nav) return;

        const scriptName = GM.info.script.name;
        const scriptVersion = GM.info.script.version;
        const menu = document.createElement('li');
        menu.classList.add(id);
        menu.setAttribute('alt', `${scriptName} ${scriptVersion}`);
        menu.setAttribute('title', `${scriptName} ${scriptVersion}`);
        const link = document.createElement('a');
        link.setAttribute('href', '#');
        link.textContent = GM.info.script.name;
        menu.appendChild(link);
        nav.insertBefore(menu, document.querySelector('#site-nav > nav > li:first-of-type'));

        menu.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            gmc.open();
        });
    };


    const toggleListDisplayingItem = (t) => {

        const m = document.documentElement;

        const p = t + '-shown';
        let currentIsShown = m.hasAttribute(p)
        if (!currentIsShown) {
            m.setAttribute(p, '')
        } else {
            m.removeAttribute(p)
        }

    }

    const createListOptionGroup = () => {

        const html = `<div class="list-option-group" id="${id}-options">${GM.info.script.name} Lists:<ul>
      <li class="list-option blacklisted"><a href="#" id="hyperlink-35389"></a></li>
      <li class="list-option hidden"><a href="#" id="hyperlink-40361"></a></li>
      </ul></div>`;
        const firstOptionGroup = document.querySelector('.list-option-groups > div');
        firstOptionGroup && firstOptionGroup.insertAdjacentHTML('beforebegin', html);

        const blacklistedOption = document.querySelector(`#${id}-options li.blacklisted`);
        blacklistedOption && blacklistedOption.addEventListener('click', (evt) => {
            evt.preventDefault();
            toggleListDisplayingItem('blacklisted');
        }, false);

        const hiddenOption = document.querySelector(`#${id}-options li.hidden`);
        hiddenOption && hiddenOption.addEventListener('click', (evt) => {
            evt.preventDefault();
            toggleListDisplayingItem('hidden');
        }, false);

    }

    const addOptions = () => {

        const gn = () => {

            let aBlackList = document.querySelector('#hyperlink-35389');
            let aHidden = document.querySelector('#hyperlink-40361');
            if (!aBlackList || !aHidden) return;
            aBlackList.textContent = `Blacklisted scripts (${document.querySelectorAll('.script-list li.blacklisted').length})`;
            aHidden.textContent = `Hidden scripts (${document.querySelectorAll('.script-list li.hidden').length})`

        }
        const callback = (entries) => {
            if (entries && entries.length >= 1) requestAnimationFrame(gn);
        }

        const setupScriptList = async () => {
            let scriptList;
            let i = 8;
            while (i-- > 0) {
                scriptList = document.querySelector('.script-list li')
                if (scriptList) scriptList = scriptList.closest('.script-list')
                if (scriptList) break;
                await new Promise(r => requestAnimationFrame(r))
            }
            if (!scriptList) return;
            createListOptionGroup();
            const mo = new MutationObserver(callback);
            mo.observe(scriptList, { childList: true, subtree: true });
            gn();
        }
        setupScriptList();

    };


    /**
     * Get script data from Greasy Fork API
     *
     * @param {number} id Script ID
     * @returns {Promise} Script data
     */
    let networkMP1 = Promise.resolve();
    let networkMP2 = Promise.resolve();
    let previousIsCache = false;
    // let ss = [];
    // var sum = function(nums) {
    //   var total = 0;
    //   for (var i = 0, len = nums.length; i < len; i++) total += nums[i];
    //   return total;
    // };
    const getScriptData = async (id, noCache) => {

        if (!(id >= 0)) return Promise.resolve()
        const url = `https://${window.location.hostname}/scripts/${id}.json`;
        return new Promise((resolve, reject) => {

            const onPageElement = document.querySelector(`[data-script-namespace][data-script-id="${id || 'null'}"][data-script-name][data-script-version][href]`)
            if (onPageElement && /^https\:\/\/update\.\w+\.org\/scripts\/\d+\/[^.?\/]+\.user\.js$/.test(onPageElement.getAttribute('href') || '')) {

                const result = {
                    "id": +onPageElement.getAttribute('data-script-id'),
                    // "created_at": "2023-08-24T21:16:50.000Z",
                    // "daily_installs": 21,
                    // "total_installs": 3310,
                    // "code_updated_at": "2023-12-20T07:46:54.000Z",
                    // "support_url": null,
                    // "fan_score": "74.1",
                    "namespace": `${onPageElement.getAttribute('data-script-namespace')}`,
                    // "contribution_url": null,
                    // "contribution_amount": null,
                    // "good_ratings": 11,
                    // "ok_ratings": 0,
                    // "bad_ratings": 0,
                    // "users": [
                    //     {
                    //         "id": 371179,
                    //         "name": "𝖢𝖸 𝖥𝗎𝗇𝗀",
                    //         "url": "https://greasyfork.org/users/371179-%F0%9D%96%A2%F0%9D%96%B8-%F0%9D%96%A5%F0%9D%97%8E%F0%9D%97%87%F0%9D%97%80"
                    //     }
                    // ],
                    "name": `${onPageElement.getAttribute('data-script-name')}`,
                    // "description": "Adds various features and improves the Greasy Fork experience",
                    // "url": "https://greasyfork.org/scripts/473830-greasy-fork",
                    // "code_url": "https://update.greasyfork.org/scripts/473830/Greasy%20Fork%2B%2B.user.js",
                    "code_url": `${onPageElement.getAttribute('href')}`,
                    // "license": "MIT License",
                    "version": `${onPageElement.getAttribute('data-script-version')}`,
                    // "locale": "en",
                    // "deleted": false
                };
                resolve(result);
                return;
            }

            networkMP1 = networkMP1.then(() => new Promise(unlock => {

                const maxAgeInSeconds = 900;
                const rd = previousIsCache ? 1 : Math.floor(Math.random() * 80 + 80);
                let fetchStart = 0;
                new Promise(r => setTimeout(r, rd))
                    .then(() => {
                        fetchStart = Date.now();
                    })
                    .then(() => fetch(url, noCache ? {
                        method: 'GET',
                        cache: 'reload',
                        credentials: 'omit',
                        headers: new Headers({
                            'Cache-Control': `max-age=${maxAgeInSeconds}`,
                        })
                    } : {
                        method: 'GET',
                        cache: 'force-cache',
                        credentials: 'omit',
                        headers: new Headers({
                            'Cache-Control': `max-age=${maxAgeInSeconds}`,
                        }),
                    }))
                    .then((response) => {

                        let fetchStop = Date.now();
                        // const dd = fetchStop - fetchStart;
                        // dd (cache) = {min: 1, max: 8, avg: 3.7}
                        // dd (normal) = {min: 136, max: 316, avg: 162.62}

                        // ss.push(dd)
                        // ss.maxValue = Math.max(...ss);
                        // ss.minValue = Math.min(...ss);
                        // ss.avgValue = sum(ss)/ss.length;
                        // console.log(dd)
                        // console.log(ss)
                        previousIsCache = (fetchStop - fetchStart) < (3.7 + 162.62) / 2;
                        UU.log(`${response.status}: ${response.url}`)
                        // UU.log(response)
                        if (response.ok === true) {
                            unlock();
                            return response.json()
                        }
                        if (response.status === 503) {
                            return new Promise(r => setTimeout(r, 270 + rd)).then(() => {
                                unlock();
                                return getScriptData(id, true);
                            });
                        }
                        if (response.status === 404) {
                            // script XXXX has been reported and is pending review by a moderator.
                            unlock();
                            return null
                        }
                        console.warn(response.status, response);
                        new Promise(r => setTimeout(r, 470)).then(unlock); // reload later
                    })
                    .then((data) => resolve(data))
                    .catch((e) => {
                        unlock();
                        UU.log(id, url)
                        console.warn(e)
                        // reject(e)
                    })

            })).catch(() => { })

        });
    }

    /**
     * Get user data from Greasy Fork API
     *
     * @param {string} userID User ID
     * @returns {Promise} User data
     */
    const getUserData = (userID, noCache) => {

        if (!(userID >= 0)) return Promise.resolve()

        const url = `https://${window.location.hostname}/users/${userID}.json`;
        return new Promise((resolve, reject) => {


            networkMP2 = networkMP2.then(() => new Promise(unlock => {

                const maxAgeInSeconds = 900;
                const rd = Math.floor(Math.random() * 80 + 80);

                new Promise(r => setTimeout(r, rd))
                    .then(() => fetch(url, noCache ? {
                        method: 'GET',
                        cache: 'reload',
                        credentials: 'omit',
                        headers: new Headers({
                            'Cache-Control': `max-age=${maxAgeInSeconds}`,
                        })
                    } : {
                        method: 'GET',
                        cache: 'force-cache',
                        credentials: 'omit',
                        headers: new Headers({
                            'Cache-Control': `max-age=${maxAgeInSeconds}`,
                        }),
                    }))
                    .then((response) => {
                        UU.log(`${response.status}: ${response.url}`)
                        if (response.ok === true) {
                            unlock();
                            return response.json()
                        }
                        if (response.status === 503) {
                            return new Promise(r => setTimeout(r, 270 + rd)).then(() => {
                                unlock();
                                return getUserData(userID, true); // reload later
                            });
                        }
                        if (response.status === 404) {
                            // user XXXX has been reported and is pending review by a moderator. ????
                            unlock();
                            return null
                        }
                        console.warn(response.status, response);
                        new Promise(r => setTimeout(r, 470)).then(unlock);
                    })
                    .then((data) => resolve(data))
                    .catch((e) => {
                        setTimeout(() => {
                            unlock()
                        }, 270)
                        UU.log(userID, url)
                        console.warn(e)
                        // reject(e)
                    })



            })).catch(() => { })

        });
    }
    const getTotalInstalls = (data) => {
        if (!data || !data.scripts) return;
        return new Promise((resolve, reject) => {
            const totalInstalls = [];

            data.scripts.forEach((element) => {
                totalInstalls.push(parseInt(element.total_installs, 10));
            });

            resolve(totalInstalls.reduce((a, b) => a + b, 0));
        });
    };


    const communicationId = WinComm.newCommunicationId();
    const wincomm = WinComm.createInstance(communicationId);


    const isInstalled = (script) => {
        return new Promise((resolve, reject) => {

            promiseScriptCheck.then(d => {

                if (!d) return null;

                const data = d.data;
                const al = data.type % 10;
                if (al === 0) {
                    // no namespace
                    resolve([null, script.name, '']);
                } else if (al === 1) {
                    // namespace

                    if (!script.namespace) {

                        getRafPromise() // foreground
                            .then(() => getScriptData(script.id))
                            .then((script) => {
                                resolve([null, script.name, script.namespace]);
                            });

                    } else {

                        resolve([null, script.name, script.namespace]);
                    }

                }


            })


        }).then((res) => {


            return new Promise((resolve, reject) => {

                if (!res) return '';


                const [_, name, namespace] = res;
                wincomm.request('installedVersion.req', {
                    name,
                    namespace
                }).then(d => {
                    resolve(d.data.version)
                })

            })

        })

        /*
        const external = unsafeWindow.external;
        const scriptHandler = GM.info.scriptHandler;
        if (external && external.Violentmonkey && (scriptHandler || 'Violentmonkey') === 'Violentmonkey') {
          external.Violentmonkey.isInstalled(name, namespace).then((data) => resolve(data));
          return;
        }

        if (external && external.Tampermonkey && (scriptHandler || 'Tampermonkey') === 'Tampermonkey') {
          external.Tampermonkey.isInstalled(name, namespace, (data) => {
            (data.installed) ? resolve(data.version) : resolve();
          });
          return;
        }
        */


    };

    const compareVersions = (v1, v2) => {
        if (!v1 || !v2) return NaN;
        if (v1 === null || v2 === null) return NaN;
        if (v1 === v2) return 0;

        const sv1 = v1.split('.').map((index) => parseInt(index));
        const sv2 = v2.split('.').map((index) => parseInt(index));

        for (let index = 0; index < Math.max(sv1.length, sv2.length); index++) {
            if (isNaN(sv1[index]) || isNaN(sv2[index])) return NaN;
            if (sv1[index] > sv2[index]) return 1;
            if (sv1[index] < sv2[index]) return -1;
        }

        return 0;
    };


    /**
     * Return label for the hide script button
     *
     * @param {boolean} hidden Is hidden
     * @returns {string} Label
     */
    const blockLabel = (hidden) => {
        return hidden ? (locales[lang] ? locales[lang].notHide : locales.en.notHide) : (locales[lang] ? locales[lang].hide : locales.en.hide)
    }

    /**
     * Return label for the install button
     *
     * @param {number} update Update value
     * @returns {string} Label
     */
    const installLabel = (update) => {
        switch (update) {
            case 0: {
                return locales[lang] ? locales[lang].reinstall : locales.en.reinstall
            }
            case 1: {
                return locales[lang] ? locales[lang].update : locales.en.update
            }
            case -1: {
                return locales[lang] ? locales[lang].downgrade : locales.en.downgrade
            }
            default: {
                return locales[lang] ? locales[lang].install : locales.en.install
            }
        }
    }

    const hideBlacklistedDiscussion = (element, list) => {

        const scriptLink = element.querySelector('a.script-link')
        const m = /\/scripts\/(\d+)/.exec(scriptLink);
        const id = m ? +m[1] : 0;
        if (!(id > 0)) return;

        switch (list) {
            case 'hiddenList': {
                const container = element.closest('.discussion-list-container') || element;
                if (hiddenList.indexOf(id) >= 0) {
                    container.classList.add('hidden');
                }
                // if (customBlacklist && (customBlacklist.test(name) || customBlacklist.test(description)) && !element.classList.contains('blacklisted')) {
                //     element.classList.add('blacklisted', 'custom-blacklist');
                //     if (gmc.get('hideBlacklistedScripts') && gmc.get('debugging')) {
                //         let scriptLink = element.querySelector('.script-link');
                //         if (scriptLink) { scriptLink.textContent += ' (custom-blacklist)'; }
                //     }
                // }
                break;
            }
            default:
                UU.log('No blacklists');
                break;
        }

    }
    const hideBlacklistedScript = (element, list) => {
        if (!element) return;
        const scriptLink = element.querySelector('.script-link')

        const name = scriptLink ? scriptLink.textContent : '';
        const descriptionElem = element.querySelector('.script-description')
        const description = descriptionElem ? descriptionElem.textContent : '';

        if (!name) return;

        switch (list) {
            case 'nonLatins':
                if ((nonLatins.test(name) || nonLatins.test(description)) && !element.classList.contains('blacklisted')) {
                    element.classList.add('blacklisted', 'non-latins');
                    if (gmc.get('hideBlacklistedScripts') && gmc.get('debugging')) {
                        let scriptLink = element.querySelector('.script-link');
                        if (scriptLink) { scriptLink.textContent += ' (non-latin)'; }
                    }
                }
                break;
            case 'blacklist':
                if (isBlackList(name, description) && !element.classList.contains('blacklisted')) {
                    element.classList.add('blacklisted', 'blacklist');
                    if (gmc.get('hideBlacklistedScripts') && gmc.get('debugging')) {
                        let scriptLink = element.querySelector('.script-link');
                        if (scriptLink) { scriptLink.textContent += ' (blacklist)'; }
                    }
                }
                break;
            case 'customBlacklist': {
                const customBlacklist = customBlacklistRE;
                if (customBlacklist && (customBlacklist.test(name) || customBlacklist.test(description)) && !element.classList.contains('blacklisted')) {
                    element.classList.add('blacklisted', 'custom-blacklist');
                    if (gmc.get('hideBlacklistedScripts') && gmc.get('debugging')) {
                        let scriptLink = element.querySelector('.script-link');
                        if (scriptLink) { scriptLink.textContent += ' (custom-blacklist)'; }
                    }
                }
                break;
            }
            default:
                UU.log('No blacklists');
                break;
        }
    };

    const hideHiddenScript = (element, id, list) => {
        id = +id;
        if (!(id >= 0)) return;

        const isInHiddenList = () => hiddenList.indexOf(id) !== -1;
        const updateScriptLink = (shouldHide) => {
            if (gmc.get('hideHiddenScript') && gmc.get('debugging')) {
                let scriptLink = element.querySelector('.script-link');
                if (scriptLink) {
                    if (shouldHide) {
                        scriptLink.innerHTML += ' (hidden)';
                    } else {
                        scriptLink.innerHTML = scriptLink.innerHTML.replace(' (hidden)', '');
                    }
                }
            }
        };

        // Check for initial state and set it
        if (isInHiddenList()) {
            element.classList.add('hidden');
            updateScriptLink(true);
        }

        // Add button to hide the script
        const insertButtonHTML = (selector, html) => {
            const target = element.querySelector(selector);
            if (!target) return;
            let p = document.createElement('template');
            p.innerHTML = html;
            target.parentNode.insertBefore(p.content.firstChild, target.nextSibling);
        };

        const isHidden = element.classList.contains('hidden');
        const blockButtonHTML = `<span class=block-button role=button style-16377>${blockLabel(isHidden)}</span>`;
        const blockButtonHeaderHTML = `<span class=block-button role=button style-77329 style="">${blockLabel(isHidden)}</span>`;

        insertButtonHTML('.badge-js, .badge-css', blockButtonHTML);
        insertButtonHTML('header h2', blockButtonHeaderHTML);

        // Add event listener
        const button = element.querySelector('.block-button');
        if (button) {
            button.addEventListener('click', (event) => {
                event.stopPropagation();
                event.stopImmediatePropagation();

                if (!isInHiddenList()) {
                    hiddenList.push(id);
                    GM.setValue('hiddenList', hiddenList);

                    element.classList.add('hidden');
                    updateScriptLink(true);

                } else {
                    const index = hiddenList.indexOf(id);
                    hiddenList.splice(index, 1);
                    GM.setValue('hiddenList', hiddenList);

                    element.classList.remove('hidden');
                    updateScriptLink(false);
                }

                const blockBtn = element.querySelector('.block-button');
                if (blockBtn) blockBtn.textContent = blockLabel(element.classList.contains('hidden'));
            });
        }
    };

    const insertButtonHTML = (element, selector, html) => {
        const target = element.querySelector(selector);
        if (!target) return;
        let p = document.createElement('template');
        p.innerHTML = html;
        let button = p.content.firstChild
        target.parentNode.insertBefore(button, target.nextSibling);
        return button;
    };

    const addInstallButton = (element, url) => {
        return setupInstallLink(insertButtonHTML(element, '.badge-js, .badge-css', `<a class="install-link" href="${url}" style-54998></a>`));
    };

    async function digestMessage(message, algo) {
        const encoder = new TextEncoder();
        const data = encoder.encode(message);
        const hash = await crypto.subtle.digest(algo, data);
        return hash;
    }

    function qexString(buffer) {
        const byteArray = new Uint8Array(buffer);
        const len = byteArray.length;
        const hexCodes = new Array(len * 2);
        const chars = 'a4b3c5d7e6f9h2t';
        for (let i = 0, j = 0; i < len; i++) {
            const byte = byteArray[i];
            hexCodes[j++] = chars[byte >> 4];
            hexCodes[j++] = chars[byte & 0x0F];
        };
        return hexCodes.join('');
    }

    const encodeFileName = (s) => {
        if (!s || typeof s !== 'string') return s;
        s = s.replace(/[.!~*'"();\/\\?@&=$,#]/g, '-').replace(/\s+/g, ' ');
        return encodeURI(s);
    }

    const isLibraryURLWithVersion = (url) => {
        if (!url || typeof url !== 'string') return;

        if (url.includes('.js?version=')) return true;

        if (/\/scripts\/\d+\/\d+\/[^.!~*'"();\/\\?@&=$,#]+\.js/.test(url)) return true;
        return false;

    }

    const showInstallButton = async (scriptID, element) => {

        await getRafPromise().then();
        // if(document.querySelector(`li[data-script-id="${scriptID}"]`))
        let _baseScript = null;
        if (element.nodeName === 'LI' && element.hasAttribute('data-script-id') && element.getAttribute('data-script-id') === `${scriptID}` && element.getAttribute('data-script-language') === 'js') {

            const version = element.getAttribute('data-script-version') || ''

            let scriptCodeURL = element.getAttribute('data-code-url');
            if (!scriptCodeURL || !isVaildURL(scriptCodeURL)) {

                const name = element.getAttribute('data-script-name') || ''
                // if (!/[^\x00-\x7F]/.test(name)) {

                // const scriptName = useHashedScriptName ? qexString(await digestMessage(`${+scriptID} ${version}`, 'SHA-1')).substring(0, 8) : encodeURI(name);
                // const token = useHashedScriptName ? `${scriptName.substring(0, 2)}${scriptName.substring(scriptName.length - 2, scriptName.length)}` : String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 19861 + 19861).toString(36);
                const scriptFilename = element.getAttribute('data-script-type') === 'library' ? `${encodeFileName(name)}.js` : `${encodeFileName(name)}.user.js`;
                // const scriptFilename = `${scriptName}.user.js`;

                // code_url: `https://${location.hostname}/scripts/${scriptID}-${token}/code/${scriptFilename}`,
                // code_url: `https://update.${location.hostname}/scripts/${scriptID}.user.js`,
                scriptCodeURL = `https://update.${location.hostname}/scripts/${scriptID}/${scriptFilename}`
            }
            _baseScript = {
                id: +scriptID,
                // name: name,
                code_url: scriptCodeURL,
                version: version
            }
            // }

        }

        const baseScript = _baseScript || (await getScriptData(scriptID));

        if ((element.nodeName === 'LI' && element.getAttribute('data-script-type') === 'library') || (baseScript.code_url.includes('.js?version='))) {

            let scriptCodeURL = element.getAttribute('data-code-url');

            if (!scriptCodeURL || !isVaildURL(scriptCodeURL)) {
                const script = baseScript.code_url.includes('.js?version=') ? baseScript : (await getScriptData(scriptID));
                scriptCodeURL = script.code_url;
            }

            if (scriptCodeURL && isLibraryURLWithVersion(scriptCodeURL)) {


                const code_url = fixLibraryCodeURL(scriptCodeURL);

                const button = addInstallButton(element, code_url);
                button.textContent = `Copy URL`;
                button.addEventListener('click', function (evt) {

                    const target = (evt || 0).target;
                    if (!target) return;

                    let a = target.nodeName === 'A' ? target : target.querySelector('a[href]');

                    if (!a) return;
                    let href = target.getAttribute('href');
                    if (!href) return;

                    evt.preventDefault();

                    copyText(href);


                });

            }


        } else {


            if (!baseScript || !baseScript.code_url || !baseScript.version) return;
            const button = addInstallButton(element, baseScript.code_url);
            button.classList.add('install-status-checking');
            button.textContent = `${installLabel()} ${baseScript.version}`;
            const script = baseScript && baseScript.name && baseScript.namespace ? baseScript : (await getScriptData(scriptID));
            if (!script) return;

            const installed = await isInstalled(script);
            const version = (
                baseScript.version && script.version && compareVersions(baseScript.version, script.version) === 1
            ) ? baseScript.version : script.version;

            const update = compareVersions(version, installed);  // NaN  1  -1  0
            const label = installLabel(update);
            button.textContent = `${label} ${version}`;
            button.classList.remove('install-status-checking');


        }

    }


    const foundScriptList = async (scriptList) => {

        let rid = 0;
        let g = () => {
            if (!scriptList || scriptList.isConnected !== true) return;

            const scriptElements = scriptList.querySelectorAll('li[data-script-id]:not([e8kk])');

            for (const element of scriptElements) {
                element.setAttribute('e8kk', '1');

                const scriptID = +element.getAttribute('data-script-id');
                if (!(scriptID > 0)) continue;

                // blacklisted scripts
                if (gmc.get('nonLatins')) hideBlacklistedScript(element, 'nonLatins');
                if (gmc.get('blacklist')) hideBlacklistedScript(element, 'blacklist');
                if (gmc.get('customBlacklist')) hideBlacklistedScript(element, 'customBlacklist');

                // hidden scripts
                if (gmc.get('hideHiddenScript')) hideHiddenScript(element, scriptID, true);

                // install button
                if (gmc.get('showInstallButton')) {
                    showInstallButton(scriptID, element)
                }
            }

        }
        let f = (entries) => {
            const tid = ++rid
            if (entries && entries.length) requestAnimationFrame(() => {
                if (tid === rid) g();
            });
        }
        let mo = new MutationObserver(f);
        mo.observe(scriptList, { subtree: true, childList: true });

        g();

    }

    const foundDiscussionList = (discussionsList) => {

        let rid = 0;
        let g = () => {
            if (!discussionsList || discussionsList.isConnected !== true) return;

            const scriptElements = discussionsList.querySelectorAll('.discussion-list-item:not([e8kk])');

            for (const element of scriptElements) {
                element.setAttribute('e8kk', '1');
                // const scriptID = +element.getAttribute('data-script-id');
                // if (!(scriptID > 0)) continue;


                // blacklisted scripts
                // if (gmc.get('nonLatins')) hideBlacklistedDiscussion(element, 'nonLatins');
                // if (gmc.get('blacklist')) hideBlacklistedDiscussion(element, 'blacklist');
                if (gmc.get('hideHiddenScript')) hideBlacklistedDiscussion(element, 'hiddenList');

                // // hidden scripts
                // if (gmc.get('hideHiddenScript')) hideBlacklistedDiscussion(element, scriptID, true);

                // // install button
                // if (gmc.get('showInstallButton')) {
                //     showInstallButton(scriptID, element)
                // }
            }

        }
        let f = (entries) => {
            const tid = ++rid
            if (entries && entries.length) requestAnimationFrame(() => {
                if (tid === rid) g();
            });
        }
        let mo = new MutationObserver(f);
        mo.observe(discussionsList, { subtree: true, childList: true });

        g();

    }

    let promiseScriptCheckResolve = null;
    const promiseScriptCheck = new Promise(resolve => {
        promiseScriptCheckResolve = resolve
    });

    const milestoneNotificationFn = async (o) => {

        const { userLink, userID } = o;


        const milestones = gmc.get('milestoneNotification').replace(/\s/g, '').split(',').map(Number);

        if (!userID) return;

        await new Promise(resolve => setTimeout(resolve, 800)); // delay for reducing server burden
        await new Promise(resolve => requestAnimationFrame(resolve)); // foreground

        const userData = await getUserData(+userID.match(/\d+(?=\D)/g));
        if (!userData) return;

        const [totalInstalls, lastMilestone] = await Promise.all([
            getTotalInstalls(userData),
            GM.getValue('lastMilestone', 0)]);

        const milestone = milestones.filter(milestone => totalInstalls >= milestone).pop();

        UU.log(`total installs are "${totalInstalls}", milestone reached is "${milestone}", last milestone reached is "${lastMilestone}"`);

        if (milestone <= lastMilestone) return;

        if (milestone && milestone >= 0) {


            GM.setValue('lastMilestone', milestone);

            const lang = document.documentElement.lang;
            const text = (locales[lang] ? locales[lang].milestone : locales.en.milestone).replace('$1', milestone.toLocaleString());

            if (typeof GM.notification === 'function') {
                GM.notification({
                    text,
                    title: GM.info.script.name,
                    image: logo,
                    onclick: () => {
                        window.location = `https://${window.location.hostname}${userID}#user-script-list-section`;
                    }
                });
            } else {
                UU.alert(text);
            }

        }

    }
    const onReady = async () => {

        try {

            const gminfo = GM.info || 0;
            if (gminfo) {

                const gminfoscript = gminfo.script || 0;


                const scriptHandlerObject = {
                    scriptHandler: gminfo.scriptHandler || '',
                    scriptName: gminfoscript.name || '', // not name_i18n
                    scriptVersion: gminfoscript.version || '',
                    scriptNamespace: gminfoscript.namespace || '',
                    communicationId
                };


                wincomm.hook('_$GreasyFork$Msg$OnScriptInstallFeedback',
                    {

                        ready: (d, evt) => promiseScriptCheckResolve(d),
                        userScriptManagerNotDetected: (d, evt) => promiseScriptCheckResolve(null),
                        'installedVersion.res': wincomm.handleResponse


                    })


                document.head.appendChild(document.createElement('script')).textContent = `;(${mWindow.contentScriptText})(${JSON.stringify(scriptHandlerObject)}, ${WinComm.createInstance});`;


            }

            addSettingsToMenu();

            setTimeout(() => {
                getRafPromise().then(() => {
                    let installBtn = document.querySelector('a[data-script-id][data-script-version]')
                    let scriptID = installBtn && installBtn.textContent ? +installBtn.getAttribute('data-script-id') : 0;
                    if (scriptID > 0) {
                        getScriptData(scriptID, true);
                    } else {
                        const userLink = document.querySelector('#site-nav .user-profile-link a[href]');
                        let userID = userLink ? userLink.getAttribute('href') : '';
                        userID = userID ? /users\/(\d+)/.exec(userID) : null;
                        if (userID) userID = userID[1];
                        if (userID) {
                            userID = +userID;
                            if (userID > 0) {
                                getUserData(userID, true);
                            }
                        }
                    }
                });
            }, 740);

            const userLink = document.querySelector('.user-profile-link a[href]');
            const userID = userLink ? userLink.getAttribute('href') : undefined;

            UU.addStyle(mWindow.pageCSS);
            // blacklisted scripts / hidden scripts / install button
            if (window.location.pathname !== userID && !/discussions/.test(window.location.pathname) && (gmc.get('hideBlacklistedScripts') || gmc.get('hideHiddenScript') || gmc.get('showInstallButton'))) {

                const scriptList = document.querySelector('.script-list');
                if (scriptList) {
                    foundScriptList(scriptList);
                } else {
                    const timeout = Date.now() + 3000;
                    /** @type {MutationObserver | null} */
                    let mo = null;
                    const mutationCallbackForScriptList = () => {
                        if (!mo) return;
                        const scriptList = document.querySelector('.script-list');
                        if (scriptList) {
                            mo.disconnect();
                            mo.takeRecords();
                            mo = null;
                            foundScriptList(scriptList);
                        } else if (Date.now() > timeout) {
                            mo.disconnect();
                            mo.takeRecords();
                            mo = null;
                        }
                    }
                    mo = new MutationObserver(mutationCallbackForScriptList);
                    mo.observe(document, { subtree: true, childList: true });
                }


                // hidden scripts on details page
                const installLinkElement = document.querySelector('#script-info .install-link[data-script-id]');
                setupInstallLink(installLinkElement);
                if (gmc.get('hideHiddenScript') && installLinkElement) {
                    const id = +installLinkElement.getAttribute('data-script-id');
                    hideHiddenScript(document.querySelector('#script-info'), id, false);
                }

                // add options and style for blacklisted/hidden scripts
                if (gmc.get('hideBlacklistedScripts') || gmc.get('hideHiddenScript')) {
                    addOptions();
                }

                if (installLinkElement && location.pathname.includes('/scripts/')) {

                    installLinkElement.addEventListener('click', async function (e) {
                        if (e && e.isTrusted && location.pathname.includes('/scripts/')) {

                            await new Promise(r => setTimeout(r, 800));
                            await new Promise(r => window.requestAnimationFrame(r));
                            await new Promise(r => setTimeout(r, 100));
                            // let ethicalads497 = 'ethicalads' in window ? window.ethicalads : undefined;
                            // window.ethicalads = { wait: new Promise() }
                            document.dispatchEvent(new Event("DOMContentLoaded"));
                            document.documentElement.dispatchEvent(new Event("turbo:load"));
                            // if (ethicalads497 === undefined) delete window.ethicalads; else window.ethicalads = ethicalads497;
                        }
                    })
                }
            } else if (/\/discussions/.test(window.location.pathname)) {

                const discussionsList = document.querySelector('.discussion-list');


                if (discussionsList) {
                    foundDiscussionList(discussionsList);
                } else {
                    const timeout = Date.now() + 3000;
                    /** @type {MutationObserver | null} */
                    let mo = null;
                    const mutationCallbackForScriptList = () => {
                        if (!mo) return;
                        const discussionsList = document.querySelector('.script-list');
                        if (discussionsList) {
                            mo.disconnect();
                            mo.takeRecords();
                            mo = null;
                            foundDiscussionList(discussionsList);
                        } else if (Date.now() > timeout) {
                            mo.disconnect();
                            mo.takeRecords();
                            mo = null;
                        }
                    }
                    mo = new MutationObserver(mutationCallbackForScriptList);
                    mo.observe(document, { subtree: true, childList: true });
                }
            }

            // total installs
            if (gmc.get('showTotalInstalls') && document.querySelector('#user-script-list')) {
                const dailyInstalls = [];
                const totalInstalls = [];

                const dailyInstallElements = document.querySelectorAll('#user-script-list li dd.script-list-daily-installs');
                for (const element of dailyInstallElements) {
                    dailyInstalls.push(parseInt(element.textContent.replace(/\D/g, ''), 10));
                }

                const totalInstallElements = document.querySelectorAll('#user-script-list li dd.script-list-total-installs');
                for (const element of totalInstallElements) {
                    totalInstalls.push(parseInt(element.textContent.replace(/\D/g, ''), 10));
                }

                const dailyInstallsSum = dailyInstalls.reduce((a, b) => a + b, 0);
                const totalInstallsSum = totalInstalls.reduce((a, b) => a + b, 0);

                const convertLi = (li) => {

                    if (!li) return null;
                    const a = li.firstElementChild
                    if (a === null) return li;
                    if (a === li.lastElementChild && a.nodeName === 'A') return a;


                    return null;
                }

                const plusSign = document.querySelector('#user-script-list-section a[rel="next"][href*="page="], #user-script-list-section a[rel="prev"][href*="page="]') ? '+' : '';

                const dailyOption = convertLi(document.querySelector('#script-list-sort .list-option:nth-child(1)'));
                dailyOption && dailyOption.insertAdjacentHTML('beforeend', `<span> (${dailyInstallsSum.toLocaleString()}${plusSign})</span>`);

                const totalOption = convertLi(document.querySelector('#script-list-sort .list-option:nth-child(2)'));
                totalOption && totalOption.insertAdjacentHTML('beforeend', `<span> (${totalInstallsSum.toLocaleString()}${plusSign})</span>`);
            }

            // milestone notification
            if (gmc.get('milestoneNotification')) {
                milestoneNotificationFn({ userLink, userID });
            }

            if (isScriptFirstUse) GM.setValue('firstUse', false).then(() => {
                gmc.open();
            });

            if (fixLibraryScriptCodeLink) {


                let xpath = "//code[contains(text(), '.js?version=') or contains(text(), '// @require https://update.greasyfork.org/scripts/')]";
                let snapshot = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

                for (let i = 0; i < snapshot.snapshotLength; i++) {
                    let element = snapshot.snapshotItem(i);
                    if (element.firstElementChild) continue;
                    element.textContent = element.textContent.replace(/\bhttps:\/\/(greasyfork|sleazyfork)\.org\/scripts\/\d+\-[^\/]+\/code\/[^\.]+\.js\?version=\d+\b/, (_) => {
                        return fixLibraryCodeURL(_);
                    });
                    element.parentNode.insertBefore(document.createTextNode('\u200B'), element);
                    element.style.display = 'inline-flex';
                    setClickToSelect(element);
                }


            }




            if (addAdditionInfoLengthHint && location.pathname.includes('/scripts/') && location.pathname.includes('/versions')) {

                function contentLength(text) {
                    return text.replace(/\n/g, '  ').length;
                }
                function contentLengthMax() {
                    return 50000;
                }
                let _spanContent = null;
                function updateText(ainfo, span) {
                    const value = ainfo.value;
                    if (typeof value !== 'string') return;

                    if (_spanContent !== value) {
                        _spanContent = value;
                        span.textContent = `Text Length: ${contentLength(value)} / ${contentLengthMax()}`;


                    }
                }
                function onChange(evt) {
                    let ainfo = (evt || 0).target;
                    if (!ainfo) return;
                    let span = ainfo.parentNode.querySelector('.script-version-ainfo-span');
                    if (!span) return;

                    updateText(ainfo, span);

                }
                function kbEvent(evt) {
                    Promise.resolve().then(() => {
                        onChange(evt);

                    })
                }
                for (const ainfo of document.querySelectorAll('textarea[id^="script-version-additional-info"]')) {
                    let span = document.createElement('span');
                    span.classList.add('script-version-ainfo-span');
                    ainfo.addEventListener('change', onChange, false);
                    ainfo.addEventListener('keydown', kbEvent, false);
                    ainfo.addEventListener('keypress', kbEvent, false);
                    ainfo.addEventListener('keyup', kbEvent, false);
                    updateText(ainfo, span);
                    ainfo.parentNode.insertBefore(span, ainfo.nextSibling);


                }


            }

        } catch (e) {
            console.log(e);
        }



    }




    Promise.resolve().then(() => {
        if (document.readyState !== 'loading') {
            onReady();
        } else {
            window.addEventListener("DOMContentLoaded", onReady, false);
        }
    });

})();