您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Base library for my scripts
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/375557/880823/Base%20Brazen%20Resource.js
// ==UserScript== // @name Base Brazen Resource // @namespace brazenvoid // @version 3.4.0 // @author brazenvoid // @license GPL-3.0-only // @description Base library for my scripts // @run-at document-end // ==/UserScript== const REGEX_LINE_BREAK = /\r?\n/g const REGEX_PRESERVE_NUMBERS = /[^0-9]/g class ChildObserver { /** * @callback observerOnMutation * @param {NodeList} nodes */ /** * @return {ChildObserver} */ static create () { return new ChildObserver } /** * ChildObserver constructor */ constructor () { this._node = null this._observer = null this._onNodesAdded = null this._onNodesRemoved = null } /** * @return {ChildObserver} * @private */ _observeNodes () { this._observer.observe(this._node, {childList: true}) return this } /** * Attach an observer to the specified node(s) * @param {Node} node * @returns {ChildObserver} */ observe (node) { this._node = node this._observer = new MutationObserver((mutations) => { for (let mutation of mutations) { if (mutation.addedNodes.length && this._onNodesAdded !== null) { this._onNodesAdded( mutation.addedNodes, mutation.previousSibling, mutation.nextSibling, mutation.target, ) } if (mutation.removedNodes.length && this._onNodesRemoved !== null) { this._onNodesRemoved( mutation.removedNodes, mutation.previousSibling, mutation.nextSibling, mutation.target, ) } } }) return this._observeNodes() } /** * @param {observerOnMutation} eventHandler * @returns {ChildObserver} */ onNodesAdded (eventHandler) { this._onNodesAdded = eventHandler return this } /** * @param {observerOnMutation} eventHandler * @returns {ChildObserver} */ onNodesRemoved (eventHandler) { this._onNodesRemoved = eventHandler return this } pauseObservation () { this._observer.disconnect() } resumeObservation () { this._observeNodes() } } class LocalStore { /** * @callback storeEventHandler * @param {Object} store */ /** * @param {string} key * @param {Object} defaults */ constructor (key, defaults) { /** * @type {Object} * @private */ this._defaults = defaults /** * @type {boolean} * @private */ this._defaultsSet = false /** * @type {string} * @private */ this._key = key // Events /** * @type {storeEventHandler} */ this._onChange = null } _handleOnChange () { if (this._onChange !== null) { this._onChange(this.get()) } } /** * @return {LocalStore} */ delete () { window.localStorage.removeItem(this._key) return this } /** * @param {string} filename */ exportToFile (filename) { let linkElement = document.createElement('a') let file = new Blob([Utilities.objectToJSON(this.get())], {type: 'application/json'}) linkElement.href = URL.createObjectURL(file) linkElement.download = filename linkElement.click() URL.revokeObjectURL(linkElement.href) linkElement.remove() } /** * @return {*} */ get () { this._defaultsSet = false let storedStore = window.localStorage.getItem(this._key) return storedStore === null ? this.restoreDefaults() : Utilities.objectFromJSON(storedStore) } importFromFile (file) { } /** * @param {storeEventHandler} handler * @return {LocalStore} */ onChange (handler) { this._onChange = handler return this } /** * @return {Object} */ restoreDefaults () { this._defaultsSet = true this.save(this._defaults) return this._defaults } /** * @param {Object} data * @return {LocalStore} */ save (data) { window.localStorage.setItem(this._key, Utilities.objectToJSON(data)) this._handleOnChange() return this } /** * @return {boolean} */ wereDefaultsSet () { return this._defaultsSet } } class Paginator { /** * @callback PaginatorAfterPaginationEventHandler * @param {Paginator} paginator */ /** * @callback PaginatorGetPageNoFromUrlHandler * @param {string} pageUrl * @param {Paginator} paginator */ /** * @callback PaginatorGetPageUrlFromPageNoHandler * @param {number} pageNo * @param {Paginator} paginator */ /** * @callback PaginatorGetPaginationElementForPageNoHandler * @param {number} pageNo * @param {Paginator} paginator */ /** * @param {JQuery} paginationWrapper * @param {JQuery.Selector} listSelector * @param {JQuery.Selector} itemClassesSelector * @param {string } lastPageUrl * @return {Paginator} */ static create (paginationWrapper, listSelector, itemClassesSelector, lastPageUrl) { return (new Paginator).configure(paginationWrapper, listSelector, itemClassesSelector, lastPageUrl) } /** * */ constructor () { /** * @type {number} * @private */ this._currentPageNo = 0 /** * @type {JQuery.Selector} * @private */ this._itemClassesSelector = '' /** * @type {number} * @private */ this._lastPageNo = 0 /** * @type {string} * @private */ this._lastPageUrl = '' /** * @type {JQuery.Selector} * @private */ this._listSelector = '' /** * @type {number} * @private */ this._paginatedPageNo = 0 /** * @type {JQuery} * @private */ this._paginationWrapper = null /** * @type {JQuery} * @private */ this._targetElement = null // Events and callbacks /** * @type {PaginatorAfterPaginationEventHandler} * @private */ this._onAfterPagination = null /** * @type {PaginatorGetPageNoFromUrlHandler} * @private */ this._onGetPageNoFromUrl = null /** * @type {PaginatorGetPageUrlFromPageNoHandler} * @private */ this._onGetPageUrlFromPageNo = null /** * @type {PaginatorGetPaginationElementForPageNoHandler} * @private */ this._onGetPaginationElementForPageNo = null } _conformUIToNewPaginatedState () { let currentPageElement = this.getPaginationElementForPageNo(this._currentPageNo) // Determine whether next page's pagination element exists let nextPageElement = this.getPaginationElementForPageNo(this._currentPageNo + 1) // Delete pagination element of paginated page // Mutate current page no element to show paginated page numbers currentPageElement.text(this._currentPageNo + '-' + this._paginatedPageNo) } /** * @param {number} threshold * @param {number} limit * @param {number} iteration * @return {number} * @private */ _loadAndParseNextPage (threshold, limit, iteration = 0) { let lastPageHasNotBeenReached = this._paginatedPageNo < this._lastPageNo let paginationLimitHasNotBeenMet = (limit <= 0 || (this._paginatedPageNo - this._currentPageNo <= limit)) let compliantItemsAreLessThanTheThreshold = this._targetElement.find(this._itemClassesSelector + ':visible').length < threshold if (lastPageHasNotBeenReached && paginationLimitHasNotBeenMet && compliantItemsAreLessThanTheThreshold) { this._sandbox.load(this.getPageUrlFromPageNo(++this._paginatedPageNo) + ' ' + this._listSelector, '', () => { this._sandbox.find(this._itemClassesSelector).insertAfter(this._targetElement.find(this._itemClassesSelector + ':last')) this._sandbox.empty() Utilities.callEventHandler(this.onAfterPagination, [this]) }) iteration = this._loadAndParseNextPage(threshold, limit, iteration + 1) } return iteration } /** * @param {JQuery} paginationWrapper * @param {JQuery.Selector} listSelector * @param {JQuery.Selector} itemClassesSelector * @param {string } lastPageUrl * @return {Paginator} */ configure (paginationWrapper, listSelector, itemClassesSelector, lastPageUrl) { this._lastPageUrl = lastPageUrl this._listSelector = listSelector this._itemClassesSelector = itemClassesSelector this._paginationWrapper = paginationWrapper return this } getCurrentPageNo () { return this._currentPageNo } getLastPageNo () { return this._lastPageNo } /** * @param {string} pageUrl * @return {number} */ getPageNoFromUrl (pageUrl) { return Utilities.callEventHandlerOrFail('onGetPageNoFromUrl', this._onGetPageNoFromUrl, [pageUrl, this]) } /** * @param {number} pageNo * @return {string} */ getPageUrlFromPageNo (pageNo) { return Utilities.callEventHandlerOrFail('onGetPageUrlFromPageNo', this._onGetPageUrlFromPageNo, [pageNo, this]) } /** * @param {number} pageNo * @return {JQuery} */ getPaginationElementForPageNo (pageNo) { return Utilities.callEventHandlerOrFail('onGetPaginationElementForPageNo', this._onGetPaginationElementForPageNo, [pageNo, this]) } getPaginatedPageNo () { return this._paginatedPageNo } getPaginationWrapper () { return this._paginationWrapper } initialize () { this._currentPageNo = this.getPageNoFromUrl(window.location.href) this._lastPageNo = this.getPageNoFromUrl(this._lastPageUrl) this._paginatedPageNo = this._currentPageNo this._sandbox = $('<div id="brazen-paginator-sandbox" hidden/>').appendTo('body') this._targetElement = $(this._listSelector + ':first') return this } /** * @param {PaginatorAfterPaginationEventHandler} handler * @return {this} */ onAfterPagination (handler) { this._onAfterPagination = handler return this } /** * @param {PaginatorGetPageNoFromUrlHandler} handler * @return {this} */ onGetPageNoFromUrl (handler) { this._onGetPageNoFromUrl = handler return this } /** * @param {PaginatorGetPageUrlFromPageNoHandler} handler * @return {this} */ onGetPageUrlFromPageNo (handler) { this._onGetPageUrlFromPageNo = handler return this } /** * @param {PaginatorGetPaginationElementForPageNoHandler} handler * @return {this} */ onGetPaginationElementForPageNo (handler) { this._onGetPaginationElementForPageNo = handler return this } run (threshold, limit) { if (this._paginationWrapper.length && threshold) { if (this._loadAndParseNextPage(threshold, limit)) { this._conformUIToNewPaginatedState() } } return this } } 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(Utilities.toKebabCase(settingName) + '-setting') } /** * @param {string} settingName * @param {boolean} getMinInputSelector * @return {string} */ getSettingsRangeInputSelector (settingName, getMinInputSelector) { return this.getSelector(Utilities.toKebabCase(settingName) + (getMinInputSelector ? '-min' : '-max') + '-setting') } /** * @param {string} statisticType * @return {string} */ getStatLabelSelector (statisticType) { return this.getSelector(Utilities.toKebabCase(statisticType) + '-stat') } } class StatisticsRecorder { /** * @param {string} selectorPrefix */ constructor (selectorPrefix) { /** * @type {SelectorGenerator} * @private */ this._selectorGenerator = new SelectorGenerator(selectorPrefix) /** * @type {{Total: number}} * @private */ this._statistics = {Total: 0} } /** * @param {string} statisticType * @param {boolean} validationResult * @param {number} value */ record (statisticType, validationResult, value = 1) { if (!validationResult) { if (typeof this._statistics[statisticType] !== 'undefined') { this._statistics[statisticType] += value } else { this._statistics[statisticType] = value } this._statistics.Total += value } } 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 Utilities { /** * @param {string[]} words * @return {RegExp} */ static buildWholeWordMatchingRegex (words) { let patternedWords = [] for (let i = 0; i < words.length; i++) { patternedWords.push('\\b' + words[i] + '\\b') } return new RegExp('(' + patternedWords.join('|') + ')', 'gi') } static callEventHandler (handler, parameters = [], defaultValue = null) { return handler ? handler(...parameters) : defaultValue } static callEventHandlerOrFail (name, handler, parameters = []) { if (handler) { return handler(...parameters) } throw new Error('Callback "' + name + '" must be defined.') } /** * @param {string} json * @return {Object} */ static objectFromJSON (json) { /** @type {{arrays: Object, objects: Object, properties: Object}} */ let parsedJSON = JSON.parse(json) let arrayObject = {} let result = {} for (let property in parsedJSON.arrays) { arrayObject = JSON.parse(parsedJSON.arrays[property]) result[property] = [] for (let key in arrayObject) { result[property].push(arrayObject[key]) } } for (let property in parsedJSON.objects) { result[property] = Utilities.objectFromJSON(parsedJSON.objects[property]) } for (let property in parsedJSON.properties) { result[property] = parsedJSON.properties[property] } return result } /** * @param {Object} object * @return {string} */ static objectToJSON (object) { let arrayToObject let json = {arrays: {}, objects: {}, properties: {}} for (let property in object) { if (typeof object[property] === 'object') { if (Array.isArray(object[property])) { arrayToObject = {} for (let key in object[property]) { arrayToObject[key] = object[property][key] } json.arrays[property] = JSON.stringify(arrayToObject) } else { json.objects[property] = Utilities.objectToJSON(object[property]) } } else { json.properties[property] = object[property] } } return JSON.stringify(json) } /** * @param milliseconds * @return {Promise<*>} */ static sleep (milliseconds) { return new Promise(resolve => setTimeout(resolve, milliseconds)) } /** * @param {string} text * @return {string} */ static toKebabCase (text) { return text.toLowerCase().replaceAll(' ', '-') } /** * @param {string[]} strings */ static trimAndKeepNonEmptyStrings (strings) { let nonEmptyStrings = [] for (let string of strings) { string = string.trim() if (string !== '') { nonEmptyStrings.push(string) } } return nonEmptyStrings } } class Validator { static iFramesRemover () { GM_addStyle(' iframe { display: none !important; } ') } /** * @param {StatisticsRecorder} statisticsRecorder */ constructor (statisticsRecorder) { /** * @type {StatisticsRecorder} * @private */ this._statisticsRecorder = statisticsRecorder } /** * @param {string} text * @param {Object} rules * @return {string} */ sanitize (text, rules) { if (rules) { for (const substitute in rules) { text = text.replace(rules[substitute], substitute) } } return text.trim() } /** * @param {JQuery} textNode * @param {Object} rules * @return {Validator} */ sanitizeTextNode (textNode, rules) { textNode.text(this.sanitize(textNode.text(), rules)) return this } /** * @param {string} selector * @param {Object} rules * @return {Validator} */ sanitizeNodeOfSelector (selector, rules) { let node = $(selector) if (node.length) { let sanitizedText = this.sanitize(node.text(), rules) node.text(sanitizedText) document.title = sanitizedText } return this } /** * @param {string} name * @param {JQuery} item * @param {string} selector * @return {boolean} */ validateNodeExistence (name, item, selector) { let validationCheck = item.find(selector).length > 0 this._statisticsRecorder.record(name, validationCheck) return validationCheck } /** * @param {string} name * @param {JQuery} item * @param {string} selector * @return {boolean} */ validateNodeNonExistence (name, item, selector) { let validationCheck = item.find(selector).length === 0 this._statisticsRecorder.record(name, 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 } /** * @param {string} text * @param {Object} rules * @param {string} key * @return {boolean} */ validateTextContains (text, rules, key) { let validationCheck = true if (rules) { this._statisticsRecorder.record(key, validationCheck = text.match(rules) !== null) } return validationCheck } /** * @param {string} text * @param {Object} rules * @param {string} key * @return {boolean} */ validateTextDoesNotContain (text, rules, key) { let validationCheck = true if (rules) { this._statisticsRecorder.record(key, validationCheck = text.match(rules) === null) } return validationCheck } }