Greasy Fork

Brazenvoid's Base Resource

Base library for my scripts

目前为 2019-03-11 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.icu/scripts/375557/678370/Brazenvoid%27s%20Base%20Resource.js

// ==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
  }
}