您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Base library for my scripts
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/375557/758018/Base%20Resource.js
// ==UserScript== // @name Base Resource // @namespace brazenvoid // @version 1.6.5 // @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 } `) /** * @param milliseconds * @return {Promise<*>} */ const sleep = (milliseconds) => { return new Promise(resolve => setTimeout(resolve, milliseconds)) } /** * @param {string} classes * @return {RegExp} * @private */ function _generateClassesIdentificationRegex (classes) { return new RegExp('(\\s|^)' + classes + '(\\s|$)') } /** * @param {Element} node * @param {string} classes * @return {Element} */ function addClasses (node, classes) { if (!hasClasses(node, classes)) { if (node.className !== '') { node.className += ' ' } node.className += classes } return node } /** * @param {Element} node * @param {string} classes * @return {boolean} */ function hasClasses (node, classes) { return !!node.className.match(_generateClassesIdentificationRegex(classes)) } /** * @param {Element} node * @param {string} classes * @return {Element} */ function removeClasses (node, classes) { if (hasClasses(node, classes)) { node.className = node.className.replace(_generateClassesIdentificationRegex(classes), ' ') } return node } class CaseConverters { /** * @param {string} text * @return {string} */ 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, '') } /** * @param {string} text * @return {string} */ static toKebab (text) { return text.toLowerCase().replace(' ', '-') } /** * @param {string} text * @return {string} */ static toKebabFromSnake (text) { return text.replace('_', '-') } /** * @param {string} text * @return {string} */ static toNormalFromKebab (text) { return text.replace('-', ' ') } /** * @param {string} text * @return {string} */ static toNormalFromSnake (text) { return text.replace('_', ' ') } /** * @param {string} text * @return {string} */ static toSnake (text) { return text.toLowerCase().replace(' ', '_') } } class ChildObserver { /** * @callback observerMutationHandler * @param {Node} target */ /** * @param {Element|Element[]} nodes * @param {observerMutationHandler} handler * @param {boolean} doInitialRun * @return {ChildObserver} */ static observe (nodes, handler, doInitialRun = false) { let instance = new ChildObserver(handler) instance.observeNodes(nodes, doInitialRun) return instance } /** * @param {observerMutationHandler} handler */ constructor (handler) { /** * @type {{subtree: boolean, attributes: boolean, childList: boolean}} * @private */ this._config = { attributes: false, childList: true, subtree: false, } /** * @type {observerMutationHandler} * @private */ this._handler = handler /** * @type {MutationObserver} * @private */ this._observer = new MutationObserver(function (mutations) { for (let mutation of mutations) { handler(mutation.target) } }) } /** * @param {Element|Element[]} nodes * @param {boolean} doInitialRun */ 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 { /** * @callback storeEventHandler * @param {Object} store */ /** * @param {string} key * @param {Object} defaultStore */ constructor (key, defaultStore) { /** * @type {string} * @private */ this._key = key /** * @type {Object} * @private */ this._store = {} /** * @type {string} * @private */ this._defaultStore = this._toJSON(defaultStore) /** * @type {storeEventHandler} */ this.onDefaultsLoaded = null /** * @type {storeEventHandler} */ this.onRetrieval = null /** * @type {storeEventHandler} */ this.onUpdated = null } /** * @param {string} json * @return {Object} * @private */ _fromJSON (json) { /** @type {{arrays: Object, objects: Object, properties: Object}} */ let parsedJSON = JSON.parse(json) let arrayObject = {} 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 } /** * @return {string} * @private */ _getStore () { return window.localStorage.getItem(this._key) } /** * @return {Object} * @private */ _getDefaults () { return this._fromJSON(this._defaultStore) } /** * @param {Object} store * @return {string} * @private */ _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) } /** * @return {LocalStore} */ delete () { window.localStorage.removeItem(this._key) return this } /** * @return {Object} */ get () { return this._store } /** * @return {LocalStore} */ restoreDefaults () { this._store = this._getDefaults() if (this.onDefaultsLoaded !== null) { this.onDefaultsLoaded(this._store) } return this } /** * @return {LocalStore} */ 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 } /** * @return {LocalStore} */ save () { window.localStorage.setItem(this._key, this._toJSON(this._store)) if (this.onUpdated !== null) { this.onUpdated(this._store) } return this } /** * @return {boolean} */ isPurged () { return this._getStore() === null } } class Logger { /** * @param {boolean} enableDebugging */ constructor (enableDebugging) { /** * @type {boolean} * @private */ this._enableDebugging = enableDebugging } /** * @param {string} message * @private */ _log (message) { if (this._enableDebugging) { console.log(message) } } /** * @param {string} task */ logTaskCompletion (task) { this._log('Completed: ' + task) this.logSeparator() } logSeparator () { this._log('------------------------------------------------------------------------------------') } /** * @param {string} filterName * @param {boolean} validationResult */ logValidation (filterName, validationResult) { this._log('Satisfies ' + filterName + ' Filter: ' + (validationResult ? 'true' : 'false')) } /** * @param {string} videoName */ logVideoCheck (videoName) { this._log('Checking Video: ' + videoName) } } class SelectorGenerator { /** * @param {string} selectorPrefix */ constructor (selectorPrefix) { /** * @type {string} * @private */ this._prefix = selectorPrefix } /** * @param {string} selector * @return {string} */ getSelector (selector) { return this._prefix + selector }; /** * @param {string} settingName * @return {string} */ getSettingsInputSelector (settingName) { return this.getSelector(CaseConverters.toKebab(settingName) + '-setting') } /** * @param {string} settingName * @param {boolean} getMinInputSelector * @return {string} */ getSettingsRangeInputSelector (settingName, getMinInputSelector) { return this.getSelector( CaseConverters.toKebab(settingName) + (getMinInputSelector ? '-min' : '-max') + '-setting') } /** * @param {string} statisticType * @return {string} */ getStatLabelSelector (statisticType) { return this.getSelector(CaseConverters.toKebab(statisticType) + '-stat') }; } class StatisticsRecorder { /** * @param {Logger} logger * @param {SelectorGenerator} selectorGenerator */ constructor (logger, selectorGenerator) { /** * @type {Logger} * @private */ this._logger = logger /** * @type {SelectorGenerator} * @private */ this._selectorGenerator = selectorGenerator /** * @type {{Total: number}} * @private */ this._statistics = {Total: 0} } /** * @param {string} statisticType * @param {boolean} validationResult * @param {number} value * @param {boolean} log */ 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 { /** * @param {Element|Node} node */ static appendToBody (node) { document.getElementsByTagName('body')[0].appendChild(node) } /** * @param {Element} node * @param {Element[]} children * @return {Element} */ static populateChildren (node, children) { for (let child of children) { node.appendChild(child) } return node } /** * @param {boolean} showUI * @param {SelectorGenerator} selectorGenerator */ constructor (showUI, selectorGenerator) { /** * @type {*} * @private */ this._buttonBackroundColor = null /** * @type {SelectorGenerator} * @private */ this._selectorGenerator = selectorGenerator /** * @type {boolean} * @private */ this._showUI = showUI } /** * @param {Element[]} children * @return {HTMLDivElement} */ 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 } /** * @param {string} caption * @param onClick * @return {HTMLButtonElement} */ 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 } /** * @param {Element[]} children * @return {Element} */ createFormGroup (children) { let divFormGroup = document.createElement('div') divFormGroup.classList.add('form-group') return UIGenerator.populateChildren(divFormGroup, children) } /** * @param {string} id * @param {Array} keyValuePairs * @param {*} defaultValue * @return {HTMLSelectElement} */ 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 } /** * @param {string} id * @param {string} type * @param {*} defaultValue * @return {HTMLInputElement} */ 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 } /** * @param {string} label * @param {string} inputID * @param {string} inputType * @return {HTMLLabelElement} */ createFormGroupLabel (label, inputID = '', inputType = '') { let labelFormGroup = document.createElement('label') labelFormGroup.classList.add('form-label') labelFormGroup.textContent = label if (inputID !== '') { labelFormGroup.setAttribute('for', inputID) } if (inputType !== '') { 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 } /** * @param {string} statisticType * @return {HTMLLabelElement} */ createFormGroupStatLabel (statisticType) { let labelFormGroup = document.createElement('label') labelFormGroup.id = this._selectorGenerator.getStatLabelSelector(statisticType) labelFormGroup.classList.add('form-stat-label') labelFormGroup.textContent = '0' return labelFormGroup } /** * @param {string} label * @param {string} inputType * @param {*} defaultValue * @return {Element} */ 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 } /** * @param {string} label * @param {string} inputsType * @param {*} defaultValues * @return {Element} */ 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 } /** * @param {string} title * @param {Element[]} children * @return {Element} */ 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) } /** * @param {string} caption * @param {string} tooltip * @param onClick * @return {HTMLButtonElement} */ createFormSectionButton (caption, tooltip, onClick) { let button = this.createFormButton(caption, onClick) button.title = tooltip return button } /** * @param {string} IDSuffix * @param {*} backgroundColor * @param {*} top * @param {*} width * @param {Element[]} children * @return {Element} */ 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) } /** * @return {HTMLHRElement} */ createSeparator () { return document.createElement('hr') } /** * @param {LocalStore} localStore * @param onClick * @param {boolean} addTopPadding * @return {HTMLDivElement} */ 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 } /** * @param {string} label * @param {Array} keyValuePairs * @param {*} defaultValue * @return {Element} */ createSettingsDropDownFormGroup (label, keyValuePairs, defaultValue = null) { let dropdownID = this._selectorGenerator.getSettingsInputSelector(label) return this.createFormGroup([ this.createFormGroupLabel(label, 'text', dropdownID), this.createFormGroupDropdown(dropdownID, keyValuePairs, defaultValue), ]) } /** * @param {string} settingsSectionIDSuffix * @return {HTMLButtonElement} */ createSettingsHideButton (settingsSectionIDSuffix) { let settingsSectionID = this._selectorGenerator.getSelector(settingsSectionIDSuffix) return this.createFormButton('Hide', function () { document.getElementById(settingsSectionID).style.display = 'none' }) } /** * @param {string} caption * @param {Element} settingsSection * @param {boolean} fixed * @return {HTMLButtonElement} */ 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 } /** * @param {string} statisticsType * @param {string} label * @return {Element} */ createStatisticsFormGroup (statisticsType, label = null) { if (label === null) { label = statisticsType } return this.createFormGroup([ this.createFormGroupLabel('Filtered ' + label + ' Videos'), this.createFormGroupStatLabel(statisticsType), ]) } /** * @param {LocalStore} localStore * @return {Element} */ 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() }), ]), ]) } /** * @param {string} label * @return {HTMLElement} */ getSettingsInput (label) { return document.getElementById(this._selectorGenerator.getSettingsInputSelector(label)) } /** * @param {string} label * @return {boolean} */ getSettingsInputCheckedStatus (label) { return this.getSettingsInput(label).checked } /** * @param {string} label * @param {boolean} lowerBound * @return {*} */ getSettingsInputValue (label) { return this.getSettingsInput(label).value } /** * @param {string} label * @param {boolean} getMinInput * @return {HTMLElement} */ getSettingsRangeInput (label, getMinInput) { return document.getElementById(this._selectorGenerator.getSettingsRangeInputSelector(label, getMinInput)) } /** * @param {string} label * @param {boolean} getMinInputValue * @return {*} */ getSettingsRangeInputValue (label, getMinInputValue) { return this.getSettingsRangeInput(label, getMinInputValue).value } /** * @param {string} label * @param {boolean} bool */ setSettingsInputCheckedStatus (label, bool) { this.getSettingsInput(label).checked = bool } /** * @param {string} label * @param {*} value */ setSettingsInputValue (label, value) { this.getSettingsInput(label).value = value } /** * @param {string} label * @param {number} lowerBound * @param {number} upperBound */ 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; } ') } /** * @param {StatisticsRecorder} statisticsRecorder */ constructor (statisticsRecorder) { /** * @type {Array} * @private */ this._blacklist = [] /** * @type {Array} * @private */ this._filters = [] /** * @type {Array} * @private */ this._optimizedBlacklist = [] /** * @type {Object} * @private */ this._optimizedSanitizationRules = {} /** * @type {Array} * @private */ this._sanitizationRules = [] /** * @type {StatisticsRecorder} * @private */ this._statisticsRecorder = statisticsRecorder } /** * @param {string[]} blacklistedWords * @return {Validator} */ addBlacklistFilter (blacklistedWords) { this._blacklist = blacklistedWords return this } /** * @param {Object} sanitizationRules * @return {Validator} */ addSanitizationFilter (sanitizationRules) { this._sanitizationRules = sanitizationRules return this } /** * @return {Validator} */ 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 } /** * @param {string} text * @return {string} */ sanitize (text) { for (const substitute in this._optimizedSanitizationRules) { for (const subject of this._optimizedSanitizationRules[substitute]) { text = text.replace('/' + subject + '/g', substitute) } } return text } /** * @param {Element} videoNameNode * @return {Validator} */ sanitizeVideoItem (videoNameNode) { videoNameNode.textContent = this.sanitize(videoNameNode.textContent) return this } /** * @param {string} videoNameNodeSelector * @return {Validator} */ sanitizeVideoPage (videoNameNodeSelector) { let videoNameNode = document.querySelector(videoNameNodeSelector) if (videoNameNode !== null) { let sanitizedVideoName = this.sanitize(videoNameNode.textContent) videoNameNode.textContent = sanitizedVideoName document.title = sanitizedVideoName } return this } /** * @param {string} text * @return {boolean} */ 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 } /** * @param {string} name * @param {number} value * @param {number[]} bounds * @return {boolean} */ 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 } /** * @param {string} name * @param {number} lowerBound * @param {number} upperBound * @param getValueCallback * @return {boolean} */ validateRangeFilter (name, lowerBound, upperBound, getValueCallback) { if (lowerBound > 0 || upperBound > 0) { return this.validateRange(name, getValueCallback(), [lowerBound, upperBound]) } return true } }