Base library for my scripts
Tính đến
Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta
// @require https://update.greasyfork.org/scripts/375557/730261/Brazenvoid%27s%20Base%20Resource%20v162.js
// ==UserScript==
// @name Brazenvoid's Base Resource v1.6.2
// @namespace brazenvoid
// @version 1.6.3
// @author brazenvoid
// @license GPL-3.0-only
// @description Base library for my scripts
// @grant GM_addStyle
// @run-at document-end
// ==/UserScript==
GM_addStyle(`
button.form-button {
padding: 0 5px;
width: 100%;
}
button.show-settings {
background-color: #ffa31a;
border: 0;
margin: 2px 5px;
padding: 2px 5px;
width: 100%;
}
button.show-settings.fixed {
color: black;
font-size: 14px;
left: 0;
margin: 0;
padding: 15px 0px;
position: fixed;
top: 250px;
width: 30px;
writing-mode: sideways-lr;
z-index: 999;
}
div.form-actions {
text-align: center;
}
div.form-actions button + button {
margin-left: 10px;
}
div.form-actions button.form-button {
padding: 0 15px;
width: auto
}
div.form-actions-wrapper {
display: inline-flex;
}
div.form-actions-wrapper > div.form-group + * {
margin-left: 15px;
}
div.form-group {
min-height: 15px;
padding: 5px 0;
}
div.form-group.form-range-input-group > input {
padding: 0 5px;
width: 70px;
}
div.form-group.form-range-input-group > input + input {
margin-right: 5px;
}
div.form-section {
text-align: center;
}
div.form-section button + button {
margin-left: 5px;
}
div.form-section label.title {
display: block;
height: 20px;
width: 100%;
}
div.form-section button.form-button {
width: auto;
}
hr {
margin: 3px;
}
input.form-input {
height: 18px;
text-align: center;
}
input.form-input.check-radio-input {
float: left;
margin-right: 5px;
}
input.form-input.regular-input {
float: right;
width: 100px;
}
label.form-label {
padding: 2px 0;
}
label.form-label.regular-input {
float: left;
}
label.form-label.check-radio-input {
float: right;
}
label.form-stat-label {
float: right;
padding: 2px 0;
}
section.form-section {
color: black;
font-size: 12px;
font-weight: bold;
position: fixed;
left: 0;
padding: 5px 10px;
z-index: 1000;
}
select.form-dropdown {
float: right;
height: 18px;
text-align: center;
width: 100px
}
`)
const sleep = (milliseconds) => {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
function _generateClassesIdentificationRegex (classes)
{
return new RegExp('(\\s|^)' + classes + '(\\s|$)')
}
function addClasses (node, classes)
{
if (!hasClasses(node, classes)) {
if (node.className !== '') {
node.className += ' '
}
node.className += classes
}
return node
}
function hasClasses (node, classes)
{
return !!node.className.match(_generateClassesIdentificationRegex(classes))
}
function removeClasses (node, classes)
{
if (hasClasses(node, classes)) {
node.className = node.className.replace(_generateClassesIdentificationRegex(classes), ' ')
}
return node
}
class CaseConverters
{
static toCamel (text)
{
return text.replace(/(?:^\w|[A-Z]|\b\w)/g, function (letter, index) {
return index === 0 ? letter.toLowerCase() : letter.toUpperCase()
}).replace(/\s+/g, '')
}
static toKebab (text)
{
return text.toLowerCase().replace(' ', '-')
}
static toKebabFromSnake (text)
{
return text.replace('_', '-')
}
static toNormalFromKebab (text)
{
return text.replace('-', ' ')
}
static toNormalFromSnake (text)
{
return text.replace('_', ' ')
}
static toSnake (text)
{
return text.toLowerCase().replace(' ', '_')
}
}
class ChildObserver
{
static observe (nodes, handler, doInitialRun = false)
{
let instance = new ChildObserver(handler)
instance.observeNodes(nodes, doInitialRun)
return instance
}
constructor (handler)
{
this.config = {
attributes: false,
childList: true,
subtree: false,
}
this.handler = handler
this.observer = new MutationObserver(function (mutations) {
for (let mutation of mutations) {
handler(mutation.target)
}
})
}
observeNodes (nodes, doInitialRun = false)
{
nodes = (Array.isArray(nodes) || nodes instanceof NodeList) ? nodes : [nodes]
for (let node of nodes) {
if (doInitialRun) {
this.handler(node)
}
this.observer.observe(node, this.config)
}
}
}
class LocalStore
{
constructor (key, defaultStore)
{
this.onDefaultsLoaded = null
this.onRetrieval = null
this.onUpdated = null
this._key = key
this._store = {}
this._defaultStore = this._toJSON(defaultStore)
}
_fromJSON (json)
{
let arrayObject = {}
let parsedJSON = JSON.parse(json)
let store = {}
for (let property in parsedJSON.arrays) {
arrayObject = JSON.parse(parsedJSON.arrays[property])
store[property] = []
for (let key in arrayObject) {
store[property].push(arrayObject[key])
}
}
for (let property in parsedJSON.objects) {
store[property] = this._fromJSON(parsedJSON.objects[property])
}
for (let property in parsedJSON.properties) {
store[property] = parsedJSON.properties[property]
}
return store
}
_getStore ()
{
return window.localStorage.getItem(this._key)
}
_getDefaults ()
{
return this._fromJSON(this._defaultStore)
}
_toJSON (store)
{
let arrayToObject = {}
let json = {arrays: {}, objects: {}, properties: {}}
for (let property in store) {
if (typeof store[property] === 'object') {
if (Array.isArray(store[property])) {
for (let key in store[property]) {
arrayToObject[key] = store[property][key]
}
json.arrays[property] = JSON.stringify(arrayToObject)
} else {
json.objects[property] = this._toJSON(store[property])
}
} else {
json.properties[property] = store[property]
}
}
return JSON.stringify(json)
}
delete ()
{
window.localStorage.removeItem(this._key)
return this
}
get ()
{
return this._store
}
restoreDefaults ()
{
this._store = this._getDefaults()
if (this.onDefaultsLoaded !== null) {
this.onDefaultsLoaded(this._store)
}
return this
}
retrieve ()
{
let storedStore = this._getStore()
if (storedStore === null) {
this.restoreDefaults()
} else {
this._store = this._fromJSON(storedStore)
}
if (this.onRetrieval !== null) {
this.onRetrieval(this._store)
}
return this
}
save ()
{
window.localStorage.setItem(this._key, this._toJSON(this._store))
if (this.onUpdated !== null) {
this.onUpdated(this._store)
}
return this
}
isPurged ()
{
return this._getStore() === null
}
}
class Logger
{
constructor (enableDebugging)
{
this.enableDebugging = enableDebugging
}
_log (message)
{
if (this.enableDebugging) {
console.log(message)
}
}
logTaskCompletion (task)
{
this._log('Completed: ' + task)
this.logSeparator()
}
logSeparator ()
{
this._log('------------------------------------------------------------------------------------')
}
logValidation (filterName, validationResult = null)
{
this._log('Satisfies ' + filterName + ' Filter: ' + (validationResult ? 'true' : 'false'))
}
logVideoCheck (videoName)
{
this._log('Checking Video: ' + videoName)
}
}
class SelectorGenerator
{
constructor (selectorPrefix)
{
this.prefix = selectorPrefix
}
getSelector (selector)
{
return this.prefix + selector
};
getSettingsInputSelector (settingName)
{
return this.getSelector(CaseConverters.toKebab(settingName) + '-setting')
}
getSettingsRangeInputSelector (settingName, getMinInputSelector)
{
return this.getSelector(
CaseConverters.toKebab(settingName) + (getMinInputSelector ? '-min' : '-max') + '-setting')
}
getStatLabelSelector (statisticType)
{
return this.getSelector(CaseConverters.toKebab(statisticType) + '-stat')
};
}
class StatisticsRecorder
{
constructor (logger, selectorGenerator)
{
this.logger = logger
this.selectorGenerator = selectorGenerator
this.statistics = {Total: 0}
}
record (statisticType, validationResult, value = 1, log = true)
{
if (!validationResult) {
if (typeof this.statistics[statisticType] !== 'undefined') {
this.statistics[statisticType] += value
} else {
this.statistics[statisticType] = value
}
this.statistics.Total += value
}
if (log) {
this.logger.logValidation(statisticType, validationResult)
}
}
reset ()
{
for (const statisticType in this.statistics) {
this.statistics[statisticType] = 0
}
}
updateUI ()
{
let label, labelSelector
for (const statisticType in this.statistics) {
labelSelector = this.selectorGenerator.getStatLabelSelector(statisticType)
label = document.getElementById(labelSelector)
if (label !== null) {
label.textContent = this.statistics[statisticType]
}
}
}
}
class UIGenerator
{
static appendToBody (node)
{
document.getElementsByTagName('body')[0].appendChild(node)
}
static populateChildren (node, children)
{
for (let child of children) {
node.appendChild(child)
}
return node
}
constructor (showUI, selectorGenerator)
{
this.buttonBackroundColor = null
this.selectorGenerator = selectorGenerator
this.showUI = showUI
}
createFormActions (children)
{
let wrapperDiv = document.createElement('div')
wrapperDiv.classList.add('form-actions-wrapper')
UIGenerator.populateChildren(wrapperDiv, children)
let formActionsDiv = document.createElement('div')
formActionsDiv.classList.add('form-actions')
formActionsDiv.appendChild(wrapperDiv)
return formActionsDiv
}
createFormButton (caption, onClick)
{
let button = document.createElement('button')
button.classList.add('form-button')
button.textContent = caption
button.addEventListener('click', onClick)
if (this.buttonBackroundColor !== null) {
button.style.backgroundColor = this.buttonBackroundColor
}
return button
}
createFormGroup (children)
{
let divFormGroup = document.createElement('div')
divFormGroup.classList.add('form-group')
return UIGenerator.populateChildren(divFormGroup, children)
}
createFormGroupDropdown (id, keyValuePairs, defaultValue = null)
{
let dropdown = document.createElement('select'), item
dropdown.id = id
dropdown.classList.add('form-dropdown')
for (let [key, value] of keyValuePairs) {
item = document.createElement('option')
item.textContent = value
item.value = key
dropdown.appendChild(item)
}
dropdown.value = defaultValue === null ? keyValuePairs[0][0] : defaultValue
return dropdown
}
createFormGroupInput (id, type, defaultValue = null)
{
let inputFormGroup = document.createElement('input')
inputFormGroup.id = id
inputFormGroup.classList.add('form-input')
inputFormGroup.type = type
switch (type) {
case 'number':
case 'text':
inputFormGroup.classList.add('regular-input')
if (defaultValue !== null) {
inputFormGroup.value = defaultValue
}
break
case 'radio':
case 'checkbox':
inputFormGroup.classList.add('check-radio-input')
if (defaultValue !== null) {
inputFormGroup.checked = defaultValue
}
break
}
return inputFormGroup
}
createFormGroupLabel (label, inputID = null, inputType = null)
{
let labelFormGroup = document.createElement('label')
labelFormGroup.classList.add('form-label')
labelFormGroup.textContent = label
if (inputID !== null) {
labelFormGroup.setAttribute('for', inputID)
}
if (inputType !== null) {
switch (inputType) {
case 'number':
case 'text':
labelFormGroup.classList.add('regular-input')
labelFormGroup.textContent += ': '
break
case 'radio':
case 'checkbox':
labelFormGroup.classList.add('check-radio-input')
break
}
}
return labelFormGroup
}
createFormGroupStatLabel (statisticType)
{
let labelFormGroup = document.createElement('label')
labelFormGroup.id = this.selectorGenerator.getStatLabelSelector(statisticType)
labelFormGroup.classList.add('form-stat-label')
labelFormGroup.textContent = '0'
return labelFormGroup
}
createFormInputGroup (label, inputType = 'text', defaultValue = null)
{
let divFormInputGroup
let inputID = this.selectorGenerator.getSettingsInputSelector(label)
let labelFormGroup = this.createFormGroupLabel(label, inputType, inputID)
let inputFormGroup = this.createFormGroupInput(inputID, inputType, defaultValue)
switch (inputType) {
case 'number':
case 'text':
divFormInputGroup = this.createFormGroup([labelFormGroup, inputFormGroup])
break
case 'radio':
case 'checkbox':
divFormInputGroup = this.createFormGroup([inputFormGroup, labelFormGroup])
break
}
return divFormInputGroup
}
createFormRangeInputGroup (label, inputsType = 'text', defaultValues = null)
{
let divFormInputGroup = this.createFormGroup([
this.createFormGroupLabel(label, null, inputsType),
this.createFormGroupInput(
this.selectorGenerator.getSettingsRangeInputSelector(label, false),
inputsType,
defaultValues === null ? null : defaultValues[1]
),
this.createFormGroupInput(
this.selectorGenerator.getSettingsRangeInputSelector(label, true),
inputsType,
defaultValues === null ? null : defaultValues[0]
),
])
divFormInputGroup.classList.add('form-range-input-group')
return divFormInputGroup
}
createFormSection (title, children)
{
let sectionDiv = document.createElement('div')
sectionDiv.classList.add('form-section')
let sectionTitle = document.createElement('label')
sectionTitle.textContent = title
sectionTitle.classList.add('title')
UIGenerator.populateChildren(sectionDiv, [sectionTitle])
return UIGenerator.populateChildren(sectionDiv, children)
}
createFormSectionButton (caption, tooltip, onClick)
{
let button = this.createFormButton(caption, onClick)
button.title = tooltip
return button
}
createSection (IDSuffix, backgroundColor, top, width, children)
{
let section = document.createElement('section')
section.id = this.selectorGenerator.getSelector(IDSuffix)
section.classList.add('form-section')
section.style.display = this.showUI ? 'block' : 'none'
section.style.top = top
section.style.width = width
section.style.backgroundColor = backgroundColor
return UIGenerator.populateChildren(section, children)
}
createSeparator ()
{
return document.createElement('hr')
}
createSettingsFormActions (localStore, onClick, addTopPadding = false)
{
let divFormActions = this.createFormActions([
this.createFormButton('Apply', onClick),
this.createFormButton('Reset', function () {
localStore.retrieve()
onClick()
}),
])
if (addTopPadding) {
divFormActions.style.paddingTop = '10px'
}
return divFormActions
}
createSettingsDropDownFormGroup (label, keyValuePairs, defaultValue = null)
{
let dropdownID = this.selectorGenerator.getSettingsInputSelector(label)
return this.createFormGroup([
this.createFormGroupLabel(label, 'text', dropdownID),
this.createFormGroupDropdown(dropdownID, keyValuePairs, defaultValue),
])
}
createSettingsHideButton (settingsSectionIDSuffix)
{
let settingsSectionID = this.selectorGenerator.getSelector(settingsSectionIDSuffix)
return this.createFormButton('Hide', function () {
document.getElementById(settingsSectionID).style.display = 'none'
})
}
createSettingsShowButton (caption, settingsSection, fixed = true)
{
let controlButton = document.createElement('button')
controlButton.textContent = caption
controlButton.classList.add('show-settings')
if (fixed) {
controlButton.classList.add('fixed')
}
controlButton.addEventListener('click', function () {
let settingsUI = document.getElementById(settingsSection.id)
settingsUI.style.display = settingsUI.style.display === 'none' ? 'block' : 'none'
})
return controlButton
}
createStatisticsFormGroup (statisticsType, label = null)
{
if (label === null) {
label = statisticsType
}
return this.createFormGroup([
this.createFormGroupLabel('Filtered ' + label + ' Videos'),
this.createFormGroupStatLabel(statisticsType),
])
}
createStoreFormSection (localStore)
{
return this.createFormSection('Store', [
this.createFormActions([
this.createFormSectionButton('Update', 'Save UI settings in store', function () {
localStore.save()
}),
this.createFormSectionButton(
'Reset',
'Reset store values to user defaults',
function () {
localStore.restoreDefaults()
},
),
this.createFormSectionButton('Purge', 'Purge store', function () {
localStore.delete()
}),
])
])
}
getSettingsInput (label)
{
return document.getElementById(this.selectorGenerator.getSettingsInputSelector(label))
}
getSettingsInputCheckedStatus (label)
{
return this.getSettingsInput(label).checked
}
getSettingsInputValue (label, lowerBound)
{
return this.getSettingsRangeInput(label, lowerBound).value
}
getSettingsRangeInput (label, getMinInput)
{
return document.getElementById(this.selectorGenerator.getSettingsRangeInputSelector(label, getMinInput))
}
getSettingsRangeInputValue (label, getMinInputValue)
{
return this.getSettingsRangeInput(label, getMinInputValue).value
}
setSettingsInputCheckedStatus (label, bool)
{
this.getSettingsInput(label).checked = bool
}
setSettingsInputValue (label, value)
{
this.getSettingsInput(label).value = value
}
setSettingsRangeInputValue (label, lowerBound, upperBound)
{
this.getSettingsRangeInput(label, true).value = lowerBound
this.getSettingsRangeInput(label, false).value = upperBound
}
}
class Validator
{
static iFramesRemover ()
{
GM_addStyle(' iframe { display: none !important; } ')
}
constructor (statisticsRecorder)
{
this._blacklist = []
this._filters = []
this._optimizedBlacklist = []
this._optimizedSanitizationRules = {}
this._sanitizationRules = []
this._statisticsRecorder = statisticsRecorder
}
addBlacklistFilter (blacklistedWords)
{
this._blacklist = blacklistedWords
return this
}
addConditionalFilter (name, shouldBe, getValueCallback)
{
return this
}
addRangeFilter (name, conditionCallback)
{
return this
}
addSanitizationFilter (sanitizationRules, videoTitleSelector, videoPageTitleSelector)
{
this._sanitizationRules = sanitizationRules
return this
}
optimize ()
{
for (let i = 0; i < this._blacklist.length; i++) {
this._optimizedBlacklist[i] = new RegExp(this._blacklist[i], 'ig')
}
for (const substitute in this._sanitizationRules) {
this._optimizedSanitizationRules[substitute] = []
for (let i = 0; i < this._sanitizationRules[substitute].length; i++) {
this._optimizedSanitizationRules[substitute][i] = new RegExp(this._sanitizationRules[substitute][i],
'ig')
}
}
return this
}
sanitize (text)
{
for (const substitute in this._optimizedSanitizationRules) {
for (const subject of this._optimizedSanitizationRules[substitute]) {
text = text.replace(subject, substitute)
}
}
return text
}
sanitizeVideoItem (videoNameNode)
{
videoNameNode.textContent = this.sanitize(videoNameNode.textContent)
return this
}
sanitizeVideoPage (videoNameNodeSelector)
{
let videoNameNode = document.querySelector(videoNameNodeSelector)
if (videoNameNode !== null) {
let sanitizedVideoName = this.sanitize(videoNameNode.textContent)
videoNameNode.textContent = sanitizedVideoName
document.title = sanitizedVideoName
}
return this
}
validateBlackList (text)
{
let validationCheck = true
if (this._optimizedBlacklist.length > 0) {
for (const blacklistedWord of this._optimizedBlacklist) {
validationCheck = text.match(blacklistedWord) === null
if (!validationCheck) {
break
}
}
this._statisticsRecorder.record('Blacklist', validationCheck)
}
return validationCheck
}
validateRange (name, value, bounds)
{
let validationCheck = true
if (bounds[0] > 0 && bounds[1] > 0) {
validationCheck = value >= bounds[0] && value <= bounds[1]
} else {
if (bounds[0] > 0) {
validationCheck = value >= bounds[0]
}
if (bounds[1] > 0) {
validationCheck = value <= bounds[1]
}
}
this._statisticsRecorder.record(name, validationCheck)
return validationCheck
}
validateRangeFilter (name, lowerBound, upperBound, getValueCallback)
{
if (lowerBound > 0 || upperBound > 0) {
return this.validateRange(name, getValueCallback(), [lowerBound, upperBound])
}
return true
}
}