let userjs = (self.userjs = {})
// Skip text/plain documents.
if (
(document instanceof Document ||
(document instanceof XMLDocument &&
document.createElement('div') instanceof HTMLDivElement)) &&
/^image\/|^text\/plain/.test(document.contentType || '') === false &&
(self.userjs instanceof Object === false || userjs.UserJS !== true)
) {
userjs = self.userjs = { UserJS: true }
}
let cfg = {}
let lang = {}
let legacyMsg = null
//#region Console
const err = (...msg) => {
console.error(
'[%cUserJS%c] %cERROR',
'color: rgb(29, 155, 240);',
'',
'color: rgb(249, 24, 128);',
...msg,
)
}
const info = (...msg) => {
console.info(
'[%cUserJS%c] %cINF',
'color: rgb(29, 155, 240);',
'',
'color: rgb(0, 186, 124);',
...msg,
)
}
const log = (...msg) => {
console.log(
'[%cUserJS%c] %cLOG',
'color: rgb(29, 155, 240);',
'',
'color: rgb(219, 160, 73);',
...msg,
)
}
//#endregion
const hasOwn = Object.hasOwn || Object.prototype.hasOwnProperty.call
const normalizeTarget = (target, root = document) => {
if (typeof target === 'string') {
return Array.from(root.querySelectorAll(target))
}
if (target instanceof Element) {
return [target]
}
if (target === null) {
return []
}
if (Array.isArray(target)) {
return target
}
return Array.from(target)
}
class dom {
static attr(target, attr, value = undefined) {
for (const elem of normalizeTarget(target)) {
if (value === undefined) {
return elem.getAttribute(attr)
}
if (value === null) {
elem.removeAttribute(attr)
} else {
elem.setAttribute(attr, value)
}
}
}
static create(a) {
if (typeof a === 'string') {
return document.createElement(a)
}
}
static prop(target, prop, value = undefined) {
for (const elem of normalizeTarget(target)) {
if (value === undefined) {
return elem[prop]
}
elem[prop] = value
}
}
static text(target, text) {
const targets = normalizeTarget(target)
if (text === undefined) {
return targets.length !== 0 ? targets[0].textContent : undefined
}
for (const elem of targets) {
elem.textContent = text
}
}
}
dom.cl = class {
static add(target, name) {
if (Array.isArray(name)) {
for (const elem of normalizeTarget(target)) {
elem.classList.add(...name)
}
} else {
for (const elem of normalizeTarget(target)) {
elem.classList.add(name)
}
}
}
static remove(target, name) {
if (Array.isArray(name)) {
for (const elem of normalizeTarget(target)) {
elem.classList.remove(...name)
}
} else {
for (const elem of normalizeTarget(target)) {
elem.classList.remove(name)
}
}
}
static toggle(target, name, state) {
let r
for (const elem of normalizeTarget(target)) {
r = elem.classList.toggle(name, state)
}
return r
}
static has(target, name) {
for (const elem of normalizeTarget(target)) {
if (elem.classList.contains(name)) {
return true
}
}
return false
}
}
const isGM = typeof GM !== 'undefined'
const isMobile = /Mobile|Tablet/.test(navigator.userAgent)
const navLang = navigator.language.split('-')[0] ?? 'en'
/**
* Object is Null
* @param {Object} obj - Object
* @returns {boolean} Returns if statement true or false
*/
const isNull = (obj) => {
return Object.is(obj, null) || Object.is(obj, undefined)
}
/**
* Object is Blank
* @param {(Object|Object[]|string)} obj - Array, Set, Object or String
* @returns {boolean} Returns if statement true or false
*/
const isBlank = (obj) => {
return (
(typeof obj === 'string' && Object.is(obj.trim(), '')) ||
(obj instanceof Set && Object.is(obj.size, 0)) ||
(Array.isArray(obj) && Object.is(obj.length, 0)) ||
(obj instanceof Object &&
typeof obj.entries !== 'function' &&
Object.is(Object.keys(obj).length, 0))
)
}
/**
* Object is Empty
* @param {(Object|Object[]|string)} obj - Array, object or string
* @returns {boolean} Returns if statement true or false
*/
const isEmpty = (obj) => isNull(obj) || isBlank(obj)
const isFunction = (obj) => {
return typeof obj === 'function' || obj instanceof Function
}
/**
* setTimeout w/ Promise
* @param {number} ms - Timeout in milliseconds (ms)
* @returns {Promise} Promise object
*/
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const alang = []
const defcfg = {
injection: 'interactive',
cache: true,
autoexpand: false,
filterlang: false,
sleazyredirect: false,
time: 10000,
blacklist: [
{
enabled: true,
regex: true,
flags: '',
name: 'Blacklist 1',
url: '(gov|cart|checkout|login|join|signin|signup|sign-up|password|reset|password_reset)',
},
{
enabled: true,
regex: true,
flags: '',
name: 'Blacklist 2',
url: '(pay|bank|money|localhost|authorize|checkout|bill|wallet|router)',
},
{
enabled: true,
regex: false,
flags: '',
name: 'Blacklist 3',
url: 'https://home.bluesnap.com',
},
{
enabled: true,
regex: false,
flags: '',
name: 'Blacklist 4',
url: ['zalo.me', 'skrill.com'],
},
],
engines: [
{
enabled: true,
name: 'greasyfork',
url: 'https://greasyfork.org',
},
{
enabled: true,
name: 'sleazyfork',
url: 'https://sleazyfork.org',
},
{
enabled: false,
name: 'openuserjs',
url: 'https://openuserjs.org/?q=',
},
{
enabled: false,
name: 'github',
url: 'https://api.github.com/search/code?q=',
token: '',
},
],
}
const langs = {
en: {
legacy: 'PLEASE RESET YOUR CONFIG!',
createdby: 'Created by',
name: 'Name',
daily: 'Daily Installs',
close: 'Close',
filterA: 'Filter',
max: 'Maximize',
min: 'Minimize',
search: 'Search',
searcher: 'Title | Description | Author...',
install: 'Install',
issue: 'New Issue',
version: 'Version',
updated: 'Last Updated',
total: 'Total Installs',
rating: 'Ratings',
good: 'Good',
ok: 'Ok',
bad: 'Bad',
created: 'Created',
redirect: 'Greasy Fork for adults',
filter: 'Filter out other languages',
dtime: 'Display Timeout',
save: 'Save',
},
es: {
legacy: 'PLEASE RESET YOUR CONFIG!',
createdby: 'Created by',
name: 'Name',
daily: 'Instalaciones diarias',
close: 'Ya no se muestra',
filterA: 'Filtro',
max: 'Maximizar',
min: 'Minimizar',
search: 'Busque en',
searcher: 'Título | Descripción | Autor...',
install: 'Instalar',
issue: 'Nueva edición',
version: 'Versión',
updated: 'Última actualización',
total: 'Total de instalaciones',
rating: 'Clasificaciones',
good: 'Bueno',
ok: 'Ok',
bad: 'Malo',
created: 'Creado',
redirect: 'Greasy Fork para adultos',
filter: 'Filtrar otros idiomas',
dtime: 'Mostrar el tiempo de espera',
save: 'Guardar',
},
ru: {
legacy: 'PLEASE RESET YOUR CONFIG!',
createdby: 'Created by',
name: 'Name',
daily: 'Ежедневные установки',
close: 'Больше не показывать',
filterA: 'Фильтр',
max: 'Максимизировать',
min: 'Минимизировать',
search: 'Поиск',
searcher: 'Название | Описание | Автор...',
install: 'Установите',
issue: 'Новый выпуск',
version: 'Версия',
updated: 'Последнее обновление',
total: 'Всего установок',
rating: 'Рейтинги',
good: 'Хорошо',
ok: 'Хорошо',
bad: 'Плохо',
created: 'Создано',
redirect: 'Greasy Fork для взрослых',
filter: 'Отфильтровать другие языки',
dtime: 'Тайм-аут отображения',
save: 'Сохранить',
},
ja: {
legacy: 'PLEASE RESET YOUR CONFIG!',
createdby: 'Created by',
name: 'Name',
daily: 'デイリーインストール',
close: '表示されなくなりました',
filterA: 'フィルター',
max: '最大化',
min: 'ミニマム',
search: '検索',
searcher: 'タイトル|説明|著者...',
install: 'インストール',
issue: '新刊のご案内',
version: 'バージョン',
updated: '最終更新日',
total: '総インストール数',
rating: 'レーティング',
good: 'グッド',
ok: '良い',
bad: '悪い',
created: '作成',
redirect: '大人のGreasyfork',
filter: '他の言語をフィルタリングする',
dtime: '表示タイムアウト',
save: '拯救',
},
fr: {
createdby: 'Created by',
name: 'Name',
daily: 'Installations quotidiennes',
close: 'Ne plus montrer',
filterA: 'Filtre',
max: 'Maximiser',
min: 'Minimiser',
search: 'Recherche',
searcher: 'Titre | Description | Auteur...',
install: 'Installer',
issue: 'Nouveau numéro',
version: 'Version',
updated: 'Dernière mise à jour',
total: 'Total des installations',
rating: 'Notations',
good: 'Bon',
ok: 'Ok',
bad: 'Mauvais',
created: 'Créé',
redirect: 'Greasy Fork pour les adultes',
filter: 'Filtrer les autres langues',
dtime: "Délai d'affichage",
save: 'Sauvez',
},
zh: {
legacy: 'PLEASE RESET YOUR CONFIG!',
createdby: 'Created by',
name: 'Name',
daily: '日常安装',
close: '不再显示',
filterA: '过滤器',
max: '最大化',
min: '最小化',
search: '搜索',
searcher: '标题|描述|作者...',
install: '安装',
issue: '新问题',
version: '版本',
updated: '最后更新',
total: '总安装量',
rating: '评级',
good: '好的',
ok: '好的',
bad: '不好',
created: '创建',
redirect: '大人的Greasyfork',
filter: '过滤掉其他语言',
dtime: '显示超时',
save: '拯救',
},
nl: {
legacy: 'PLEASE RESET YOUR CONFIG!',
createdby: 'Created by',
name: 'Name',
daily: 'Dagelijkse Installaties',
close: 'Sluit',
filterA: 'Filter',
max: 'Maximaliseer',
min: 'Minimaliseer',
search: 'Zoek',
searcher: 'Titel | Beschrijving | Auteur...',
install: 'Installeer',
issue: 'Nieuw Issue',
version: 'Versie',
updated: 'Laatste Update',
total: 'Totale Installaties',
rating: 'Beoordeling',
good: 'Goed',
ok: 'Ok',
bad: 'Slecht',
created: 'Aangemaakt',
redirect: 'Greasy Fork voor volwassenen',
filter: 'Filter andere talen',
dtime: 'Weergave timeout',
save: 'Opslaan',
},
}
/**
* preventDefault + stopPropagation
* @param {Object} e - Selected Element
*/
const halt = (e) => {
e.preventDefault()
e.stopPropagation()
}
/**
* Add Event Listener
* @param {Object} root - Selected Element
* @param {string} type - root Event Listener
* @param {Function} callback - Callback function
* @param {Object} [options={}] - (Optional) Options
* @returns {Object} Returns selected Element
*/
const ael = (root = document, type, callback, options = {}) => {
try {
root =
root ||
document.documentElement ||
document.body ||
document.head ||
document.querySelector(':root')
if (isMobile && type === 'click') {
type = 'mouseup'
root.addEventListener('touchstart', callback)
root.addEventListener('touchend', callback)
}
if (type === 'fclick') {
type = 'click'
}
return root.addEventListener(type, callback, options)
} catch (ex) {
return err(ex)
}
}
/**
* Prefix for document.querySelectorAll()
* @param {Object} element - Elements for query selection
* @param {Object} [root=document] - Root selector Element
* @returns {Object} Returns root.querySelectorAll(element)
*/
const qsA = (element, root = document) => {
root =
root ||
document.documentElement ||
document.body ||
document.head ||
document.querySelector(':root')
return root.querySelectorAll(element)
}
/**
* Prefix for document.querySelector()
* @param {Object} element - Element for query selection
* @param {Object} [root=document] - Root selector Element
* @returns {Object} Returns root.querySelector(element)
*/
const qs = (element, root = document) => {
root =
root ||
document.documentElement ||
document.body ||
document.head ||
document.querySelector(':root')
return root.querySelector(element)
}
/**
* Prefix for document.querySelector() w/ Promise
* @param {Object} element - Element for query selection
* @param {Object} [root=document] - Root selector Element
* @returns {Object} Returns root.querySelector(element)
*/
const query = async (element, root = document) => {
const waitForElement = async () => {
while (isNull(root.querySelector(element))) {
await new Promise((resolve) => requestAnimationFrame(resolve))
}
return root.querySelector(element)
}
return Promise.any([
waitForElement(),
delay(5000).then(() =>
Promise.reject(new Error('Unable to locate element')),
),
])
}
/**
* Create/Make Element
* @param {string} element - Element to create
* @param {string} cname - (Optional) Element class name
* @param {Object} [attrs={}] - (Optional) Element attributes
* @returns {Object} Returns created Element
*/
const make = (element, cname, attrs = {}) => {
let el
try {
el = dom.create(element)
if (!isEmpty(cname)) {
if (typeof cname === 'string') {
el.className = cname
}
}
if (!isEmpty(attrs)) {
/**
* Form Attributes of Element
* @param {Object} element - Element
* @param {Object} [attrib={}] - (Optional) Element attributes
*/
const formAttrs = (element, attr = {}) => {
for (const key in attr) {
if (typeof attr[key] === 'object') {
formAttrs(element[key], attr[key])
} else if (typeof attr[key] === 'function') {
if (key === 'container') {
key()
continue
}
if (/^on/.test(key)) {
element[key] = attr[key]
continue
}
ael(element, key, attrs[key])
// if (/^on/.test(key)) {
// const evt = /^on(.+)/.exec(key)
// ael(element, evt[1], attrs[key])
// } else {
// ael(element, key, attrs[key])
// }
} else {
element[key] = attr[key]
}
}
}
formAttrs(el, attrs)
// for (const key in attrs) {
// if (typeof attrs[key] === 'object') {
// el[key] = attrs[key]
// }
// if (key === 'dataset') {
// for (const key2 in attrs[key]) {
// el[key][key2] = attrs[key][key2]
// }
// } else if (key === 'click') {
// ael(el, 'click', attrs[key])
// } else if (key === 'mouseenter') {
// ael(el, 'mouseenter', attrs[key])
// } else if (key === 'mouseleave') {
// ael(el, 'mouseleave', attrs[key])
// } else if (key === 'container') {
// if (typeof key === 'function') {
// key()
// }
// } else {
// el[key] = attrs[key]
// }
// }
}
return el
} catch (ex) {
err(ex)
return el
}
}
const iconSVG = {
cfg: '',
close:
'',
filter:
'',
fsClose:
'',
fsOpen:
'',
fullscreen:
'',
gf: '',
gh: '',
hide: '',
install:
'',
issue:
'',
nav: '',
plus: '',
search:
'',
}
const Timeout = class {
constructor() {
this.ids = []
}
set(delay, reason) {
return new Promise((resolve, reject) => {
const id = setTimeout(() => {
isNull(reason) ? resolve() : reject(reason)
this.clear(id)
}, delay)
this.ids.push(id)
})
}
clear(...ids) {
this.ids = this.ids.filter((id) => {
if (ids.includes(id)) {
clearTimeout(id)
return false
}
return true
})
}
}
const MU = {
/**
* Get Value
* @param {string} key - Key to get the value of
* @param {Object} def - Fallback default value of key
* @returns {Object} Value or default value of key
* @link https://violentmonkey.github.io/api/gm/#gm_getvalue
* @link https://developer.mozilla.org/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
*/
getValue(key, def = {}) {
try {
const params = JSON.stringify(def)
if (isGM) {
return JSON.parse(GM_getValue(key, params))
}
return window.localStorage.getItem(`MUJS-${key}`)
? JSON.parse(window.localStorage.getItem(`MUJS-${key}`))
: def
} catch (ex) {
err(ex)
}
},
/**
* Get info of script
* @returns {Object} Script info
* @link https://violentmonkey.github.io/api/gm/#gm_info
*/
info: isGM
? GM_info
: {
script: {
icon: '',
name: 'Magic Userscript+',
namespace: 'https://github.com/magicoflolis/Userscript-Plus',
updateURL: 'https://github.com/magicoflolis/Userscript-Plus/releases',
version: 'Bookmarklet',
},
},
/**
* Open a new window
* @param {string} url - URL of webpage to open
* @param {object} params - GM parameters
* @returns {object} GM_openInTab object with Window object as a fallback
* @link https://violentmonkey.github.io/api/gm/#gm_openintab
* @link https://developer.mozilla.org/docs/Web/API/Window/open
*/
openInTab(
url,
params = {
active: true,
insert: true,
},
features,
) {
if (!isGM && isBlank(params)) {
params = '_blank'
}
if (features) {
return window.open(url, params, features)
}
return isGM ? GM_openInTab(url, params) : window.open(url, params)
},
/**
* Set value
* @param {string} key - Key to set the value of
* @param {Object} v - Value of key
* @returns {Promise} Saves key to either GM managed storage or webpages localstorage
* @link https://violentmonkey.github.io/api/gm/#gm_setvalue
* @link https://developer.mozilla.org/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
*/
setValue(key, v) {
return new Promise((resolve) => {
v = typeof v !== 'string' ? JSON.stringify(v ?? {}) : v
if (isGM) {
resolve(GM_setValue(key, v))
} else {
resolve(window.localStorage.setItem(`MUJS-${key}`, v))
}
})
},
xmlRequest: isGM
? GM_xmlhttpRequest
: () => {
return {}
},
/**
* Fetch a URL with fetch API as fallback
*
* When GM is supported, makes a request like XMLHttpRequest, with some special capabilities, not restricted by same-origin policy
* @param {string} url - The URL to fetch
* @param {string} method - Fetch method
* @param {string} responseType - Response type
* @param {Object} data - Fetch parameters
* @param {boolean} forcefetch - Force use fetch API
* @returns {*} Fetch results
* @link https://violentmonkey.github.io/api/gm/#gm_xmlhttprequest
* @link https://developer.mozilla.org/docs/Web/API/Fetch_API
*/
fetchURL(
url = '',
method = 'GET',
responseType = 'json',
data = {},
forcefetch = false,
) {
return new Promise((resolve, reject) => {
responseType = responseType.toLocaleLowerCase()
const params = {
method: method.toLocaleUpperCase(),
...data,
}
if (isGM && !forcefetch) {
if (params.credentials) {
Object.assign(params, {
anonymous: false,
})
if (Object.is(params.credentials, 'omit')) {
Object.assign(params, {
anonymous: true,
})
}
delete params.credentials
}
} else {
if (params.onprogress) {
delete params.onprogress
}
}
if (/buffer/i.test(responseType)) {
fetch(url, params)
.then((response) => {
if (!response.ok) reject(response)
resolve(response.arrayBuffer())
})
.catch(reject)
} else if (isGM && !forcefetch) {
MU.xmlRequest({
url,
responseType,
...params,
onerror: reject,
onload: (r) => {
if (r.status !== 200) reject(new Error(`${r.status} ${url}`))
if (/basic/i.test(responseType)) resolve(r)
resolve(r.response)
},
})
} else {
fetch(url, params)
.then((response) => {
if (!response.ok) reject(response)
if (/json/i.test(responseType)) {
resolve(response.json())
} else if (/text/i.test(responseType)) {
resolve(response.text())
} else if (/blob/i.test(responseType)) {
resolve(response.blob())
} else if (/document/i.test(responseType)) {
const data = new DOMParser().parseFromString(
response.text(),
'text/html',
)
resolve(data)
}
resolve(response)
})
.catch(reject)
}
})
},
}
MU.storage = class {
static getItem(key) {
return window.localStorage.getItem(key)
}
static has(key) {
return !isNull(this.getItem(key))
}
static setItem(key, value) {
return window.localStorage.setItem(key, value)
}
static remove(key) {
return window.localStorage.removeItem(key)
}
}
const Container = class {
constructor() {
this.remove = this.remove.bind(this)
this.onFrameLoad = this.onFrameLoad.bind(this)
this.supported = isFunction(
document.createElement('main-userjs').attachShadow,
)
this.ready = false
if (this.supported) {
this.frame = make('main-userjs', '', {
dataset: {
insertedBy: 'userscript-plus',
role: 'primary-container',
},
})
this.root = this.frame.attachShadow({ mode: 'open' })
this.ready = true
} else {
this.frame = make('iframe', 'mujs-iframe', {
dataset: {
insertedBy: 'userscript-plus',
role: 'primary-iframe',
},
loading: 'lazy',
src: 'about:blank',
style:
'position: fixed;bottom: 1rem;right: 1rem;height: 525px;width: 90%;margin: 0px 1rem;z-index: 100000000000000020 !important;',
onload: this.onFrameLoad,
})
}
ael(window.self, 'beforeunload', this.remove)
// info('Container:', this)
}
inject() {
try {
if (this.ready === false) {
this.waitFor(this.ready === false).then(this.inject)
}
if (!document.body) {
query('body').then(this.inject)
return
}
document.body.appendChild(this.frame)
} catch (ex) {
err(ex)
}
}
remove() {
this.frame.remove()
}
async onReady(callback) {
await this.waitFor(this.ready === false)
if (isFunction(callback)) {
callback(this.root)
}
}
onFrameLoad(iFrame) {
this.root = iFrame.target.contentDocument.documentElement
this.ready = true
this.root.classList.add('mujs-iframe')
iFrame.target.contentDocument.body.classList.add('mujs-iframe')
}
async waitFor(obj) {
while (obj) {
await new Promise((resolve) => requestAnimationFrame(resolve))
}
return true
}
}
const container = new Container()
const sleazyRedirect = () => {
if (!/greasyfork\.org/.test(location.hostname) && cfg.sleazyredirect) {
return
}
const otherSite = /greasyfork\.org/.test(location.hostname)
? 'sleazyfork'
: 'greasyfork'
qs('span.sign-in-link')
? /scripts\/\d+/.test(location.href)
? !qs('#script-info') &&
(otherSite == 'greasyfork' || qs('div.width-constraint>section>p>a'))
? location.assign(
location.href.replace(
/\/\/([^.]+\.)?(greasyfork|sleazyfork)\.org/,
'//$1' + otherSite + '.org',
),
)
: false
: false
: false
}
const main = (injCon) => {
try {
//#region Static Elements
const table = make('table')
const tabbody = make('tbody')
const tabhead = make('thead')
const main = make('mu-js', 'main hidden')
const tbody = make('mu-js', 'mujs-body')
const header = make('mu-js', 'mujs-header-prim')
const cfgpage = make('mujs-row', 'mujs-cfg hidden')
const countframe = make('mujs-column')
const btnframe = make('mujs-column')
const btnHandles = make('mujs-column', 'btn-handles')
const gfcounter = make('count-frame', '', {
title: 'https://greasyfork.org + https://sleazyfork.org',
style: 'background: #00b7ff;',
})
const sfcounter = make('count-frame', '', {
title: 'https://openuserjs.org',
style: 'background: #ed3f14;',
})
const fsearch = make('mujs-btn', 'hidden')
const ssearch = make('mujs-btn', 'hidden')
const mainbtn = make('count-frame', 'mainbtn', {
innerHTML: '0',
})
const rateContainer = make('mujs-column', 'rate-container')
const usercss = make('style', '', {
dataset: {
insertedBy: 'userscript-plus',
role: 'primary-stylesheet',
},
innerHTML: main_css,
})
//#endregion
const template = {
bad_ratings: 0,
good_ratings: 0,
ok_ratings: 0,
daily_installs: 0,
total_installs: 0,
name: 'NOT FOUND',
description: 'NOT FOUND',
version: '0.0.0',
url: 'about:blank',
code_url: 'about:blank',
created_at: Date.now(),
code_updated_at: Date.now(),
users: [
{
name: '',
url: '',
},
],
}
const ContainerHandler = class {
constructor() {
this.cache = new Map()
this.host = location.hostname.split('.').splice(-2).join('.')
this.site = window.top.document.location.href
this.unsaved = false
this.isBlacklisted = false
this.switchRows = true
this.rebuild = false
this.siteujs = []
this.forkCount = 0
this.customCount = 0
this.showError = this.showError.bind(this)
this.cleanup = this.cleanup.bind(this)
ael(window.self, 'beforeunload', this.cleanup)
}
checkBlacklist() {
const blacklist = cfg.blacklist.filter((b) => b.enabled)
for (const b of blacklist) {
if (b.regex) {
const reg = new RegExp(b.url, b.flags)
const testurl = reg.test(this.site)
if (!testurl) continue
MUJS.isBlacklisted = true
}
if (!Array.isArray(b.url)) {
if (!this.site.includes(b.url)) continue
MUJS.isBlacklisted = true
}
for (const c of b.url) {
if (!this.site.includes(c)) continue
this.isBlacklisted = true
}
}
if (this.isBlacklisted) {
this.showError('Blacklisted')
timeoutFrame()
}
return this.isBlacklisted
}
addCustomCnt(cnt) {
this.customCount += cnt
this.updateCounters()
}
addForkCnt(cnt) {
this.forkCount += cnt
this.updateCounters()
}
updateCounters() {
sfcounter.innerHTML = this.customCount
gfcounter.innerHTML = this.forkCount
mainbtn.innerHTML = this.customCount + this.forkCount
}
save() {
this.unsaved = false
MU.setValue('Config', cfg)
log('Saved:', cfg)
}
showError(ex) {
err(ex)
const txt = make('mujs-row', 'error', {
innerHTML: `ERROR: ${typeof ex === 'string' ? ex : ex.message}`,
})
tbody.prepend(txt)
}
refresh() {
this.siteujs.length = 0
this.forkCount = 0
this.customCount = 0
this.updateCounters()
tabbody.innerHTML = ''
rateContainer.innerHTML = ''
if (sh('.error')) {
sh('.error').remove()
}
}
cleanup() {
this.cache.clear()
}
}
const MUJS = new ContainerHandler()
const timeout = new Timeout()
const timeoutFrame = async () => {
if (typeof cfg.time === 'number' && !isNaN(cfg.time)) {
timeout.clear(...timeout.ids)
await timeout.set(MUJS.isBlacklisted ? cfg.time / 2 : cfg.time)
container.remove()
return timeout.clear(...timeout.ids)
}
}
const sh = (elem) => injCon.querySelector(elem)
const shA = (elem) => injCon.querySelectorAll(elem)
const sortRowBy = (cellIndex) => {
const rows = normalizeTarget(tabbody.rows).sort((tr1, tr2) => {
const t1cell = tr1.cells[cellIndex]
const t2cell = tr2.cells[cellIndex]
const tr1Text = (t1cell.firstElementChild ?? t1cell).textContent
const tr2Text = (t2cell.firstElementChild ?? t2cell).textContent
const t1pDate = Date.parse(tr1Text)
const t2pDate = Date.parse(tr2Text)
if (!Number.isNaN(t1pDate) && !Number.isNaN(t2pDate)) {
return new Date(t1pDate) - new Date(t2pDate)
}
if (Number(tr1Text) && Number(tr2Text)) {
return tr1Text - tr2Text
}
return tr1Text.localeCompare(tr2Text)
})
if (MUJS.switchRows) {
rows.reverse()
}
MUJS.switchRows = !MUJS.switchRows
tabbody.append(...rows)
}
const createjs = (ujs, issleazy) => {
for (const key in template) {
if (!hasOwn(ujs, key)) {
ujs[key] = template[key]
}
}
const eframe = make('td', 'install-btn')
const uframe = make('td', 'mujs-uframe')
const fdaily = make('td', 'mujs-list', {
innerHTML: ujs.daily_installs,
})
const fupdated = make('td', 'mujs-list', {
innerHTML: new Intl.DateTimeFormat(navigator.language).format(
new Date(ujs.code_updated_at),
),
})
const fname = make('td', 'mujs-name')
const ftitle = make('mujs-a', 'mujs-homepage', {
title: ujs.name,
innerHTML: ujs.name,
onclick(e) {
halt(e)
MU.openInTab(ujs.url)
},
})
const fver = make('mu-js', 'mujs-list', {
innerHTML: `${lang.version}: ${ujs.version}`,
})
const fcreated = make('mu-js', 'mujs-list', {
innerHTML: `${lang.created}: ${new Intl.DateTimeFormat(
navigator.language,
).format(new Date(ujs.created_at))}`,
})
const fmore = make('mujs-column', 'mujs-list hidden')
const ftotal = make('mu-js', 'mujs-list', {
innerHTML: `${lang.total}: ${ujs.total_installs}`,
})
const fratings = make('mu-js', 'mujs-list', {
title: lang.rating,
innerHTML: `${lang.rating}:`,
})
const fgood = make('mu-js', 'mujs-list mujs-ratings', {
title: lang.good,
innerHTML: ujs.good_ratings,
style:
'border-color: rgb(51, 155, 51); background-color: #339b331a; color: rgb(51, 255, 51);',
})
const fok = make('mu-js', 'mujs-list mujs-ratings', {
title: lang.ok,
innerHTML: ujs.ok_ratings,
style:
'border-color: rgb(155, 155, 0); background-color: #9b9b001a; color: rgb(255, 255, 0);',
})
const fbad = make('mu-js', 'mujs-list mujs-ratings', {
title: lang.bad,
innerHTML: ujs.bad_ratings,
style:
'border-color: rgb(155, 0, 0); background-color: #9b33331a; color: rgb(255, 0, 0);',
})
const fdesc = make('mu-js', 'mujs-list mujs-pointer', {
title: ujs.description,
innerHTML: ujs.description,
onclick(e) {
halt(e)
if (fmore.classList.contains('hidden')) {
fmore.classList.remove('hidden')
} else {
fmore.classList.add('hidden')
}
},
})
const fdwn = make('mu-jsbtn', 'install', {
title: `${lang.install} { ${ujs.name} }`,
innerHTML: `${iconSVG.install} ${lang.install}`,
onclick(e) {
halt(e)
MU.openInTab(ujs.code_url)
},
})
for (const u of ujs.users) {
const user = make('mujs-a', 'mujs-euser', {
innerHTML: u.name,
onclick(e) {
halt(e)
MU.openInTab(u.url)
},
})
uframe.append(user)
}
eframe.append(fdwn)
fmore.append(ftotal, fratings, fgood, fok, fbad, fver, fcreated)
fname.append(ftitle, fdesc, fmore)
const tr = make('tr', `frame${issleazy ? ' sf' : ''}`)
for (const e of [fname, uframe, fdaily, fupdated, eframe]) {
tr.append(e)
}
tabbody.append(tr)
}
if (!isEmpty(navigator.languages)) {
for (const nlang of navigator.languages) {
const lg = nlang.split('-')[0]
if (alang.indexOf(lg) === -1) {
alang.push(lg)
}
}
}
const makerow = (desc, type, nm, attrs = {}) => {
const sec = make('mujs-section', '', {
style: !isGM && nm === 'cache' ? 'display: none;' : '',
})
const lb = make('label')
const divDesc = make('mu-js', '', {
innerHTML: desc,
})
lb.append(divDesc)
sec.append(lb)
cfgpage.append(sec)
if (isNull(type)) {
return sec
}
const inp = make('input', '', {
type,
id: nm,
name: nm,
...attrs,
})
if (type === 'checkbox') {
const inlab = make('mu-js', 'mujs-inlab')
const la = make('label', '', {
onclick() {
inp.dispatchEvent(new MouseEvent('click'))
},
})
inlab.append(inp, la)
lb.append(inlab)
if (/(greasy|sleazy)fork|openuserjs|gi(thub|st)/gi.test(nm)) {
for (const i of cfg.engines) {
if (i.name === nm) {
inp.checked = i.enabled
ael(inp, 'change', (e) => {
MUJS.unsaved = true
i.enabled = e.target.checked
})
}
}
} else {
inp.checked = cfg[nm]
ael(inp, 'change', (e) => {
MUJS.unsaved = true
if (/filterlang/i.test(nm)) {
MUJS.rebuild = true
}
cfg[nm] = e.target.checked
})
}
} else {
lb.append(inp)
}
return inp
}
//#region Build List
const buildlist = async (host) => {
try {
if (isEmpty(host)) {
host = MUJS.host
}
MUJS.refresh()
if (MUJS.checkBlacklist()) return
const template = {}
for (const engine of cfg.engines) {
template[engine.name] = []
}
const engines = cfg.engines.filter((e) => e.enabled)
if (isEmpty(MUJS.cache.get(host))) {
MUJS.cache.set(host, template)
}
const cache = MUJS.cache.get(host)
const customRecords = []
const rateFN = (data) => {
try {
for (const [key, value] of Object.entries(
data.resources.code_search,
)) {
const txt = make('mujs-row', 'rate-info', {
innerHTML: `${key.toLocaleUpperCase()}: ${value}`,
})
rateContainer.append(txt)
}
} catch (ex) {
MUJS.showError(ex)
}
}
info('Building list', { cache, MUJS, engines })
if (!isNull(legacyMsg)) {
const txt = make('mujs-row', 'legacy-config', {
innerHTML: legacyMsg,
})
rateContainer.append(txt)
return
}
for (const engine of engines) {
const forkFN = async (data) => {
const hideData = []
const filterDeleted = data.filter((ujs) => !ujs.deleted)
const filterLang = filterDeleted.filter((d) => {
if (!cfg.filterlang) {
return true
}
const dlocal = d.locale.split('-')[0] ?? d.locale
if (alang.length > 1) {
for (const a of alang) {
if (dlocal.includes(a)) {
return true
}
}
} else if (dlocal.includes(navLang)) {
return true
}
hideData.push(d)
return false
})
let finalList = filterLang
if (!isBlank(hideData)) {
const hds = []
for (const h of hideData) {
const txt = await MU.fetchURL(h.code_url, 'GET', 'text')
const headers = txt.match(/\/\/\s@[\w][\s\S]+/g) || []
if (headers.length > 0) {
const regName = new RegExp(`// @name:${navLang}\\s+.+`, 'gi')
const findName = (regName.exec(headers[0]) ?? []).join('')
if (isEmpty(findName)) {
continue
}
const cReg = new RegExp(`// @name:${navLang}\\s+`, 'gi')
const cutName = findName.replace(cReg, '')
Object.assign(h, {
name: cutName,
})
const regDesc = new RegExp(
`// @description:${navLang}\\s+.+`,
'gi',
)
const findDesc = (regDesc.exec(headers[0]) ?? []).join('')
if (isEmpty(findDesc)) {
continue
}
Object.assign(h, {
description: findDesc.replace(
new RegExp(`// @description:${navLang}\\s+`, 'gi'),
'',
),
})
hds.push(h)
}
}
finalList = [...new Set([...hds, ...filterLang])]
}
for (const ujs of finalList) {
MUJS.siteujs.push(ujs)
createjs(ujs, false)
}
cache[engine.name].push(...finalList)
MUJS.addForkCnt(finalList.length)
}
const customFN = async (data) => {
const parser = new DOMParser()
const htmlDocument = parser.parseFromString(data, 'text/html')
const selected = htmlDocument.documentElement
if (qs('.col-sm-8 .tr-link', selected)) {
log('.col-sm-8 .tr-link', qsA('.col-sm-8 .tr-link', selected))
for (const i of qsA('.col-sm-8 .tr-link', selected)) {
await query('.script-version', i)
const fixurl = qs('.tr-link-a', i).href.replace(
new RegExp(document.location.origin, 'gi'),
'https://openuserjs.org',
)
const layout = {
name: qs('.tr-link-a', i).textContent,
description: qs('p', i).textContent,
version: qs('.script-version', i).textContent,
url: fixurl,
code_url: `${fixurl.replace(
/\/scripts/gi,
'/install',
)}.user.js`,
total_installs: qs('td:nth-child(2) p', i).textContent,
created_at: qs('td:nth-child(4) time', i).getAttribute(
'datetime',
),
code_updated_at: qs('td:nth-child(4) time', i).getAttribute(
'datetime',
),
users: [
{
name: qs('.inline-block a', i).textContent,
url: qs('.inline-block a', i).href,
},
],
}
createjs(layout, true)
// MUJS.addCustomCnt(1)
customRecords.push(layout)
}
}
if (qs('div.gist-snippet', selected)) {
log('div.gist-snippet', qsA('div.gist-snippet', selected))
for (const g of qsA('div.gist-snippet', selected)) {
if (
qs('span > a:nth-child(2)', g).textContent.includes(
'.user.js',
)
) {
const fixurl = qs('span > a:nth-child(2)', g).href.replace(
new RegExp(document.location.origin, 'gi'),
'https://gist.github.com',
)
const layout = {}
Object.assign(layout, {
url: fixurl,
code_url: `${fixurl}/raw/${
qs('span > a:nth-child(2)', g).textContent
}`,
created_at: qs('time-ago.no-wrap', g).getAttribute(
'datetime',
),
users: [
{
name: qs('span > a[data-hovercard-type]', g)
.textContent,
url: qs(
'span > a[data-hovercard-type]',
g,
).href.replace(
new RegExp(document.location.origin, 'gi'),
'https://gist.github.com',
),
},
],
})
for (const i of qsA('.file-box table tr .blob-code', g)) {
const txt = i.textContent
const headers = txt.match(/\/\/\s@[\w][\s\S]+/gi) || []
if (headers.length > 0) {
const crop = headers[0].split(
/\/\/\s@(name|description|author|version)\s+/gi,
)
if (
headers[0].includes('@name') &&
!headers[0].includes('@namespace')
) {
Object.assign(layout, {
name: crop[2].trim(),
})
}
if (headers[0].includes('@description')) {
Object.assign(layout, {
description: crop[2].trim(),
})
}
if (headers[0].includes('@version')) {
Object.assign(layout, {
version: crop[2].trim(),
})
}
}
}
createjs(layout, true)
// MUJS.addCustomCnt(1)
customRecords.push(layout)
}
}
}
cache[engine.name].push(...customRecords)
MUJS.addCustomCnt(customRecords.length)
}
const gitFN = async (data) => {
try {
if (isBlank(data.items)) return
for (const r of data.items) {
const layout = {
name: r.name,
description: isEmpty(r.repository.description)
? 'No Description'
: r.repository.description,
url: r.html_url,
code_url: r.html_url.replace(/\/blob\//g, '/raw/'),
code_updated_at: Date.now(), // r.commit
total_installs: r.score,
users: [
{
name: r.repository.owner.login,
url: r.repository.owner.html_url,
},
],
}
createjs(layout, true)
customRecords.push(layout)
}
MUJS.addCustomCnt(data.items.length)
cache[engine.name].push(...customRecords)
} catch (ex) {
MUJS.showError(ex)
}
}
const eURL = engine.url
const cEngine = cache[`${engine.name}`]
if (engine.name.match(/fork/gi)) {
if (!isEmpty(cEngine)) {
for (const ujs of cEngine) {
createjs(ujs, false)
}
MUJS.addForkCnt(cEngine.length)
continue
}
if (cfg.filterlang) {
if (alang.length > 1) {
for (const a of alang) {
MU.fetchURL(
`${eURL}/${a}/scripts/by-site/${host}.json?page=1`,
)
.then(forkFN)
.catch(MUJS.showError)
}
continue
}
MU.fetchURL(
`${eURL}/${navLang}/scripts/by-site/${host}.json?page=1`,
)
.then(forkFN)
.catch(MUJS.showError)
continue
}
MU.fetchURL(`${eURL}/scripts/by-site/${host}.json`)
.then(forkFN)
.catch(MUJS.showError)
} else if (engine.name.match(/(openuserjs|github)/gi)) {
if (!isEmpty(cEngine)) {
for (const ujs of cEngine) {
createjs(ujs, true)
}
MUJS.addCustomCnt(cEngine.length)
continue
}
if (/github/gi.test(engine.name)) {
MU.fetchURL(
`${eURL}"// ==UserScript=="+${host}+ "// ==/UserScript=="+in:file+language:js&per_page=30`,
'GET',
'json',
{
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${engine.token}`,
'X-GitHub-Api-Version': '2022-11-28',
},
},
)
.then(gitFN)
.then(() => {
MU.fetchURL(
'https://api.github.com/rate_limit',
'GET',
'json',
{
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${engine.token}`,
'X-GitHub-Api-Version': '2022-11-28',
},
},
)
.then(rateFN)
.catch(MUJS.showError)
})
.catch(MUJS.showError)
} else {
MU.fetchURL(`${eURL}${host}`, 'GET', 'text')
.then(customFN)
.catch(MUJS.showError)
}
}
}
sortRowBy(2)
} catch (ex) {
MUJS.showError(ex)
}
}
//#endregion
//#region Make Config
const makecfg = () => {
makerow('Sync with GM', 'checkbox', 'cache')
makerow('Auto Fullscreen', 'checkbox', 'autoexpand', {
onchange(e) {
if (e.target.checked) {
btnfullscreen.classList.add('expanded')
main.classList.add('expanded')
btnfullscreen.innerHTML = iconSVG.fsClose
} else {
btnfullscreen.classList.remove('expanded')
main.classList.remove('expanded')
btnfullscreen.innerHTML = iconSVG.fsOpen
}
},
})
makerow(lang.redirect, 'checkbox', 'sleazyredirect')
makerow(lang.filter, 'checkbox', 'filterlang')
makerow('Greasy Fork', 'checkbox', 'greasyfork')
makerow('Sleazy Fork', 'checkbox', 'sleazyfork')
makerow('Open UserJS', 'checkbox', 'openuserjs')
makerow('GitHub API', 'checkbox', 'github')
const cfgAPI = cfg.engines.filter((c) => c.name === 'github')[0]
makerow('GitHub API (Token)', 'password', 'github', {
defaultValue: '',
value: cfgAPI.token ?? '',
placeholder: 'Paste Access Token',
onchange(e) {
MUJS.unsaved = true
MUJS.rebuild = true
if (isNull(legacyMsg)) {
cfgAPI.token = e.target.value
}
},
})
makerow(`${lang.dtime} (ms)`, 'number', 'time', {
defaultValue: 10000,
value: cfg.time,
min: 0,
step: 500,
onbeforeinput(e) {
if (e.target.validity.badInput) {
dom.cl.add(e.target, 'mujs-invalid')
dom.prop(savebtn, 'disabled', true)
} else {
dom.cl.remove(e.target, 'mujs-invalid')
dom.prop(savebtn, 'disabled', false)
}
},
oninput(e) {
MUJS.unsaved = true
const t = e.target
if (
t.validity.badInput ||
(t.validity.rangeUnderflow && t.value !== '-1')
) {
dom.cl.add(t, 'mujs-invalid')
dom.prop(savebtn, 'disabled', true)
} else {
dom.cl.remove(t, 'mujs-invalid')
dom.prop(savebtn, 'disabled', false)
cfg.time = isEmpty(t.value) ? cfg.time : parseFloat(t.value)
}
},
})
const cbtn = make('mu-js', 'mujs-sty-flex')
const savebtn = make('mujs-btn', 'save', {
disabled: false,
innerHTML: lang.save,
onclick(e) {
halt(e)
if (sh('.saveerror')) {
sh('.saveerror').remove()
}
if (!isNull(legacyMsg)) {
legacyMsg = null
MUJS.rebuild = true
rateContainer.innerHTML = ''
}
if (!dom.prop(e.target, 'disabled')) {
MUJS.save()
sleazyRedirect()
if (MUJS.rebuild) {
MUJS.cache.clear()
buildlist()
}
MUJS.unsaved = false
MUJS.rebuild = false
}
},
})
const txta = make('textarea', 'tarea', {
name: 'blacklist',
id: 'blacklist',
rows: '10',
autocomplete: false,
spellcheck: false,
wrap: 'soft',
value: JSON.stringify(cfg.blacklist, null, ' '),
oninput(e) {
let isvalid = true
try {
cfg.blacklist = JSON.parse(e.target.value)
isvalid = true
} catch (ex) {
err(ex)
isvalid = false
} finally {
if (isvalid) {
dom.cl.remove(e.target, 'mujs-invalid')
dom.prop(savebtn, 'disabled', false)
} else {
dom.cl.add(e.target, 'mujs-invalid')
dom.prop(savebtn, 'disabled', true)
}
}
},
})
const resetbtn = make('mujs-btn', 'reset', {
innerHTML: 'Reset',
onclick(e) {
halt(e)
cfg = defcfg
MUJS.unsaved = true
txta.value = JSON.stringify(cfg.blacklist, null, ' ')
for (const i of cfg.engines) {
if (sh(`[id="${i.name}"]`)) {
sh(`[id="${i.name}"]`).checked = i.enabled
}
}
for (const i of shA('.mujs-inlab input[type="checkbox"]')) {
if (
!i.name.match(/((greasy|sleazy)fork|openuserjs|gi(thub|st))/gi)
) {
i.checked = cfg[i.name]
}
}
},
})
cbtn.append(savebtn, resetbtn)
cfgpage.append(txta, cbtn)
}
//#endregion
const makeTHead = (rows) => {
const tr = make('tr')
for (const r of normalizeTarget(rows)) {
const tparent = make('th', r.class ?? '', r)
tr.append(tparent)
}
tabhead.append(tr)
table.append(tabhead, tabbody)
}
const btnHide = make('mujs-btn', 'hide-list', {
title: lang.min,
innerHTML: iconSVG.hide,
onclick(e) {
halt(e)
main.classList.add('hidden')
mainframe.classList.remove('hidden')
timeoutFrame()
},
})
const btnfullscreen = make('mujs-btn', 'fullscreen', {
title: lang.max,
innerHTML: iconSVG.fullscreen,
onclick(e) {
halt(e)
if (btnfullscreen.classList.contains('expanded')) {
btnfullscreen.classList.remove('expanded')
main.classList.remove('expanded')
btnfullscreen.innerHTML = iconSVG.fsOpen
return
}
btnfullscreen.classList.add('expanded')
main.classList.add('expanded')
btnfullscreen.innerHTML = iconSVG.fsClose
},
})
const mainframe = make('mu-js', 'mainframe', {
onclick(e) {
e.preventDefault()
timeout.clear(...timeout.ids)
main.classList.remove('hidden')
mainframe.classList.add('hidden')
if (cfg.autoexpand) {
btnfullscreen.classList.add('expanded')
main.classList.add('expanded')
btnfullscreen.innerHTML = iconSVG.fsClose
}
},
})
const filterList = make('input', 'mujs-fltlist', {
autocomplete: 'off',
spellcheck: false,
type: 'text',
placeholder: lang.searcher,
oninput(e) {
e.preventDefault()
if (!isEmpty(e.target.value)) {
const reg = new RegExp(e.target.value, 'gi')
for (const ujs of shA('.frame')) {
const m = ujs.children[0]
const n = ujs.children[1]
const final =
m.textContent.match(reg) || n.textContent.match(reg) || []
if (final.length === 0) {
ujs.classList.add('hidden')
} else {
ujs.classList.remove('hidden')
}
}
} else {
for (const ujs of shA('.frame')) {
ujs.classList.remove('hidden')
}
}
},
})
const filterBtn = make('mujs-btn', 'filter', {
title: lang.filterA,
innerHTML: iconSVG.filter,
onclick(e) {
e.preventDefault()
fsearch.classList.toggle('hidden')
},
})
const siteSearcher = make('input', 'mujs-searcher', {
autocomplete: 'off',
spellcheck: false,
type: 'text',
placeholder: MUJS.host,
onchange(e) {
e.preventDefault()
buildlist(e.target.value)
},
})
const siteSearchbtn = make('mujs-btn', 'search', {
title: lang.search,
innerHTML: iconSVG.search,
onclick(e) {
e.preventDefault()
ssearch.classList.toggle('hidden')
},
})
const closebtn = make('mujs-btn', 'close', {
title: lang.close,
innerHTML: iconSVG.close,
onclick: async (e) => {
halt(e)
container.remove()
},
})
const btncfg = make('mujs-btn', 'settings', {
title: 'Settings',
innerHTML: iconSVG.cfg,
onclick(e) {
e.preventDefault()
if (MUJS.unsaved && !sh('.saveerror')) {
const txt = make('mujs-row', 'saveerror', {
innerHTML: 'Unsaved changes',
})
countframe.insertAdjacentHTML('afterend', txt.outerHTML.toString())
}
if (dom.cl.has(cfgpage, 'hidden')) {
dom.cl.remove(cfgpage, 'hidden')
dom.cl.add(tbody, 'hidden')
dom.cl.add(main, 'auto-height')
if (!container.supported) {
dom.attr(container.frame, 'style', 'height: 100%;')
}
} else {
dom.cl.add(cfgpage, 'hidden')
dom.cl.remove(tbody, 'hidden')
dom.cl.remove(main, 'auto-height')
main.classList.remove('auto-height')
if (!container.supported) {
dom.attr(container.frame, 'style', '')
}
}
MUJS.rebuild = false
},
})
const btnhome = make('mujs-btn', 'github hidden', {
title: `GitHub (v${
MU.info.script.version.includes('.') ||
MU.info.script.version.includes('Book')
? MU.info.script.version
: MU.info.script.version.slice(0, 5)
})`,
innerHTML: iconSVG.gh,
onclick(e) {
halt(e)
MU.openInTab('https://github.com/magicoflolis/Userscript-Plus')
},
})
const btnissue = make('mujs-btn', 'issue hidden', {
title: lang.issue,
innerHTML: iconSVG.issue,
onclick(e) {
e.preventDefault()
MU.openInTab(
'https://github.com/magicoflolis/Userscript-Plus/issues/new',
)
},
})
const btngreasy = make('mujs-btn', 'greasy hidden', {
title: 'Greasy Fork',
innerHTML: iconSVG.gf,
onclick(e) {
e.preventDefault()
MU.openInTab('https://greasyfork.org/scripts/421603')
},
})
const btnnav = make('mujs-btn', 'nav', {
title: 'Navigation',
innerHTML: iconSVG.nav,
onclick(e) {
halt(e)
if (btngreasy.classList.contains('hidden')) {
btnissue.classList.remove('hidden')
btnhome.classList.remove('hidden')
btngreasy.classList.remove('hidden')
} else {
btnissue.classList.add('hidden')
btnhome.classList.add('hidden')
btngreasy.classList.add('hidden')
}
},
})
countframe.append(gfcounter, sfcounter)
fsearch.append(filterList)
ssearch.append(siteSearcher)
btnHandles.append(btnHide, btnfullscreen, closebtn)
btnframe.append(
fsearch,
filterBtn,
ssearch,
siteSearchbtn,
btncfg,
btnissue,
btnhome,
btngreasy,
btnnav,
btnHandles,
)
header.append(countframe, rateContainer, btnframe)
tbody.append(table)
makeTHead([
{
class: 'mujs-header-name',
textContent: lang.name,
},
{
textContent: lang.createdby,
},
{
textContent: lang.daily,
},
{
textContent: lang.updated,
},
{
textContent: lang.install,
},
])
for (const th of tabhead.rows[0].cells) {
if (dom.text(th) === lang.install) continue
dom.cl.add(th, 'mujs-pointer')
ael(th, 'click', () => {
sortRowBy(th.cellIndex)
})
}
main.append(header, tbody, cfgpage)
mainframe.append(mainbtn)
const mujsRoot = make('mujs-root')
mujsRoot.append(usercss, mainframe, main)
injCon.append(mujsRoot)
makecfg()
buildlist()
timeoutFrame()
} catch (ex) {
err(ex)
}
}
const onDomReady = () => {
try {
container.inject()
// Remove legacy config storage
if (isGM) {
if (MU.storage.has('MUJSConfig')) {
MU.storage.remove('MUJSConfig')
}
}
const setConfig = (config = {}, lcfg = {}) => {
for (const [key, value] of Object.entries(config)) {
if (!hasOwn(lcfg, key)) {
lcfg[key] = config[key]
} else if (typeof value === 'object') {
setConfig(config[key], lcfg[key])
}
}
return lcfg
}
cfg = setConfig(defcfg, MU.getValue('Config'))
sleazyRedirect()
lang = langs[cfg.language] || langs[navLang] || langs.en
// Remove legacy engines
const engines = cfg.engines.filter((c) => c.name === 'gist')
for (const engine of engines) {
if (isNull(engine.token)) {
if (isGM) {
legacyMsg = lang.legacy
} else {
MU.setValue('Config', defcfg)
cfg = defcfg
}
}
}
log('Config:', cfg)
container.onReady(main)
} catch (ex) {
err(ex)
}
}
if (typeof userjs === 'object' && userjs.UserJS && window.self === window.top) {
const readyState = document.readyState
if (readyState === 'interactive' || readyState === 'complete') {
onDomReady()
} else {
document.addEventListener('DOMContentLoaded', onDomReady, { once: true })
}
}