Greasy Fork镜像++

加入多種功能並改善Greasy Fork镜像的體驗

目前為 2023-08-25 提交的版本,檢視 最新版本

// ==UserScript==
// @name               Greasy Fork镜像++
// @author             CY Fung <https://gf.qytechs.cn/users/371179> & Davide <[email protected]>
// @namespace          https://github.com/iFelix18
// @icon               https://www.google.com/s2/favicons?domain=https://gf.qytechs.cn
// @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://gf.qytechs.cn/users/371179); 2021, Davide (https://github.com/iFelix18)
// @license            MIT
// @version            3.0.9
// @require            https://fastly.jsdelivr.net/gh/sizzlemctwizzle/GM_config@06f2015c04db3aaab9717298394ca4f025802873/gm_config.js
// @require            https://fastly.jsdelivr.net/npm/@violentmonkey/[email protected]/dist/index.min.js
// @match              *://gf.qytechs.cn/*
// @match              *://sleazyfork.org/*
// @connect            gf.qytechs.cn
// @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
// @run-at             document-start
// @inject-into        page
// ==/UserScript==

/* global GM_config, VM, GM */

//  -------- UU Fucntion - original code: https://fastly.jsdelivr.net/npm/@ifelix18/[email protected]/lib/index.min.js  --------
// optimized by CY Fung to remove $ dependency and observe creation
const UU = (function () {
  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 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/[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: '更新到'
    },
    '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 = [ /* 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{
  --config-var-display: flex;
}
#greasyfork-plus *{
    font-family:Open Sans,sans-serif,Segoe UI Emoji !important;
    font-size:12px
}
#greasyfork-plus .section_header{
    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);
}
#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%
}

#greasyfork-plus_wrapper {
  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_buttons_holder {
  position: fixed;
  bottom: 0;
  right: 0;
  margin: 0 12px 6px 0;
}

#greasyfork-plus .saveclose_buttons[class] {
  padding: 4px 14px;
  margin: 6px;
}

  `;

  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;

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

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

        const hiddenList = await GM.getValue('hiddenList', []);
        const unsavedHiddenList = gmc.get('hiddenList') !== '' ? gmc.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) {
          gmc.fields.hiddenList.value = hiddenList.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));

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

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

          UU.alert('settings saved');
          gmc.close();
          setTimeout(() => window.location.reload(false), 500);
        }
      }
    }
  });
  gmc.initialized = new Promise(r => (gmc.initializedResolve = r));
  await gmc.initialized.then();

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

  const { register } = VM.shortcut;
  register('ctrl-alt-s', () => {
    gmc.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();
      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) => {

      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);
              });
            }
            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 (gmc.get('hideBlacklistedScripts') && gmc.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 (gmc.get('hideBlacklistedScripts') && gmc.get('debugging')) {
            let scriptLink = element.querySelector('.script-link');
            if (scriptLink) { scriptLink.textContent += ' (blacklist)'; }
          }
        }
        break;
      case 'customBlacklist': {
        const customBlacklist = new RegExp(gmc.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 (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);

          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 (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 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) && (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
      if (gmc.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 (gmc.get('hideBlacklistedScripts') || gmc.get('hideHiddenScript')) {
        addOptions();
        UU.addStyle(mWindow.pageCSS);
      }
    }

    // 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 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 (gmc.get('milestoneNotification')) {
      const milestones = gmc.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' && 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);
        }

      }

    }
  }



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

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址