您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Base class for search enhancement scripts
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.sleazyfork.org/scripts/416105/882641/Brazen%20Base%20Search%20Enhancer.js
// ==UserScript== // @name Brazen Base Search Enhancer // @namespace brazenvoid // @version 2.3.0 // @author brazenvoid // @license GPL-3.0-only // @description Base class for search enhancement scripts // ==/UserScript== const CONFIG_PAGINATOR_LIMIT = 'Pagination Limit' const CONFIG_PAGINATOR_THRESHOLD = 'Pagination Threshold' const OPTION_ALWAYS_SHOW_SETTINGS_PANE = 'Always Show Settings Pane' const OPTION_DISABLE_COMPLIANCE_VALIDATION = 'Disable All Filters' const FILTER_TEXT_BLACKLIST = 'Blacklist' const FILTER_TEXT_SEARCH = 'Search' const FILTER_TEXT_SANITIZATION = 'Text Sanitization Rules' const FILTER_TEXT_WHITELIST = 'Whitelist' class BrazenPaginator { /** * @callback PaginatorAfterPaginationEventHandler * @param {BrazenPaginator} paginator */ /** * @callback PaginatorGetPageNoFromUrlHandler * @param {string} pageUrl * @param {BrazenPaginator} paginator */ /** * @callback PaginatorGetPageUrlFromPageNoHandler * @param {number} pageNo * @param {BrazenPaginator} paginator */ /** * @callback PaginatorGetPaginationElementForPageNoHandler * @param {number} pageNo * @param {BrazenPaginator} paginator */ /** * @param {JQuery} paginationWrapper * @param {JQuery.Selector} listSelector * @param {JQuery.Selector} itemClassesSelector * @param {string } lastPageUrl * @return {BrazenPaginator} */ static create (paginationWrapper, listSelector, itemClassesSelector, lastPageUrl) { return (new BrazenPaginator).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 {boolean} * @private */ this._pageConcatenated = false /** * @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 () { if (this._pageConcatenated) { this._pageConcatenated = false let currentPageElement = this.getPaginationElementForPageNo(this._currentPageNo) let newSubsequentPageNo = this._paginatedPageNo + 1 let newSubsequentPageNoUrl = this.getPageUrlFromPageNo(newSubsequentPageNo) // Mutate current page no element to show paginated page numbers currentPageElement.text(this._currentPageNo + '-' + this._paginatedPageNo) // Get next pages' pagination elements let currentNextPageElements = currentPageElement.nextAll() if (this._paginatedPageNo === this._lastPageNo) { // Delete all pagination elements if last page is paginated currentNextPageElements.remove() } else { // Determine whether the paginated page immediately precedes the last page if (newSubsequentPageNo !== this._lastPageNo) { // If not so, determine whether pagination element for the page following the paginated page exists let newSubsequentPageElement = this.getPaginationElementForPageNo(newSubsequentPageNo) if (!newSubsequentPageElement.length) { // If it does not exist then try getting the old next page no element let oldSubsequentPageElement = this.getPaginationElementForPageNo(this._currentPageNo + 1) if (oldSubsequentPageElement.length) { // If it does exist then mutate it for this purpose oldSubsequentPageElement.attr('href', newSubsequentPageNoUrl).text(newSubsequentPageNo) } else { // If even that does not exist, then clone the less desirable alternative; the last page element and mutate it to this use let lastPageElement = this.getPaginationElementForPageNo(this._lastPageNo) lastPageElement.clone().insertAfter(currentPageElement).attr('href', newSubsequentPageNoUrl).text(newSubsequentPageNo) } } // Remove any other pagination elements for already paginated pages currentNextPageElements.each((index, element) => { let paginationLink = $(element) if (this.getPageNoFromUrl(paginationLink.attr('href')) <= this._paginatedPageNo) { paginationLink.remove() } }) } } Utilities.callEventHandler(this._onAfterPagination, [this]) } } /** * @param {number} threshold * @param {number} limit * @private */ _loadAndParseNextPage (threshold, limit) { let lastPageHasNotBeenReached = this._paginatedPageNo < this._lastPageNo let paginationLimitHasNotBeenMet = limit > 0 && (this._paginatedPageNo - this._currentPageNo) < limit let compliantItemsAreLessThanTheThreshold = this._targetElement.find(this._itemClassesSelector + ':not(.noncompliant-item)').length < threshold if (lastPageHasNotBeenReached && paginationLimitHasNotBeenMet && compliantItemsAreLessThanTheThreshold) { this._sandbox.load(this.getPageUrlFromPageNo(++this._paginatedPageNo) + ' ' + this._listSelector, '', () => { this._pageConcatenated = true this._sandbox.find(this._itemClassesSelector).insertAfter(this._targetElement.find(this._itemClassesSelector + ':last')) this._sandbox.empty() }) } else { this._conformUIToNewPaginatedState() } } /** * @param {JQuery} paginationWrapper * @param {JQuery.Selector} listSelector * @param {JQuery.Selector} itemClassesSelector * @param {string } lastPageUrl * @return {BrazenPaginator} */ 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 } getListSelector () { return this._listSelector } /** * @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) { this._loadAndParseNextPage(threshold, limit) } return this } } class BrazenBaseSearchEnhancer { /** * @return BrazenBaseSearchEnhancer */ static initialize () { BrazenBaseSearchEnhancer.throwOverrideError() } static throwOverrideError () { throw new Error('Method must be overridden.') } /** * @param {string} scriptPrefix * @param {string|string[]} itemClasses */ constructor (scriptPrefix, itemClasses) { /** * Array of item compliance filters ordered in intended sequence of execution * @type {Function[]} * @protected */ this._complianceFilters = [] /** * @type {string[]} * @protected */ this._itemClasses = Array.isArray(itemClasses) ? itemClasses : [itemClasses] /** * @type {string} * @protected */ this._itemClassesSelector = '.' + this._itemClasses.join(',.') /** * Pagination manager * @type BrazenPaginator|null * @protected */ this._paginator = null /** * @type {string} * @protected */ this._scriptPrefix = scriptPrefix /** * @type {StatisticsRecorder} * @protected */ this._statistics = new StatisticsRecorder(this._scriptPrefix) /** * @type {BrazenUIGenerator} * @protected */ this._uiGen = new BrazenUIGenerator(this._scriptPrefix) /** * @type {Validator} * @protected */ this._validator = (new Validator(this._statistics)) /** * Local storage store with defaults * @type {BrazenConfigurationManager} * @protected */ this._configurationManager = BrazenConfigurationManager.create(this._uiGen). addFlagField(OPTION_DISABLE_COMPLIANCE_VALIDATION, 'Disables all search filters.'). addFlagField(OPTION_ALWAYS_SHOW_SETTINGS_PANE, 'Always show configuration interface.'). addNumberField(CONFIG_PAGINATOR_LIMIT, 1, 50, 'Limit paginator to concatenate the specified number of maximum pages.'). addNumberField(CONFIG_PAGINATOR_THRESHOLD, 1, 1000, 'Make paginator ensure the specified number of minimum results.'). addRulesetField(FILTER_TEXT_BLACKLIST, '', null, null, (rules) => Utilities.buildWholeWordMatchingRegex(rules)). addRulesetField(FILTER_TEXT_SANITIZATION, '', (rules) => { let sanitizationRulesText = [] for (let substitute in rules) { sanitizationRulesText.push(substitute + '=' + rules[substitute].join(',')) } return sanitizationRulesText }, (rules) => { let sanitizationRules = {}, fragments, validatedTargetWords for (let sanitizationRule of rules) { if (sanitizationRule.includes('=')) { fragments = sanitizationRule.split('=') if (fragments[0] === '') { fragments[0] = ' ' } validatedTargetWords = Utilities.trimAndKeepNonEmptyStrings(fragments[1].split(',')) if (validatedTargetWords.length) { sanitizationRules[fragments[0]] = validatedTargetWords } } } return sanitizationRules }, (rules) => { let optimizedRules = {} for (const substitute in rules) { optimizedRules[substitute] = Utilities.buildWholeWordMatchingRegex(rules[substitute]) } return optimizedRules }). addRulesetField(FILTER_TEXT_WHITELIST, '', null, null, (rules) => Utilities.buildWholeWordMatchingRegex(rules)). addTextField(FILTER_TEXT_SEARCH, 'Show videos with these comma separated words in their names.') // Events /** * Operations to perform after script initialization * @type {Function} * @protected */ this._onAfterInitialization = null /** * Operations to perform after UI generation * @type {Function} * @protected */ this._onAfterUIBuild = null /** * Operations to perform before compliance validation. This callback can also be used to skip compliance validation by returning false. * @type {null} * @protected */ this._onBeforeCompliance = null /** * Operations to perform before UI generation * @type {Function} * @protected */ this._onBeforeUIBuild = null /** * Operations to perform after compliance checks, the first time a item is retrieved * @type {Function} * @protected */ this._onFirstHitAfterCompliance = null /** * Operations to perform before compliance checks, the first time a item is retrieved * @type {Function} * @protected */ this._onFirstHitBeforeCompliance = null /** * Get item lists from the page * @type {Function} * @protected */ this._onGetItemLists = null /** * @type {Function} * @private */ this._onGetItemName = null /** * Logic to hide a non-compliant item * @type {Function} * @protected */ this._onItemHide = (item) => { item.addClass('noncompliant-item') item.hide() } /** * Logic to show compliant item * @type {Function} * @protected */ this._onItemShow = (item) => { item.removeClass('noncompliant-item') item.show() } /** * Must return the generated settings section node * @type {Function} * @protected */ this._onUIBuild = null /** * Validate initiating initialization. * Can be used to stop script init on specific pages or vice versa * @type {Function} * @protected */ this._onValidateInit = () => true } /** * Filters items as per settings * @param {JQuery} itemsList * @param {boolean} fromObserver * @protected */ _complyItemsList (itemsList, fromObserver = false) { let items = fromObserver ? itemsList.filter(this._itemClassesSelector) : itemsList.find(this._itemClassesSelector) items.each((index, element) => { let item = $(element) if (typeof element['scriptProcessedOnce'] === 'undefined') { element.scriptProcessedOnce = false element.scriptItemName = Utilities.callEventHandlerOrFail('getItemName', this._onGetItemName, [item]) Utilities.callEventHandler(this._onFirstHitBeforeCompliance, [item]) } this._validateItemCompliance(item) if (!element['scriptProcessedOnce']) { Utilities.callEventHandler(this._onFirstHitAfterCompliance, [item]) element.scriptProcessedOnce = true } this._statistics.updateUI() }) } /** * @protected */ _createSettingsFormActions () { return this._uiGen.createFormSection().append([ this._uiGen.createFormActions([ this._uiGen.createFormButton('Apply', 'Apply settings.', () => this._onApplyNewSettings()), this._uiGen.createFormButton('Save', 'Apply and update saved configuration.', () => this._onSaveSettings()), this._uiGen.createFormButton('Reset', 'Revert to saved configuration.', () => this._onResetSettings()), ]), ]) } /** * @param {JQuery} UISection * @private */ _embedUI (UISection) { UISection.on('mouseleave', (event) => { if (!this._configurationManager.getValue(OPTION_ALWAYS_SHOW_SETTINGS_PANE)) { $(event.currentTarget).hide(300) } }) if (this._configurationManager.getValue(OPTION_ALWAYS_SHOW_SETTINGS_PANE)) { UISection.show() } this._uiGen.constructor.appendToBody(UISection) this._uiGen.constructor.appendToBody(this._uiGen.createSettingsShowButton('', UISection)) } _onApplyNewSettings () { this._configurationManager.update() this._validateCompliance() } _onResetSettings () { this._configurationManager.revertChanges() this._validateCompliance() } _onSaveSettings () { this._onApplyNewSettings() this._configurationManager.save() } /** * @protected */ _showNotLoggedInAlert () { alert('You need to be logged in to use this functionality') } /** * @param {boolean} firstRun * @protected */ _validateCompliance (firstRun = false) { let itemLists = Utilities.callEventHandler(this._onGetItemLists) if (!firstRun) { this._statistics.reset() itemLists.each((index, itemsList) => { this._complyItemsList($(itemsList)) }) } else { itemLists.each((index, itemList) => { let itemListObject = $(itemList) if (this._paginator && itemListObject.is(this._paginator.getListSelector())) { ChildObserver.create().onNodesAdded((itemsAdded) => { this._complyItemsList($(itemsAdded), true) this._paginator.run(this._configurationManager.getValue(CONFIG_PAGINATOR_THRESHOLD), this._configurationManager.getValue(CONFIG_PAGINATOR_LIMIT)) }).observe(itemList) } else { ChildObserver.create().onNodesAdded((itemsAdded) => this._complyItemsList($(itemsAdded), true)).observe(itemList) } this._complyItemsList(itemListObject) }) } if (this._paginator) { this._paginator.run(this._configurationManager.getValue(CONFIG_PAGINATOR_THRESHOLD), this._configurationManager.getValue(CONFIG_PAGINATOR_LIMIT)) } } /** * @param {JQuery} item * @return {boolean} * @protected */ _validateItemBlacklist (item) { let field = this._configurationManager.getField(FILTER_TEXT_BLACKLIST) return field.value.length ? this._validator.validateTextDoesNotContain(item[0].scriptItemName, field.optimized, FILTER_TEXT_BLACKLIST) : true } /** * @param {JQuery} item * @protected */ _validateItemCompliance (item) { let itemComplies = true if (!this._configurationManager.getValue(OPTION_DISABLE_COMPLIANCE_VALIDATION) && this._validateItemWhiteList(item) && Utilities.callEventHandler(this._onBeforeCompliance, [item], true) ) { for (let complianceFilter of this._complianceFilters) { if (!complianceFilter(item)) { itemComplies = false break } } } itemComplies ? Utilities.callEventHandler(this._onItemShow, [item]) : Utilities.callEventHandler(this._onItemHide, [item]) } /** * @param {JQuery} item * @return {boolean} * @protected */ _validateItemWhiteList (item) { let field = this._configurationManager.getField(FILTER_TEXT_WHITELIST) return field.value.length ? this._validator.validateTextContains(item[0].scriptItemName, field.optimized, FILTER_TEXT_WHITELIST) : true } /** * Initialize the script and do basic UI removals */ init () { if (Utilities.callEventHandler(this._onValidateInit)) { this._configurationManager.initialize(this._scriptPrefix) if (this._paginator) { this._paginator.initialize() } Utilities.callEventHandler(this._onBeforeUIBuild) this._embedUI(Utilities.callEventHandler(this._onUIBuild)) Utilities.callEventHandler(this._onAfterUIBuild) this._configurationManager.updateInterface() this._validateCompliance(true) this._uiGen.updateStatus('Initial run completed.') Utilities.callEventHandler(this._onAfterInitialization) } } }