Skrip ini tidak untuk dipasang secara langsung. Ini adalah pustaka skrip lain untuk disertakan dengan direktif meta // @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)
}
}
}