// ==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 & Davide // @namespace https://github.com/iFelix18 // @icon https://www.google.com/s2/favicons?domain=https://greasyfork.org // @description Adds various features and improves the Greasy Fork experience // @description:de Fügt verschiedene Funktionen hinzu und verbessert das Greasy Fork-Erlebnis // @description:es Agrega varias funciones y mejora la experiencia de Greasy Fork // @description:fr Ajoute diverses fonctionnalités et améliore l'expérience Greasy Fork // @description:it Aggiunge varie funzionalità e migliora l'esperienza di Greasy Fork // @description:ru Добавляет различные функции и улучшает работу с Greasy Fork // @description:zh-CN 添加各种功能并改善 Greasy Fork 体验 // @copyright 2023, CY Fung (https://greasyfork.org/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/shortcut@1.3.0/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 // @downloadURL none // ==/UserScript== /* global $, GM_config, UU, VM */ /* eslint-disable unicorn/prefer-top-level-await */ // -------- UU Fucntion - original code: https://fastly.jsdelivr.net/npm/@ifelix18/utils@6.5.0/lib/index.min.js -------- // optimized by CY Fung to remove $ dependency and 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/utils@6.5.0/lib/index.min.js -------- const mWindow = (() => { const fields = { hideBlacklistedScripts: { label: 'Hide blacklisted scripts:
Choose which lists to activate in the section below, press Ctrl + Alt + B to show Blacklisted scripts', section: ['Features'], labelPos: 'right', type: 'checkbox', default: true }, hideHiddenScript: { label: 'Hide scripts:
Add a button to hide the script
See and edit the list of hidden scripts below, press Ctrl + Alt + H to show Hidden script', labelPos: 'right', type: 'checkbox', default: true }, showInstallButton: { label: 'Install button:
Add to the scripts list a button to install the script directly', labelPos: 'right', type: 'checkbox', default: true }, showTotalInstalls: { label: 'Installations:
Shows the number of daily and total installations on the user profile', labelPos: 'right', type: 'checkbox', default: true }, milestoneNotification: { label: 'Milestone notifications:
Get notified whenever your total installs got over any of these milestone
Separate milestones with a comma, leave blank to turn off notifications
', 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:
This list blocks all scripts with non-Latin characters in the title/description', section: ['Lists'], labelPos: 'right', type: 'checkbox', default: false // not true }, blacklist: { label: 'Blacklist:
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"', labelPos: 'right', type: 'checkbox', default: true }, customBlacklist: { label: 'Custom Blacklist:
Personal blacklist defined by a set of unwanted words
Separate unwanted words with a comma (example: YouTube, Facebook, pizza), leave blank to disable this list
', labelPos: 'left', type: 'text', title: 'Separate unwanted words with a comma!', size: 150, default: '' }, hiddenList: { label: 'Hidden Scripts:
Block individual undesired scripts by their unique IDs
Separate IDs with a comma
', 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 = `
${GM.info.script.name} Lists:
`; 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 = `${blockLabel(isHidden)}`; const blockButtonHeaderHTML = `${blockLabel(isHidden)}`; 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', `${label} ${version}`); }; 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', ` (${dailyInstallsSum.toLocaleString()})`); const totalOption = convertLi(document.querySelector('#script-list-sort .list-option:nth-child(2)')); totalOption && totalOption.insertAdjacentHTML('beforeend', ` (${totalInstallsSum.toLocaleString()})`); } // 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); } }); })();