Greasy Fork

Greasy Fork is available in English.

Greasy Fork++

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

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

您需要先安装一款用户脚本管理器扩展,例如 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.0
// @require            https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@6a82709680bbeb3bd2041a4345638b628d537c96/gm_config.js
// @require            https://cdn.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 */




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

// GM.deleteValue('lastMilestone')
// GM.deleteValue('hiddenList')

//  -------- UU Fucntion - original code: https://fastly.jsdelivr.net/npm/@ifelix18/[email protected]/lib/index.min.js  --------
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}`);
  };

  const short = (text, length) => {
    return text.split(" ").length > Number(length)
      ? `${text.split(" ", Number(length)).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];
        document.querySelectorAll(selector).forEach((element) => {
          const onlyVisible = config.onlyVisible;
          if (!element.getAttribute(scriptId)) {
            if ((!onlyVisible || isElementVisible(element)) && element.matches(selector)) {
              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 css = '#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 style = '.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}'




  return { fields, logo, locales, blacklist, css, style }



})();

(async () => {

  await Promise.all([
    fixValue('hiddenList', [], v => (console.log(v) || v) && typeof v === 'object' && typeof v.length === 'number' && (v.length === 0 || typeof v[0] === 'number')),
    fixValue('lastMilestone', 0, v => (console.log(v) || 1) && 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.css,
    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', () => {
    document.querySelectorAll('.script-list li.blacklisted').forEach(el => el.style.display = el.style.display === 'none' ? '' : 'none');
  });
  register('ctrl-alt-h', () => {
    document.querySelectorAll('.script-list li.hidden').forEach(el => el.style.display = el.style.display === 'none' ? '' : 'none');
  });

  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 addOptions = () => {
    const html = `<div class="list-option-group" id="${id}-options">${GM.info.script.name} Lists:<ul><li class="list-option blacklisted"><a href="/blacklist" onclick="return false;">Blacklisted scripts (${document.querySelectorAll('.script-list li.blacklisted').length})</a></li><li class="list-option hidden"><a href="/blacklist" onclick="return false;">Hidden scripts (${document.querySelectorAll('.script-list li.hidden').length})</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', () => {
      toggleElementVisibility('.script-list li.blacklisted');
    });

    const hiddenOption = document.querySelector(`#${id}-options li.hidden`);
    hiddenOption && hiddenOption.addEventListener('click', () => {
      toggleElementVisibility('.script-list li.hidden');
    });
  };

  const toggleElementVisibility = (selector) => {
    document.querySelectorAll(selector).forEach((element) => {
      element.style.display = element.style.display === 'none' ? 'list-item' : 'none';
    });
  };


  /**
   * 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) => {
    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;
        Promise.resolve()
          .then(() => fetch(url, {
            method: 'GET',
            cache: 'force-cache',
            credentials: 'omit',
            headers: new Headers({
              'Cache-Control': `max-age=${maxAgeInSeconds}`,
            }),
          }))
          .then((response) => {
            unlock();
            UU.log(`${response.status}: ${response.url}`)
            return response.json()
          })
          .then((data) => resolve(data))
          .catch((e) => {
            unlock();
            reject(e)
          })

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

    });
  }

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

    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;

        Promise.resolve()
          .then(() => fetch(url, {
            method: 'GET',
            cache: 'force-cache',
            credentials: 'omit',
            headers: new Headers({
              'Cache-Control': `max-age=${maxAgeInSeconds}`,
            }),
          }))
          .then((response) => {
            unlock();
            UU.log(`${response.status}: ${response.url}`)
            return response.json()
          })
          .then((data) => resolve(data))
          .catch((e) => {
            unlock();
            reject(e)
          })



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

    });
  }
  const getTotalInstalls = (data) => {
    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;
    // if is in hiddenList hide it
    if (hiddenList.indexOf(id) !== -1) {
      element.classList.add('hidden');
      if (GM_config.get('hideHiddenScript') && GM_config.get('debugging')) {
        let link = element.querySelector('.script-link');
        if (link) link.innerText += ' (hidden)';
      }
    }

    // add button to hide the script
    let blockButtonHTML = `<span class=block-button role=button style=cursor:pointer;font-size:70%;white-space:nowrap>${blockLabel(element.classList.contains('hidden'))}</span>`;
    let blockButtonHeaderHTML = `<span class=block-button role=button style=cursor:pointer;font-size:50%;margin-left:1ex;white-space:nowrap>${blockLabel(element.classList.contains('hidden'))}</span>`;

    element.querySelector('.badge-js, .badge-css') && element.querySelector('.badge-js, .badge-css').insertAdjacentHTML('beforebegin', blockButtonHTML);
    element.querySelector('header h2') && element.querySelector('header h2').insertAdjacentHTML('beforeend', blockButtonHeaderHTML);

    let button = element.querySelector('.block-button');

    button && button.addEventListener('click', (event) => {
      event.stopPropagation();
      event.stopImmediatePropagation();

      // ...if it is not in the list add it and hide it...
      if (hiddenList.indexOf(id) === -1) {
        hiddenList.push(id);

        GM.setValue('hiddenList', hiddenList);

        if (list) {
          element.style.display = 'none';
          element.classList.add('hidden');
          let blockBtn = element.querySelector('.block-button')
          if (blockBtn) blockBtn.innerText = blockLabel(element.classList.contains('hidden'));
          if (GM_config.get('hideHiddenScript') && GM_config.get('debugging')) {
            let scriptLink = element.querySelector('.script-link');
            if (scriptLink) scriptLink.innerHTML += ' (hidden)';
          }
        } else {
          element.classList.add('hidden');
          element.querySelector('.block-button').innerText = blockLabel(element.classList.contains('hidden'));
          if (GM_config.get('hideHiddenScript') && GM_config.get('debugging')) {
            let scriptLink = element.querySelector('.script-link');
            if (scriptLink) scriptLink.innerHTML += ' (hidden)';
          }
        }
      } else { // ...else remove it
        hiddenList.splice(hiddenList.indexOf(id), 1);

        GM.setValue('hiddenList', hiddenList);

        element.classList.remove('hidden');
        element.querySelector('.block-button').innerText = blockLabel(element.classList.contains('hidden'));
        if (GM_config.get('hideHiddenScript') && GM_config.get('debugging')) {
          let scriptLink = element.querySelector('.script-link');
          if (scriptLink) scriptLink.innerHTML = scriptLink.innerHTML.replace(' (hidden)', '');
        }
      }
    });
  }
    ;

  const addInstallButton = (element, url, label, version) => {
    element.querySelector('.badge-js, .badge-css') && element.querySelector('.badge-js, .badge-css').insertAdjacentHTML('afterend', `<a class="install-link" href="${url}" style="float:right;zoom:.7;-moz-transform:scale(.7);text-decoration:none">${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 onReady = async () => {
    addSettingsToMenu();

    const userLink = document.querySelector('.user-profile-link a');
    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'))) {

      // for each script in the list
      UU.observeCreation('.script-list', async (scriptList) => {
        const scriptElements = scriptList.querySelectorAll('li[data-script-id]');
        for (const element of scriptElements) {

          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)
          }
        }
      });

      // 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.style);
      }
    }

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

      const totalOption = document.querySelector('#script-list-sort .list-option:nth-child(2) a');
      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();

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

      if (milestone <= lastMilestone) return;

      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);
    }
  });

})()