Greasy Fork

Greasy Fork is available in English.

Greasy Fork++

添加各种功能并改善 Greasy Fork 体验

当前为 2023-08-25 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name               Greasy Fork++
// @name:de            Greasy Fork++
// @name:es            Greasy Fork++
// @name:fr            Greasy Fork++
// @name:it            Greasy Fork++
// @name:ru            Greasy Fork++
// @name:zh-CN         Greasy Fork++
// @author             CY Fung <http://greasyfork.icu/users/371179> & Davide <[email protected]>
// @namespace          https://github.com/iFelix18
// @icon               https://www.google.com/s2/favicons?domain=http://greasyfork.icu
// @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 体验
// @copyright          2023, CY Fung (http://greasyfork.icu/users/371179); 2021, Davide (https://github.com/iFelix18)
// @license            MIT
// @version            3.0.4
// @require            https://fastly.jsdelivr.net/gh/sizzlemctwizzle/GM_config@6a82709680bbeb3bd2041a4345638b628d537c96/gm_config.js
// @require            https://fastly.jsdelivr.net/npm/@violentmonkey/[email protected]/dist/index.min.js
// @match              *://greasyfork.org/*
// @match              *://sleazyfork.org/*
// @connect            greasyfork.org
// @compatible         chrome
// @compatible         edge
// @compatible         firefox
// @compatible         safari
// @grant              GM.deleteValue
// @grant              GM.getValue
// @grant              GM.notification
// @grant              GM.registerMenuCommand
// @grant              GM.setValue
// @run-at             document-start
// @inject-into        page
// ==/UserScript==

/* global $, GM_config, UU, VM */
/* eslint-disable unicorn/prefer-top-level-await */

//  -------- UU Fucntion - original code: https://fastly.jsdelivr.net/npm/@ifelix18/[email protected]/lib/index.min.js  --------
// optimized by CY Fung to remove $ dependency and improve the coding
const UU = (function () {
  const observers = {};
  const scriptName = GM.info.script.name;
  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 observeCreation = (selector, callback, options = {}) => {
    observers[selector] = {
      callback,
      onlyVisible: typeof options.onlyVisible === "boolean" ? options.onlyVisible : false,
      onlyFirstMatch: typeof options.onlyFirstMatch === "boolean" ? options.onlyFirstMatch : false,
    };
    observeMutations();
  };

  const observeMutations = () => {
    const observer = new MutationObserver(() => {
      for (const selector in observers) {
        const config = observers[selector];
        const elements = document.querySelectorAll(selector);
        for (const element of elements) {
          const onlyVisible = config.onlyVisible;
          if (!element.getAttribute(scriptId)) {
            if ((!onlyVisible || isElementVisible(element))) {
              element.setAttribute(scriptId, "1");
              config.callback.call(element, element);
              if (config.onlyFirstMatch) {
                delete observers[selector];
              }
            }
          }
        }
      }
    });
    observer.observe(document, { attributes: true, childList: true, subtree: true });
  };

  const isElementVisible = (element) => {
    return element.offsetHeight > 0 || element.offsetWidth > 0;
  };

  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,
    observeCreation,
  };
})();

//  -------- UU Fucntion - original code: https://fastly.jsdelivr.net/npm/@ifelix18/[email protected]/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 emoji 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,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'

  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: '更新到'
    }
  };

  const blacklist = [ /* cSpell: disable-next-line */
    '\\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 *{
    font-family:Open Sans,sans-serif,Segoe UI Emoji!important;
    font-size:12px
}
#greasyfork-plus .section_header{
    background-color:#670000!important;
    background-image:linear-gradient(#670000,#900)!important;
    border:1px solid transparent!important;
    color:#fff!important
}
#greasyfork-plus .field_label{
    margin-bottom:4px!important
}
#greasyfork-plus .field_label span{
    font-size:95%!important;
    font-style:italic!important;
    opacity:.8!important
}
#greasyfork-plus .field_label b{
    color:#670000!important
}
#greasyfork-plus .config_var{
    display:flex!important
}
#greasyfork-plus_customBlacklist_var,#greasyfork-plus_hiddenList_var,#greasyfork-plus_milestoneNotification_var{
    flex-direction:column!important;
    margin-left:21px!important
}
#greasyfork-plus_field_customBlacklist,#greasyfork-plus_field_milestoneNotification{
    flex:1!important
}
#greasyfork-plus_field_hiddenList{
    box-sizing:border-box!important;
    overflow:hidden!important;
    resize:none!important;
    width:100%!important
}


  `;

  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;
  transform: scale(0.7);
  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;
}
[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);

}

  `




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



})();

(async () => {

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

  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)
  ])

  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 = new RegExp(mWindow.blacklist.join('|'), 'giu');
  const hiddenList = await GM.getValue('hiddenList', []);
  const lang = document.documentElement.lang;
  const locales = mWindow.locales;

  GM_config.init({
    id,
    title,
    fields,
    css: mWindow.settingsCSS,
    events: {
      init: () => {
        if (!Array.isArray(hiddenList)) {
          GM.deleteValue('hiddenList');
          setTimeout(() => window.location.reload(false), 500);
        }

        if (GM.info.scriptHandler !== 'Userscripts') {
          GM.registerMenuCommand('Configure', () => GM_config.open());
        }
      },
      open: async (document) => {
        const textarea = document.querySelector(`#${id}_field_hiddenList`);

        const hiddenList = await GM.getValue('hiddenList', []);
        const unsavedHiddenList = GM_config.get('hiddenList') !== '' ? GM_config.get('hiddenList').split(',').map(Number) : [];

        if ((hiddenList.filter(item => !unsavedHiddenList.includes(item)).length > 0 || unsavedHiddenList.filter(item => !hiddenList.includes(item)).length > 0) && hiddenList.length !== 0) {
          GM_config.fields.hiddenList.value = hiddenList.sort((a, b) => a - b).join(', ');

          GM_config.close();
          GM_config.open();
        }

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

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

        }
      },
      save: async (forgotten) => {
        const unsavedHiddenList = forgotten.hiddenList !== '' ? forgotten.hiddenList.split(',').map(Number).filter((element) => element !== 0) : undefined;

        if (GM_config.isOpen) {
          await GM.setValue('hiddenList', Array.from(unsavedHiddenList));

          UU.alert('settings saved');
          GM_config.close();
          setTimeout(() => window.location.reload(false), 500);
        }
      }
    }
  });

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

  const { register } = VM.shortcut;
  register('ctrl-alt-s', () => {
    GM_config.open();
  });
  register('ctrl-alt-b', () => {
    toggleListDisplayingItem('blacklisted')
    // blacklistedToggled = !blacklistedToggled;
    // toggleElementVisibility('.script-list li.blacklisted');
  });
  register('ctrl-alt-h', () => {
    toggleListDisplayingItem('hidden')
    // hiddenToggled = !hiddenToggled;
    // toggleElementVisibility('.script-list li.hidden');
  });

  const addSettingsToMenu = () => {
    const menu = document.createElement('li');
    menu.classList.add(id);
    const link = document.createElement('a');
    link.setAttribute('href', '#');
    link.textContent = GM.info.script.name;
    menu.appendChild(link);
    let nav = document.querySelector('#site-nav > nav')
    nav && nav.insertBefore(menu, document.querySelector('#site-nav > nav > li:first-child'));

    menu.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      e.stopImmediatePropagation();
      GM_config.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();
  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) => {

      networkMP1 = networkMP1.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}`)
            // 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);
              });
            }
            console.warn(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
              });
            }
            console.warn(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 isInstalled = (name, namespace) => {
    return new Promise((resolve, reject) => {
      if (window.external && window.external.Violentmonkey) {
        window.external.Violentmonkey.isInstalled(name, namespace).then((data) => resolve(data));
        return;
      }

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

      resolve();
    });
  };

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

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

    for (let index = 0; index < Math.max(sv1.length, sv2.length); index++) {
      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 undefined: {
        return locales[lang] ? locales[lang].install : locales.en.install
      }
      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].reinstall : locales.en.reinstall
      }
    }
  }

  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 (GM_config.get('hideBlacklistedScripts') && GM_config.get('debugging')) {
            let scriptLink = element.querySelector('.script-link');
            if (scriptLink) { scriptLink.textContent += ' (non-latin)'; }
          }
        }
        break;
      case 'blacklist':
        if ((blacklist.test(name) || blacklist.test(description)) && !element.classList.contains('blacklisted')) {
          element.classList.add('blacklisted', 'blacklist');
          if (GM_config.get('hideBlacklistedScripts') && GM_config.get('debugging')) {
            let scriptLink = element.querySelector('.script-link');
            if (scriptLink) { scriptLink.textContent += ' (blacklist)'; }
          }
        }
        break;
      case 'customBlacklist': {
        const customBlacklist = new RegExp(GM_config.get('customBlacklist').replace(/\s/g, '').split(',').join('|'), 'giu');
        if ((customBlacklist.test(name) || customBlacklist.test(description)) && !element.classList.contains('blacklisted')) {
          element.classList.add('blacklisted', 'custom-blacklist');
          if (GM_config.get('hideBlacklistedScripts') && GM_config.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 (GM_config.get('hideHiddenScript') && GM_config.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);

          if (list) element.style.display = 'none';
        } 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;
    target.parentNode.insertBefore(p.content.firstChild, target.nextSibling);
  };

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

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

    const script = await getScriptData(scriptID);
    if (!script) return;

    const installed = await isInstalled(script.name, script.namespace)

    const update = compareVersions(script.version, installed);
    const label = installLabel(update);
    addInstallButton(element, script.code_url, label, script.version);

  }


  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 (GM_config.get('nonLatins')) hideBlacklistedScript(element, 'nonLatins');
        if (GM_config.get('blacklist')) hideBlacklistedScript(element, 'blacklist');
        if (GM_config.get('customBlacklist')) hideBlacklistedScript(element, 'customBlacklist');

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

        // install button
        if (GM_config.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 onReady = async () => {
    addSettingsToMenu();


    setTimeout(() => {
      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.getAttribute('href');

        userID = /users\/(\d+)/.exec(userID);
        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;

    // blacklisted scripts / hidden scripts / install button
    if (window.location.pathname !== userID && !/discussions/.test(window.location.pathname) && (GM_config.get('hideBlacklistedScripts') || GM_config.get('hideHiddenScript') || GM_config.get('showInstallButton'))) {


      if (document.querySelector('.script-list')) {
        foundScriptList();
      } else {

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


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

      // add options and style for blacklisted/hidden scripts
      if (GM_config.get('hideBlacklistedScripts') || GM_config.get('hideHiddenScript')) {
        addOptions();
        UU.addStyle(mWindow.pageCSS);
      }
    }

    // total installs
    if (GM_config.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 dailyOption = convertLi(document.querySelector('#script-list-sort .list-option:nth-child(1)'));
      dailyOption && dailyOption.insertAdjacentHTML('beforeend', `<span> (${dailyInstallsSum.toLocaleString()})</span>`);

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

    // milestone notification
    if (GM_config.get('milestoneNotification')) {
      const milestones = GM_config.get('milestoneNotification').replace(/\s/g, '').split(',').map(Number);

      if (!userID) return;

      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 (GM.info.scriptHandler !== 'Userscripts') {
          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);
        }

      }

    }
  }



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

})();