Greasy Fork

Greasy Fork is available in English.

RateYourMusic - Visual Rating Bar

Adds a visual rating bar on releases pages.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         RateYourMusic - Visual Rating Bar
// @namespace    rym-visual-rating-bar
// @version      0.5
// @description  Adds a visual rating bar on releases pages.
// @author       ewauq
// @supportURL   https://github.com/ewauq/userscripts/issues
// @match        https://rateyourmusic.com/release/*
// @icon         https://www.google.com/s2/favicons?domain=rateyourmusic.com
// ==/UserScript==
/* eslint-disable @typescript-eslint/no-extra-semi */
/* eslint-disable indent */
;(function () {
  'use strict'

  const Themes = {
    vibrant: ['#fa4146', '#fa961e', '#fac850', '#91be6e', '#55a569', '#4182a5'],
    rainbow: ['#ff0000', '#ff961e', '#ffff00', '#00cd00', '#aaaaff', '#6532ff', '#c800ff'],
    neon: ['#ff00d9', '#ffb400', '#96ff00', '#00ffc8', '#0082ff', '#b432ff'],
    colorBlind: ['#dc321e', '#ffb400', '#faff00', '#23fffa', '#28b4ff', '#2850ff'],
  }

  class Localstorage {
    constructor(localstorageClient) {
      this.getValue = (name) => {
        const storedValue = this.localstorageClient.getItem(name)
        if (!storedValue) return
        return storedValue
      }
      this.setValue = (name, value) => {
        this.localstorageClient.setItem(name, String(value))
      }
      this.localstorageClient = localstorageClient
    }
  }

  const localstorageClient$1 = new Localstorage(localStorage)
  class OptionsMenu {
    constructor() {
      this.getBackgroundColor = () => {
        const elementNode = document.querySelector('.release_right_column')
        if (!elementNode) throw new Error("Can't find the .release_right_column element")
        return window.getComputedStyle(elementNode).backgroundColor
      }
    }
    show() {
      const overlayNode = document.getElementById('userscript-options-menu')
      const containerNode = document.querySelector('#userscript-options-menu > div')
      const headerNode = document.getElementById('page_header')
      const pageWrapper = document.getElementById('content_wrapper_outer')
      document.body.style.overflow = 'hidden'
      if (containerNode) containerNode.style.backgroundColor = this.getBackgroundColor()
      if (overlayNode) overlayNode.style.display = 'flex'
      if (headerNode) headerNode.style.filter = 'blur(3px)'
      if (pageWrapper) pageWrapper.style.filter = 'blur(3px)'
    }
    hide() {
      const optionsMenuOverlayNode = document.getElementById('userscript-options-menu')
      const headerNode = document.getElementById('page_header')
      const pageWrapper = document.getElementById('content_wrapper_outer')
      document.body.style.overflow = 'visible'
      if (optionsMenuOverlayNode) optionsMenuOverlayNode.style.display = 'none'
      if (headerNode) headerNode.style.filter = 'none'
      if (pageWrapper) pageWrapper.style.filter = 'none'
      location.reload()
    }
    build() {
      const optionsMenuOverlay = document.createElement('div')
      optionsMenuOverlay.id = 'userscript-options-menu'
      optionsMenuOverlay.style.width = '100%'
      optionsMenuOverlay.style.height = '100%'
      optionsMenuOverlay.style.zIndex = '1010'
      optionsMenuOverlay.style.top = '0'
      optionsMenuOverlay.style.left = '0'
      optionsMenuOverlay.style.position = 'fixed'
      optionsMenuOverlay.style.display = 'none'
      optionsMenuOverlay.style.justifyContent = 'center'
      optionsMenuOverlay.style.fontFamily = 'Roboto, Helvetica, Arial, sans-serif'
      optionsMenuOverlay.style.fontSize = '16px'
      optionsMenuOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
      const optionsMenuContainer = document.createElement('div')
      optionsMenuContainer.style.width = '450px'
      optionsMenuContainer.style.margin = '300px auto auto auto'
      optionsMenuContainer.style.padding = '20px'
      optionsMenuContainer.style.zIndex = '1020'
      optionsMenuContainer.style.backgroundColor = '#FFFFFF'
      optionsMenuContainer.style.borderRadius = '6px'
      optionsMenuContainer.style.outline = '6px solid rgba(0,0,0,0.3)'
      const optionsMenuSectionTitle = document.createElement('h2')
      optionsMenuSectionTitle.style.fontSize = '20px'
      optionsMenuSectionTitle.style.marginBottom = '10px'
      optionsMenuSectionTitle.style.fontWeight = 'bold'
      optionsMenuSectionTitle.innerHTML = 'Options'
      const optionsElements = [
        {
          type: 'checkbox',
          name: 'animation-option',
          label: 'Animate the bar on display',
          default: true,
        },
        {
          type: 'select',
          name: 'theme-option',
          options: Object.keys(Themes),
          label: 'Color theme',
          default: 'vibrant',
        },
        {
          type: 'select',
          name: 'color-style-option',
          options: ['gradual', 'blend'],
          label: 'Color mode',
          default: 'gradual',
        },
        {
          type: 'number',
          name: 'height-option',
          min: 0,
          max: 100,
          label: 'Bar height',
          default: 20,
        },
        {
          type: 'number',
          name: 'border-radius-option',
          min: 0,
          label: 'Bar border radius',
          default: 6,
        },
        {
          type: 'checkbox',
          name: 'shadow-option',
          label: 'Display a shadow on the bar',
          default: true,
        },
      ]
      optionsMenuContainer.appendChild(optionsMenuSectionTitle)
      const optionsElementsWrapper = document.createElement('div')
      optionsElementsWrapper.style.display = 'flex'
      optionsElementsWrapper.style.flexDirection = 'column'
      optionsMenuContainer.appendChild(optionsElementsWrapper)
      let input
      optionsElements.forEach((element) => {
        var _a
        let storedValue = localstorageClient$1.getValue(element.name)
        if (!storedValue) {
          localstorageClient$1.setValue(element.name, element.default)
          storedValue = String(element.default)
        }
        if (element.type == 'checkbox') {
          const label = document.createElement('label')
          label.style.fontWeight = 'normal'
          label.style.padding = '10px 0px'
          label.htmlFor = element.name
          label.innerHTML = element.label
          input = document.createElement('input')
          input.type = 'checkbox'
          input.id = element.name
          input.checked = storedValue === 'true'
          input.style.marginRight = '10px'
          input.style.height = '14px'
          input.style.width = '14x'
          optionsElementsWrapper.appendChild(label)
          label.prepend(input)
          input.addEventListener('change', (event) => {
            const target = event.target
            localstorageClient$1.setValue(element.name, String(target.checked))
          })
        } else if (element.type == 'number') {
          const label = document.createElement('label')
          label.style.fontWeight = 'normal'
          label.style.padding = '10px 0px'
          label.htmlFor = element.name
          label.innerHTML = element.label
          input = document.createElement('input')
          input.type = 'number'
          input.min = String(element.min)
          input.max = String(element.max)
          input.id = element.name
          input.value = storedValue
          input.style.marginLeft = '10px'
          input.style.width = '50px'
          optionsElementsWrapper.appendChild(label)
          label.appendChild(input)
          input.addEventListener('change', (event) => {
            const target = event.target
            localStorage.setItem(element.name, target.value)
          })
        } else if (element.type == 'select') {
          const label = document.createElement('label')
          label.style.fontWeight = 'normal'
          label.style.padding = '10px 0px'
          label.htmlFor = element.name
          label.innerHTML = element.label
          input = document.createElement('select')
          input.id = element.name
          input.style.marginLeft = '10px'
          ;(_a = element.options) === null || _a === void 0
            ? void 0
            : _a.forEach((option) => {
                const selectOption = document.createElement('option')
                selectOption.value = option
                selectOption.innerHTML = option
                if (option == storedValue) selectOption.defaultSelected = true
                input.appendChild(selectOption)
              })
          optionsElementsWrapper.appendChild(label)
          label.appendChild(input)
          input.addEventListener('change', (event) => {
            const target = event.target
            localstorageClient$1.setValue(element.name, target.value)
          })
        }
        optionsMenuOverlay.addEventListener('click', (event) => {
          const target = event.target
          if (target.id === 'userscript-options-menu') this.hide()
        })
        document.addEventListener('keydown', (event) => {
          if (event.key === 'Escape') this.hide()
        })
      })
      const footerInformation = document.createElement('div')
      footerInformation.style.color = 'gray'
      footerInformation.style.fontSize = '13px'
      footerInformation.style.marginTop = '16px'
      footerInformation.innerHTML = `Your settings are automatically saved in your browser local storage.
    <br/>
    Press ESC or click out the window to close it and reload the page.`
      optionsMenuOverlay.appendChild(optionsMenuContainer)
      optionsMenuContainer.appendChild(footerInformation)
      return optionsMenuOverlay
    }
  }

  const localstorageClient = new Localstorage(localStorage)
  class VisualRatingBar {
    constructor() {
      this.barOptions = {
        animation: true,
        borderRadius: 6,
        theme: 'vibrant',
        height: 20,
        shadow: true,
        style: 'gradual',
      }
      this.getBackgroundColor = () => {
        const elementNode = document.querySelector('.release_right_column')
        if (!elementNode) throw new Error("Can't find the .release_right_column element")
        return window.getComputedStyle(elementNode).backgroundColor
      }
      this.getThemeMode = () => {
        const currentThemeMode = localstorageClient.getValue('theme')
        if (!currentThemeMode) return null
        if (!['eve', 'night', 'light'].includes(currentThemeMode)) return null
        return currentThemeMode
      }
      this.buildGradient = () => {
        const { theme, style } = this.barOptions
        const colorsCount = Themes[theme].length
        const colorCodes = Themes[theme]
        const gradientStep = 100 / colorsCount
        const cssValues = []
        let currentStep = 0
        colorCodes.forEach((color, index) => {
          cssValues.push(`${color} ${currentStep}%`)
          if (style === 'gradual' && index + 1 < colorsCount) {
            cssValues.push(`${color} ${currentStep + gradientStep}%`)
          }
          if (index + 1 == colorsCount) {
            cssValues.push(`${color} 98%`)
            cssValues.push('transparent 98%')
            cssValues.push('transparent 100%')
          }
          currentStep += gradientStep
        })
        return cssValues.join(', ')
      }
      const animationValue = localstorageClient.getValue('animation-option')
      if (animationValue !== undefined) this.barOptions.animation = animationValue === 'true'
      const borderRadiusValue = localstorageClient.getValue('border-radius-option')
      if (borderRadiusValue === 'false') {
        this.barOptions.borderRadius = 0
      } else if (borderRadiusValue !== undefined) {
        this.barOptions.borderRadius = parseInt(borderRadiusValue)
      }
      const themeValue = localstorageClient.getValue('theme-option')
      if (themeValue !== undefined && Object.keys(Themes).includes(themeValue)) {
        this.barOptions.theme = themeValue
      }
      const styleValue = localstorageClient.getValue('color-style-option')
      if (styleValue !== undefined) this.barOptions.style = styleValue
      const heightOption = localstorageClient.getValue('height-option')
      if (heightOption !== undefined) this.barOptions.height = parseInt(heightOption)
      const shadowValue = localstorageClient.getValue('shadow-option')
      if (shadowValue !== undefined) this.barOptions.shadow = shadowValue === 'true'
    }
    init() {
      var _a, _b, _c
      const ratingNode = document.querySelector('span.avg_rating')
      if (!ratingNode) return
      const { animation, height, borderRadius, shadow } = this.barOptions
      const ratingText =
        ratingNode === null || ratingNode === void 0 ? void 0 : ratingNode.textContent
      if (!ratingText) throw new Error("Can't retrieve the rating text of element span.avg_rating")
      const rymThemeMode = this.getThemeMode()
      const rating = parseFloat(ratingText.trim())
      const ratingPercentage = (rating * 100) / 5
      const barWrapper = document.createElement('div')
      barWrapper.id = 'userscript-bar-wrapper'
      barWrapper.style.height = `${height}px`
      barWrapper.style.cursor = 'pointer'
      barWrapper.style.position = 'relative'
      barWrapper.style.marginTop = '6px'
      barWrapper.title = `${rating}/5 (${ratingPercentage}%)`
      const barMask = document.createElement('div')
      barMask.style.width = animation ? '90%' : `${100 - ratingPercentage}%`
      barMask.style.height = '100%'
      barMask.style.backgroundColor = this.getBackgroundColor()
      barMask.style.marginTop = `-${height}px`
      barMask.style.right = '0'
      barMask.style.position = 'absolute'
      barMask.style.filter = 'contrast(80%)'
      if (animation)
        barMask.style.transition = 'width 500ms cubic-bezier(0.250, 0.460, 0.450, 0.940)'
      if (borderRadius) barMask.style.borderTopRightRadius = `${borderRadius}px`
      if (borderRadius) barMask.style.borderBottomRightRadius = `${borderRadius}px`
      if (shadow) barMask.style.boxShadow = '#00000063 0px 0px 3px 0px inset'
      const barGradient = document.createElement('div')
      barGradient.style.height = '100%'
      barGradient.style.background = `linear-gradient(90deg, ${this.buildGradient()})`
      if (borderRadius) barGradient.style.borderRadius = `${borderRadius}px`
      if (rymThemeMode !== 'light') barGradient.style.filter = 'saturate(50%)'
      if (shadow) barGradient.style.boxShadow = '#0000009c 0px 0px 4px 0px inset'
      const ratingNodeParent =
        (_a = ratingNode === null || ratingNode === void 0 ? void 0 : ratingNode.parentNode) ===
          null || _a === void 0
          ? void 0
          : _a.parentNode
      ratingNodeParent === null || ratingNodeParent === void 0
        ? void 0
        : ratingNodeParent.appendChild(barWrapper)
      barWrapper.appendChild(barGradient)
      barWrapper.appendChild(barMask)
      const visibilityCheckInterval = window.setInterval(function () {
        const barWrapperNode = document.getElementById('userscript-bar-wrapper')
        if (barWrapperNode) {
          barMask.style.width = `${100 - ratingPercentage}%`
          window.clearInterval(visibilityCheckInterval)
        }
      }, 50)
      ;(_b = document.querySelectorAll('div.header_theme_button')[1]) === null || _b === void 0
        ? void 0
        : _b.addEventListener('click', () => {
            const themeMode = this.getThemeMode()
            barMask.style.backgroundColor = this.getBackgroundColor()
            barGradient.style.filter = `saturate(${themeMode === 'light' ? 100 : 50}%)`
          })
      const optionsMenu = new OptionsMenu()
      document.body.appendChild(optionsMenu.build())
      ;(_c = document.getElementById('userscript-bar-wrapper')) === null || _c === void 0
        ? void 0
        : _c.addEventListener('click', () => {
            optionsMenu.show()
          })
      console.log('[USERSCRIPT] RateYourMusic Visual Rating Bar added')
    }
  }
  const visualRatingBar = new VisualRatingBar()
  visualRatingBar.init()
})()