Brazenvoid's Base Resource

Base library for my scripts

当前为 2019-03-11 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/375557/678370/Brazenvoid%27s%20Base%20Resource.js

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name          Brazenvoid's Base Resource
// @namespace     brazenvoid
// @version       1.4.13
// @author        brazenvoid
// @license       GPL-3.0-only
// @description   Base library for my scripts
// @run-at	  document-end
// ==/UserScript==

const sleep = (milliseconds) => {
  return new Promise(resolve => setTimeout(resolve, milliseconds))
}

function _generateClassesIdentificationRegex (classes) {
  return new RegExp('(\\s|^)' + classes + '(\\s|$)')
}

function addClasses (node, classes) {
  if (!hasClasses(node,classes)) {
    if (node.className !== '') {
      node.className += ' '
    }
    node.className += classes
  }
  return node
}

function hasClasses (node, classes) {
  return !!node.className.match(_generateClassesIdentificationRegex(classes))
}

function removeClasses (node, classes) {
  if (hasClasses(node,classes)) {
    node.className = node.className.replace(_generateClassesIdentificationRegex(classes), ' ')
  }
  return node
}

class CaseConverters {

  toCamel (text) {
    return text.replace(/(?:^\w|[A-Z]|\b\w)/g, function (letter, index) {
      return index === 0 ? letter.toLowerCase() : letter.toUpperCase()
    }).replace(/\s+/g, '')
  }

  toKebab (text) {
    return text.toLowerCase().replace(' ', '-')
  }

  toKebabFromSnake (text) {
    return text.replace('_', '-')
  }

  toNormalFromKebab (text) {
    return text.replace('-', ' ')
  }

  toNormalFromSnake (text) {
    return text.replace('_', ' ')
  }

  toSnake (text) {
    return text.toLowerCase().replace(' ', '_')
  }
}

class ChildObserver {

  static observe (nodes, handler, doInitialRun = false) {
    let instance = new ChildObserver(handler)
    instance.observeNodes(nodes, doInitialRun)
    return instance;
  }

  constructor (handler) {
    this.config = {
      attributes: false,
      childList: true,
      subtree: false
    }
    this.handler = handler
    this.observer = new MutationObserver(function (mutations) {
      for (let mutation of mutations) {
        handler(mutation.target)
      }
    })
  }

  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 {

  constructor (key, defaultStore) {
    this.onDefaultsLoaded = null
    this.onRetrieval = null
    this.onUpdated = null

    this._key = key
    this._store = {}
    this._defaultStore = this._toJSON(defaultStore)
  }

  _fromJSON (json) {
    let arrayObject = {}
    let parsedJSON = JSON.parse(json)
    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
  }

  _getStore () {
    return window.localStorage.getItem(this._key)
  }

  _getDefaults () {
    return this._fromJSON(this._defaultStore)
  }

  _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)
  }

  delete () {
    window.localStorage.removeItem(this._key)
    return this
  }

  get () {
    return this._store
  }

  restoreDefaults () {
    this._store = this._getDefaults()

    if (this.onDefaultsLoaded !== null) {
      this.onDefaultsLoaded(this._store)
    }
    return this
  }

  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
  }

  save () {
    window.localStorage.setItem(this._key, this._toJSON(this._store))

    if (this.onUpdated !== null) {
      this.onUpdated(this._store)
    }
    return this
  }

  isPurged () {
    return this._getStore() === null
  }
}

class Logger {

  constructor (enableDebugging) {
    this.enableDebugging = enableDebugging
  }

  _log (message) {
    if (this.enableDebugging) {
      console.log(message)
    }
  }

  logTaskCompletion (task) {
    this._log('Completed: ' + task)
    this.logSeparator()
  }

  logSeparator () {
    this._log('------------------------------------------------------------------------------------')
  }

  logValidation (filterName, validationResult = null) {
    this._log('Satisfies ' + filterName + ' Filter: ' + (validationResult ? 'true' : 'false'))
  }

  logVideoCheck (videoName) {
    this._log('Checking Video: ' + videoName)
  }
}

class SelectorGenerator {

  constructor (selectorPrefix) {
    this.prefix = selectorPrefix
  }

  getSelector (selector) {
    return this.prefix + selector
  };

  getSelectorFromName (name) {
    return CaseConverters.prototype.toKebab(name)
  }

  getSettingsInputSelector (settingName) {
    return this.getSelector(this.getSelectorFromName(settingName) + '-setting')
  }

  getStatLabelSelector (statisticType) {
    return this.getSelector(this.getSelectorFromName(statisticType) + '-stat')
  };
}

class StatisticsRecorder {

  constructor (logger, selectorGenerator) {
    this.logger = logger
    this.selectorGenerator = selectorGenerator
    this.statistics = {Total: 0}
  }

  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 {

  constructor (showUI, selectorGenerator) {
    this.buttonBackroundColor = null
    this.selectorGenerator = selectorGenerator
    this.showUI = showUI
  }

  _populdateChildren (node, children) {
    for (let child of children) {
      node.appendChild(child)
    }
    return node
  }

  appendToBody (node) {
    document.getElementsByTagName('body')[0].appendChild(node)
  }

  createFormButton (caption, onClick) {

    let button = document.createElement('button')
    button.textContent = caption
    button.style.height = '30px'
    button.style.width = '100%'
    button.addEventListener('click', onClick)

    if (this.buttonBackroundColor !== null) {
      button.style.backgroundColor = this.buttonBackroundColor
    }
    return button
  }

  createFormGroup (children) {

    let divFormGroup = document.createElement('div')
    divFormGroup.style.display = 'block'
    divFormGroup.style.height = '18px'
    divFormGroup.style.marginBottom = '2px'
    divFormGroup.style.padding = '5px 0'

    return this._populdateChildren(divFormGroup, children)
  }

  createFormGroupDropdown (id, keyValuePairs, defaultValue = null) {

    let dropdown = document.createElement('select'), item
    dropdown.id = id
    dropdown.style.float = 'right'
    dropdown.style.height = '18px'
    dropdown.style.textAlign = 'center'
    dropdown.style.width = '100px'

    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
  }

  createFormGroupInput (id, type, defaultValue = null) {

    let inputFormGroup = document.createElement('input')
    inputFormGroup.id = id
    inputFormGroup.type = type

    inputFormGroup.style.height = '18px'
    inputFormGroup.style.textAlign = 'center'
    switch (type) {
      case 'number':
      case 'text':
        inputFormGroup.style.float = 'right'
        inputFormGroup.style.width = '100px'

        if (defaultValue !== null) {
          inputFormGroup.value = defaultValue
        }
        break
      case 'radio':
      case 'checkbox':
        inputFormGroup.style.float = 'left'
        inputFormGroup.style.marginRight = '5px'

        if (defaultValue !== null) {
          inputFormGroup.checked = defaultValue
        }
        break
    }

    return inputFormGroup
  }

  createFormGroupLabel (label, inputID = null, inputType = null) {

    let labelFormGroup = document.createElement('label')
    labelFormGroup.style.padding = '2px 0'
    labelFormGroup.textContent = label

    if (inputID !== null) {
      labelFormGroup.setAttribute('for', inputID)

      switch (inputType) {
        case 'number':
        case 'text':
          labelFormGroup.textContent += ': '
          labelFormGroup.style.float = 'left'
          break
        case 'radio':
        case 'checkbox':
          labelFormGroup.style.float = 'right'
          break
      }
    }
    return labelFormGroup
  }

  createFormGroupStatLabel (statisticType) {

    let labelFormGroup = document.createElement('label')
    labelFormGroup.id = this.selectorGenerator.getStatLabelSelector(statisticType)
    labelFormGroup.style.float = 'right'
    labelFormGroup.style.padding = '2px 0'
    labelFormGroup.textContent = '0'

    return labelFormGroup
  }

  createSection (IDSuffix, backgroundColor, top, width, children) {

    let section = document.createElement('section')
    section.id = this.selectorGenerator.getSelector(IDSuffix)
    section.style.color = 'black'
    section.style.display = this.showUI ? 'block' : 'none'
    section.style.fontSize = '12px'
    section.style.fontWeight = 'bold'
    section.style.position = 'fixed'
    section.style.top = top
    section.style.left = '0'
    section.style.width = width
    section.style.padding = '5px 10px'
    section.style.backgroundColor = backgroundColor
    section.style.zIndex = '1000'

    return this._populdateChildren(section, children)
  }

  createSeparator () {
    let separator = document.createElement('hr')
    separator.style.margin = '3px'
    return separator
  }

  createSettingsFormActionButton (caption, onClick) {
    let button = this.createFormButton(caption, onClick)
    button.style.width = '95px'
    return button
  }

  createSettingsFormActions (localStore, applyOnClick, addTopPadding = false) {

    let divFormActions = document.createElement('div')
    divFormActions.style.display = 'block'

    let applyButton = this.createSettingsFormActionButton('Apply', applyOnClick)
    applyButton.style.marginRight = '10px'

    if (addTopPadding) {
      divFormActions.style.paddingTop = '10px'
    }
    return this._populdateChildren(divFormActions, [applyButton, this.createSettingsFormActionButton('Reset', function () {
      localStore.retrieve()
      applyOnClick()
    })])
  }

  createSettingsFormGroup (label, inputType = 'text', defaultValue = null) {

    let divFormGroup
    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':
        divFormGroup = this.createFormGroup([labelFormGroup, inputFormGroup])
        break
      case 'radio':
      case 'checkbox':
        divFormGroup = this.createFormGroup([inputFormGroup, labelFormGroup])
        break
    }
    return divFormGroup
  }

  createSettingsDropDownFormGroup (label, keyValuePairs, defaultValue = null) {

    let dropdownID = this.selectorGenerator.getSettingsInputSelector(label)

    return this.createFormGroup([
      this.createFormGroupLabel(label, 'text', dropdownID),
      this.createFormGroupDropdown(dropdownID, keyValuePairs, defaultValue)
    ])
  }

  createSettingsHideButton (settingsSectionIDSuffix) {
    let settingsSectionID = this.selectorGenerator.getSelector(settingsSectionIDSuffix);
    return this.createFormButton('Hide', function () {
      document.getElementById(settingsSectionID).style.display = 'none'
    })
  }

  createSettingsShowButton (caption, settingsSection, fixed = true) {

    let controlButton = document.createElement('button')
    controlButton.textContent = caption

    if (fixed) {
      controlButton.style.backgroundColor = '#ffa31a'
      controlButton.style.border = '0'
      controlButton.style.color = 'black'
      controlButton.style.fontSize = '14px'
      controlButton.style.left = '0'
      controlButton.style.padding = '15px 0px'
      controlButton.style.position = 'fixed'
      controlButton.style.writingMode = 'sideways-lr'
      controlButton.style.top = '250px'
      controlButton.style.width = '30px'
      controlButton.style.zIndex = '999'
    } else {
      controlButton.style.width = '100%'
      controlButton.style.margin = '2px 5px'
      controlButton.style.padding = '2px 5px'
      controlButton.style.backgroundColor = '#ffa31a'
      controlButton.style.border = '0'
    }
    controlButton.addEventListener('click', function () {
      let settingsUI = document.getElementById(settingsSection.id)
      settingsUI.style.display = settingsUI.style.display === 'none' ? 'block' : 'none'
    })
    return controlButton
  }

  createStatisticsFormGroup (statisticsType, label = null) {

    if (label === null) {
      label = statisticsType
    }
    return this.createFormGroup([
      this.createFormGroupLabel('Filtered ' + label + ' Videos'),
      this.createFormGroupStatLabel(statisticsType),
    ])
  }

  createStoreControlPanel (localStore) {

    let controlPanelDiv = document.createElement('div')
    controlPanelDiv.style.textAlign = 'center'

    let controlPanelLabel = document.createElement('label')
    controlPanelLabel.textContent = 'Store'
    controlPanelLabel.style.display = 'block'
    controlPanelLabel.style.height = '20px'
    controlPanelLabel.style.width = '100%'

    let resetButton = this.createStoreControlPanelButton('Reset', 'Reset store values to user defaults', function () {
      localStore.restoreDefaults()
    })
    resetButton.style.margin = '0 5px'

    return this._populdateChildren(controlPanelDiv, [
      controlPanelLabel,
      this.createStoreControlPanelButton('Update', 'Save UI settings in store', function () {
        localStore.save()
      }),
      resetButton,
      this.createStoreControlPanelButton('Purge', 'Purge store', function () {
        localStore.delete()
      }),
    ])
  }

  createStoreControlPanelButton (caption, tooltip, onClick) {

    let button = this.createFormButton(caption, onClick)
    button.title = tooltip
    button.style.padding = '0 2px'
    button.style.width = '55px'

    return button
  }

  getSettingsInput (label) {
    return document.getElementById(this.selectorGenerator.getSettingsInputSelector(label))
  }

  getSettingsInputCheckedStatus (label) {
    return this.getSettingsInput(label).checked
  }

  getSettingsInputValue (label) {
    return this.getSettingsInput(label).value;
  }

  setSettingsInputCheckedStatus (label, bool) {
    this.getSettingsInput(label).checked = bool
  }

  setSettingsInputValue (label, value) {
    this.getSettingsInput(label).value = value
  }
}

class Validator {

  static iFramesRemover () {
    GM_addStyle(`
      iframe {
          display: none !important;
      }
    `)
  }

  constructor (statisticsRecorder) {
    this._blacklist = []
    this._filters = []
    this._optimizedBlacklist = []
    this._optimizedSanitizationRules = {}
    this._sanitizationRules = []
    this._statisticsRecorder = statisticsRecorder
  }

  addBlacklistFilter (blacklistedWords) {
    this._blacklist = blacklistedWords
    return this
  }

  addConditionalFilter (name, shouldBe, getValueCallback) {
    return this
  }

  addRangeFilter (name, conditionCallback) {
    return this
  }

  addSanitizationFilter (santizationRules, videoTitleSelector, videoPageTitleSelector) {
    this._sanitizationRules = santizationRules
    return this
  }

  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
  }

  sanitize (text) {
    for (const substitute in this._optimizedSanitizationRules) {
      for (const subject of this._optimizedSanitizationRules[substitute]) {
        text = text.replace(subject, substitute)
      }
    }
    return text
  }

  sanitizeVideoItem(videoNameNode) {
    videoNameNode.textContent = this.sanitize(videoNameNode.textContent)
    return this
  }

  sanitizeVideoPage(videoNameNodeSelector) {

    let videoNameNode = document.querySelector(videoNameNodeSelector)
    if (videoNameNode !== null) {

      let sanitizedVideoName = this.sanitize(videoNameNode.textContent)
      videoNameNode.textContent = sanitizedVideoName
      document.title = sanitizedVideoName
    }
    return this
  }

  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
  }

  validateRange (name, value, bounds) {

    let validationCheck = true

    if (bounds[0] > 0) {
      validationCheck = value >= bounds[0]
    }
    if (bounds[1] > 0) {
      validationCheck = value <= bounds[1]
    }
    this._statisticsRecorder.record(name, validationCheck)

    return validationCheck
  }

  validateRangeFilter (name, lowerBound, upperBound, getValueCallback) {
    if (lowerBound > 0 || upperBound > 0) {
      return this.validateRange(name, getValueCallback(), [lowerBound, upperBound])
    }
    return true
  }
}