您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Sleazy Fork is available in English.
Base class for search enhancement scripts
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.sleazyfork.org/scripts/416105/877459/Brazen%20Base%20Search%20Enhancer.js
// ==UserScript== // @name Brazen Base Search Enhancer // @namespace brazen // @version 2.0.0 // @author brazenvoid // @license GPL-3.0-only // @description Base class for search enhancement scripts // @require https://greasyfork.org/scripts/375557-base-resource/code/Base%20Resource.js?version=869326 // ==/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 Item Compliance Validation' const FILTER_TEXT_BLACKLIST = 'Blacklist' const FILTER_TEXT_SANITIZATION = 'Text Sanitization Rules' const FILTER_TEXT_WHITELIST = 'Whitelist' class BrazenBaseSearchEnhancer { static initialize () { BrazenBaseSearchEnhancer.throwOverrideError() } static throwOverrideError () { throw new Error('Method must be overridden.') } /** * @param {Object} options * @param {string} options.scriptPrefix * @param {string|string[]} options.itemClasses * @param {Object} options.paginator * @param {boolean} options.paginator.enable * @param {string} options.paginator.listSelector * @param {string} options.paginator.lastPageUrl * @param {Function|null} options.paginator.getPageNo * @param {Function|null} options.paginator.getPageUrl * @param {Function|null} options.paginator.afterPagination */ constructor (options) { /** * Array of item compliance filters ordered in intended sequence of execution * @type {Function[]} * @protected */ this._complianceFilters = [] /** * @type {string[]} * @protected */ this._itemClasses = Array.isArray(options.itemClasses) ? options.itemClasses : [options.itemClasses] /** * @type {string} * @protected */ this._itemClassesSelector = '.' + this._itemClasses.join(',.') /** * Pagination manager * @type {{lastPageUrl: string, listSelector: string, paginatedPageNo: number, enable: boolean, lastPageNo: number, getPageNo: Function, getPageUrl: Function, * currentPageNo: number, afterPagination: Function, targetElement: null|JQuery}} * @protected */ this._paginator = { enable: options.paginator.enable, targetElement: null, currentPageNo: 0, lastPageNo: 0, lastPageUrl: options.paginator.lastPageUrl, listSelector: options.paginator.listSelector, paginatedPageNo: 0, afterPagination: options.paginator.afterPagination, getPageNo: options.paginator.getPageNo, getPageUrl: options.paginator.getPageUrl, } /** * @type {string} * @protected */ this._scriptPrefix = options.scriptPrefix /** * @type {StatisticsRecorder} * @protected */ this._statistics = new StatisticsRecorder(this._scriptPrefix) /** * @type {BrazenUIGenerator} * @protected */ this._uiGen = new BrazenUIGenerator(this._scriptPrefix) /** * Local storage store with defaults * @type {ConfigurationManager} * @protected */ this._configurationManager = ConfigurationManager.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, 10, 1000, 'Make paginator ensure the specified number of minimum results.'). addNumberField(CONFIG_PAGINATOR_THRESHOLD, 0, 50, 'Limit paginator to concatenate the specified number of maximum pages. Zero equates to infinite.'). addRulesetField(FILTER_TEXT_BLACKLIST, '', null, (blacklistedWords, field) => { field.optimized = Utilities.buildWholeWordMatchingRegex(blacklistedWords) return blacklistedWords }). addRulesetField(FILTER_TEXT_SANITIZATION, '', (sanitizationRules) => { let sanitizationRulesText = [] for (let substitute in sanitizationRules) { sanitizationRulesText.push(substitute + '=' + sanitizationRules[substitute].join(',')) } return sanitizationRulesText }, (sanitizationRulesText, field) => { let sanitizationRules = {} if (sanitizationRulesText.length) { let fragments, validatedTargetWords for (let sanitizationRule of sanitizationRulesText) { if (sanitizationRule.includes('=')) { fragments = sanitizationRule.split('=') if (fragments[0] === '') { fragments[0] = ' ' } validatedTargetWords = Utilities.trimAndKeepNonEmptyStrings(fragments[1].split(',')) if (validatedTargetWords.length) { sanitizationRules[fragments[0]] = validatedTargetWords } } } /** * @type {{}} */ field.optimized = {} for (const substitute in sanitizationRules) { field.optimized[substitute] = Utilities.buildWholeWordMatchingRegex(sanitizationRules[substitute]) } } else { field.optimized = null } return sanitizationRules }). addRulesetField(FILTER_TEXT_WHITELIST, '', null, (whitelistedWords, field) => { field.optimized = Utilities.buildWholeWordMatchingRegex(whitelistedWords) return whitelistedWords }) /** * @type {Validator} * @protected */ this._validator = (new Validator(this._statistics)) // 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 /** * Logic to hide a non-compliant item * @type {Function} * @protected */ this._onItemHide = (item) => item.hide(100) /** * Logic to show compliant item * @type {Function} * @protected */ this._onItemShow = (item) => item.show(100) /** * 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 } /** * @param {Function} eventHandler * @param {*} parameters * @return {*} * @private */ _callEventHandler (eventHandler, ...parameters) { if (eventHandler) { return eventHandler(...parameters) } return null } /** * 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 this._callEventHandler(this._onFirstHitBeforeCompliance, item) } this._validateItemCompliance(item) if (!element['scriptProcessedOnce']) { this._callEventHandler(this._onFirstHitAfterCompliance, item) element.scriptProcessedOnce = true } this._statistics.updateUI() }) this._validatePaginationThreshold() } /** * @protected */ _createSettingsFormActions () { return this._uiGen.createFormSection().append([ this._uiGen.createFormActions([ this._uiGen.createFormButton('Apply', () => this._onApplyNewSettings(), 'Apply settings.'), this._uiGen.createFormButton('Save', () => this._onSaveSettings(), 'Apply and update saved configuration.'), this._uiGen.createFormButton('Reset', () => this._onResetSettings(), 'Revert to saved configuration.'), ]), ]) } /** * @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() } /** * @param {boolean} firstRun * @protected */ _validateCompliance (firstRun = false) { let itemLists = this._callEventHandler(this._onGetItemLists) if (!firstRun) { this._statistics.reset() itemLists.each((index, itemsList) => { this._complyItemsList($(itemsList)) }) } else { itemLists.each((index, itemList) => { ChildObserver.create().onNodesAdded((itemsAdded) => this._complyItemsList($(itemsAdded), true).observe(itemList)) this._complyItemsList($(itemList)) }) } } /** * @param {JQuery} item * @protected */ _validateItemCompliance (item) { let itemComplies = true if (!this._configurationManager.getValue(OPTION_DISABLE_COMPLIANCE_VALIDATION) && this._callEventHandler(this._onBeforeCompliance, item) !== false) { for (let complianceFilter of this._complianceFilters) { if (!complianceFilter(item)) { itemComplies = false break } } } itemComplies ? this._callEventHandler(this._onItemShow, item) : this._callEventHandler(this._onItemHide, item) } _validatePaginationThreshold () { let paginatorLimit = this._configurationManager.getValue(CONFIG_PAGINATOR_LIMIT) let paginatorThreshold = this._configurationManager.getValue(CONFIG_PAGINATOR_THRESHOLD) if (this._paginator.enable && paginatorThreshold && (paginatorLimit <= 0 || (this._paginator.paginatedPageNo - this._paginator.currentPageNo <= paginatorLimit)) && this._paginator.paginatedPageNo < this._paginator.lastPageNo ) { let compliantItems = this._paginator.targetElement.find(this._itemClassesSelector + ':visible') if (compliantItems.length < paginatorThreshold) { let nextPageUrl = this._paginator.getPageUrl(++this._paginator.paginatedPageNo) let sandbox = $('<div id="brazen-sandbox" hidden/>') sandbox.appendTo('body') sandbox.load(nextPageUrl + ' ' + this._paginator.listSelector, '', () => { sandbox.find(this._itemClassesSelector).insertAfter(this._paginator.targetElement.find(this._itemClassesSelector + ':last')) sandbox.remove() this._paginator.afterPagination(this._paginator) }) } } } /** * Initialize the script and do basic UI removals */ init () { if (this._callEventHandler(this._onValidateInit)) { this._configurationManager.initialize(this._scriptPrefix) if (this._paginator.enable) { this._paginator.targetElement = $(this._paginator.listSelector + ':first') this._paginator.currentPageNo = this._paginator.getPageNo(window.location.href) this._paginator.lastPageNo = this._paginator.getPageNo(this._paginator.lastPageUrl) this._paginator.paginatedPageNo = this._paginator.currentPageNo } this._callEventHandler(this._onBeforeUIBuild) this._embedUI(this._callEventHandler(this._onUIBuild)) this._callEventHandler(this._onAfterUIBuild) this._configurationManager.updateInterface() this._validateCompliance(true) this._uiGen.updateStatus('Initial run completed.') this._callEventHandler(this._onAfterInitialization) } } }