Sleazy Fork is available in English.

Iwara Enhancement

Please refer to the script's homepage for more information.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         Iwara Enhancement
// @name:zh-CN   Iwara增强
// @description  Please refer to the script's homepage for more information.
// @description:zh-CN 请参考脚本的主页以获取更多信息
// @noframes
// @grant        unsafeWindow
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_download
// @grant        GM_info
// @grant        GM_addStyle
// @require      https://unpkg.com/[email protected]
// @require      https://unpkg.com/[email protected]
// @require      https://unpkg.com/[email protected]
// @match        *://*.iwara.tv/*
// @namespace    https://github.com/guansss/userscripts
// @version      1.5
// @author       guansss
// @source       https://github.com/guansss/userscripts
// @supportURL   https://github.com/guansss/userscripts/issues
// ==/UserScript==
;(() => {
  "use strict"

  $

  const external_Vue_namespaceObject = Vue

  function onClickOutside(el, callback) {
    document.addEventListener("click", handler)

    function handler(e) {
      if (!el.contains(e.target)) {
        document.removeEventListener("click", handler)
        callback(e)
      }
    }
  }

  /**
   * MutationObserver that calls callback with just a single mutation.
   */
  class SimpleMutationObserver extends MutationObserver {
    // since calling `new NodeList()` is illegal, this is the only way to create an empty NodeList

    constructor(callback) {
      super((mutations) => {
        for (const mutation of mutations) if (this.callback(mutation)) break
      })
      this.callback = callback
    }

    /**
     * @param options.immediate - When observing "childList", immediately trigger a mutation with existing nodes.
     */
    observe(target, options) {
      super.observe(target, options)

      if (options && options.immediate && options.childList && target.childNodes.length)
        this.callback({
          target,
          type: "childList",
          addedNodes: target.childNodes,
          removedNodes: SimpleMutationObserver.emptyNodeList,
        })
    }
  }

  SimpleMutationObserver.emptyNodeList = document.querySelectorAll("#__absolutely_nonexisting")
  function hasClass(node, className) {
    var _classList
    return !!(
      null !== (_classList = node.classList) &&
      void 0 !== _classList &&
      _classList.contains(className)
    )
  }

  let log

  setLogger(console.log)

  function setLogger(logger) {
    log = logger.bind(console, `[${GM_info.script.name}]`)
  }

  function clamp(val, min, max) {
    return val < min ? min : val > max ? max : val
  }

  function parseAbbreviatedNumber(str) {
    const units = { k: 1e3, m: 1e6, b: 1e9 }

    let number = parseFloat(str)

    if (!isNaN(number)) {
      const unit = str.trim().slice(-1).toLowerCase()

      return number * (units[unit] || 1)
    }

    return NaN
  }

  function formatDate(date) {
    let delimiter = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : "/"
    return [
      date.getFullYear(),
      date.getMonth() + 1,
      date.getDate(),
      date.getHours(),
      date.getMinutes(),
      date.getSeconds(),
    ]
      .map((num) => String(num).padStart(2, "0"))
      .join(delimiter)
  }

  function formatError(e) {
    let fallback = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : "Unknown error"
    if ("string" === typeof e) return e || fallback

    if (null !== e && "object" === typeof e) {
      if (e instanceof Event && "error" === e.type) return "Failed to load resource"

      if (e.message) return e.message

      const str = String(e)
      return "[object Object]" === str ? fallback : str
    }

    return fallback
  }

  function adjustHexColor(color, amount) {
    return color.replace(/\w\w/g, (color) =>
      clamp(parseInt(color, 16) + amount, 0, 255)
        .toString(16)
        .padStart(2, "0")
    )
  }

  // written by ChatGPT
  function adjustAlpha(color, alpha) {
    if (alpha < 0 || alpha > 1) throw new Error("Alpha value must be between 0 and 1")

    let r, g, b

    if (color.startsWith("#")) {
      if (7 !== color.length) throw new Error("Invalid color format")

      r = parseInt(color.slice(1, 3), 16)
      g = parseInt(color.slice(3, 5), 16)
      b = parseInt(color.slice(5, 7), 16)
    } else if (color.startsWith("rgb")) {
      const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/)

      if (!match) throw new Error("Invalid color format")

      r = parseInt(match[1], 10)
      g = parseInt(match[2], 10)
      b = parseInt(match[3], 10)
    } else throw new Error("Invalid color format")

    return `rgba(${r}, ${g}, ${b}, ${alpha})`
  }

  /**
   * Replaces characters that are forbidden in file systems.
   */
  function sanitizePath(path, illegalCharReplacement) {
    let keepDelimiters = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : true
    path = path.replace(/[*:<>?|]/g, illegalCharReplacement)

    if (!keepDelimiters) path = path.replace(/[\\/]/g, illegalCharReplacement)

    return path
  }

  const external_VueI18n_namespaceObject = VueI18n

  const en_namespaceObject = JSON.parse(
    '{"language":"English","name":"Iwara Enhancement","description":"Multiple UI enhancements for better experience.","ui":{},"s":{"enabled":"Enabled","extra":"Extra settings","download":{"label":"Download","auto":{"label":"One-click Download","desc":"Automatically starts download when clicking the download button."},"resolution":{"label":"Preferred Resolution for Download"},"filename":{"label":"Filename","desc":"Filename template to use when downloading a video.<br>Each keyword should be surrounded by <b>{\'{ }\'}</b>.","preview":"Preview","key":{"id":"video\'s ID","title":"video\'s title","res":"video\'s resolution","author":"author\'s name","date":"date time when the download starts","up_date":"date time when the video was uploaded","date_ts":"DATE in timestamp format","up_date_ts":"UP_DATE in timestamp format"},"replace_illegal_char":"Replace characters that are disallowed in filename with:","tips":["Tips","You can use \\"/\\" in the filename to create subfolders, for example {AUTHOR}/{DATE}-{TITLE}.","If the filename doesn\'t work, check if you have any browser extensions that may interfere with the download, such as the Aria2 extension."],"warn":"Only works in Tampermonkey.","warn_tm":{"desc":"This feature requires Tampermonkey\'s download mode to be set to <b>Browser API</b>, please follow <a href=\'https://www.tampermonkey.net/faq.php#Q302\' target=\'_blank\' rel=\'noopener noreferrer\'>this guide↗</a> and refresh the page once done.","steps":[]}}},"ui":{"label":"UI","like_rate":{"label":"Like rate","desc":"Displays like rates in video and image list."},"highlight_threshold":{"label":"Highlight threshold","desc":"Highlights video and image items over certain like rate."},"highlight_bg":{"label":"Highlight opacity"},"widen_content":{"label":"Widen content","desc":"Widen the content area in video and image pages.","scale":"Additional scale (%)"}},"script":{"label":"Script","language":{"label":"Language"}}}}'
  )

  const zh_namespaceObject = JSON.parse(
    '{"language":"中文","name":"Iwara增强","description":"多种增强体验的界面优化","s":{"enabled":"启用","extra":"更多选项","download":{"label":"下载","auto":{"label":"一键下载","desc":"点击下载按钮时自动开始下载"},"resolution":{"label":"优先下载的分辨率"},"filename":{"label":"文件名","desc":"下载视频时使用的文件名模板<br>每个关键词必须使用 <b>{\'{ }\'}</b> 来包围","preview":"预览","key":{"id":"视频 ID","title":"视频标题","author":"作者名","res":"视频分辨率","date":"下载开始时的日期和时间","up_date":"视频发布时的日期和时间","date_ts":"DATE 的时间戳格式","up_date_ts":"UP_DATE 的时间戳格式"},"replace_illegal_char":"将文件名中的非法字符替换为:","tips":["提示","可以在文件名里使用\\"/\\"来创建文件夹,例如{AUTHOR}/{DATE}-{TITLE}","如果文件名不起作用,检查一下是否安装了与下载相关的浏览器插件,比如 Aria2 插件"],"warn":"该功能仅在 Tampermonkey 中可用","warn_tm":{"desc":"该功能需要启用 Tampermonkey 的<b>浏览器 API</b>下载模式,请按照以下步骤启用,或者查看<a href=\'https://www.tampermonkey.net/faq.php#Q302\' target=\'_blank\' rel=\'noopener noreferrer\'>官方指南↗</a>","steps":["进入 Tampermonkey 的设置面板,选择<b>设置</b>标签页","在<b>通用</b>里,设置<b>配置模式</b>为<b>高级<b>(或者<b>初学者</b>)","在<b>下载 BETA</b>里,设置<b>下载模式</b>为<b>浏览器 API</b>","在<b>下载 BETA</b>里,点击<b>保存</b>","如果请求权限的话,选择同意","刷新当前页面"]}}},"ui":{"label":"界面","like_rate":{"label":"喜爱率","desc":"在视频和图片列表里显示喜爱率"},"highlight_threshold":{"label":"高亮分界点","desc":"喜爱率高于此值的视频和图片将会被高亮显示"},"highlight_bg":{"label":"高亮透明度"},"widen_content":{"label":"加宽内容区域","desc":"加宽视频页和图片页的内容区域","scale":"额外缩放 (%)"}},"script":{"label":"脚本","language":{"label":"语言"}}}}'
  )

  /* harmony default export */ const i18n = { zh: zh_namespaceObject, en: en_namespaceObject }

  function createStorage(prefix, schema) {
    if (prefix) prefix += "."

    return {
      get(key) {
        return GM_getValue(prefix + key, schema[key])
      },
      set(key, val) {
        if ("function" === typeof val) val = val(this.get(key))

        GM_setValue(prefix + key, val)
      },
    }
  }

  const storage = createStorage("", {
    v: GM_info.script.version,
    locale: navigator.language,
    volume: 0.5,
    auto_down_enabled: true,
    preferred_res: "Source",
    filename: "{DATE} {TITLE} - {AUTHOR} ({ID})",
    illegal_char_replacement: "_",
    dark: false,
    like_rates: true,
    like_rate_highlight: 4,
    like_rate_highlight_opacity: 0.2,
    widen_content: true,
    widen_content_scale: 100,
  })

  const i18n_i18n = (0, external_VueI18n_namespaceObject.createI18n)({
    locale: storage.get("locale"),
    fallbackLocale: "en",
    messages: i18n,

    // disable warnings - I know what I'm doing!!
    silentFallbackWarn: true,
    silentTranslationWarn: true,
    warnHtmlInMessage: "off",
  })

  function matchLocale(locale) {
    return i18n_i18n.global.availableLocales.includes(locale)
      ? locale
      : i18n_i18n.global.availableLocales.find((loc) => locale.startsWith(loc)) || "en"
  }

  const locale = (0, external_Vue_namespaceObject.ref)(storage.get("locale"))

  ;(0, external_Vue_namespaceObject.watchEffect)(() => {
    i18n_i18n.global.locale = locale.value

    storage.set("locale", locale.value)
  })

  function useConfigSettings() {
    // locale that will actually be used, with fallback applied
    const activeLocale = (0, external_Vue_namespaceObject.computed)(() => matchLocale(locale.value))

    return { locale, activeLocale }
  }

  /* harmony default export */ function mitt(n) {
    return {
      all: (n = n || new Map()),
      on: function (t, e) {
        var i = n.get(t)
        i ? i.push(e) : n.set(t, [e])
      },
      off: function (t, e) {
        var i = n.get(t)
        i && (e ? i.splice(i.indexOf(e) >>> 0, 1) : n.set(t, []))
      },
      emit: function (t, e) {
        var i = n.get(t)
        i &&
          i.slice().map(function (n) {
            n(e)
          }),
          (i = n.get("*")) &&
            i.slice().map(function (n) {
              n(t, e)
            })
      },
    }
  }

  const ready = new Promise((resolve) => {
    if ("loading" === document.readyState)
      document.addEventListener("DOMContentLoaded", () => resolve())
    else resolve()
  })

  function once(emitter, event, listener) {
    const fn = (data) => {
      emitter.off(event, fn)
      listener(data)
    }

    emitter.on(event, fn)

    return fn
  }

  function setupPaging() {
    ready.then(() => {
      const appDiv = document.getElementById("app")

      if (!appDiv) {
        log("Missing app div.")
        return
      }

      log("Start observing pages.")

      const appObserver = new SimpleMutationObserver((mutation) => {
        detectPageChange(appDiv, mutation.addedNodes, "pageEnter")
        detectPageChange(appDiv, mutation.removedNodes, "pageLeave")
      })
      appObserver.observe(appDiv, { childList: true, immediate: true })
    })
  }

  const emitter = mitt()

  let currentClassName = ""

  emitter.on("pageEnter", (className) => (currentClassName = className))

  const ALL = "*"

  // page listener for iwara
  function page(id, enter) {
    const match = (() => {
      if (id === ALL) return () => id

      const ids = "string" === typeof id ? [id] : id
      const classes = ids.map((id) => `page-${id}`)

      return (className) => {
        const split = className.split(" ")
        const index = classes.findIndex((cls) => split.includes(cls))

        return ids[index]
      }
    })()

    function callIfMatch(listener) {
      return (className) => {
        const matchedID = match(className)

        if (void 0 !== matchedID)
          try {
            listener(matchedID)
          } catch (e) {
            log("Error executing page listener", e)
          }
      }
    }

    const onPageEnter = callIfMatch((matchedID) => {
      enter(matchedID, (onLeave) => {
        once(emitter, "pageLeave", callIfMatch(onLeave))
      })
    })

    emitter.on("pageEnter", onPageEnter)
  }

  function detectPageChange(appDiv, nodes, event) {
    if (nodes.length)
      // a valid class name will be like "page page-videoList", where "videoList" is the ID
      for (const node of nodes)
        if (hasClass(node, "page")) {
          // sometimes there are two (maybe more) "page" elements, and one of them contains only the "page" class,
          // we ignore it in this case
          const hasOtherPageElements =
            $(appDiv)
              .children(".page")
              .filter((_, e) => e !== node && !node.className.includes("page-")).length > 0

          if (!hasOtherPageElements) emitter.emit(event, node.className)
          break
        }
  }

  function cancelOnLeave(onLeave, promise) {
    onLeave(() => promise.cancel())
    return promise
  }
  let reactEventHandlersKey = ""

  function getReactEventHandlers(element) {
    if (reactEventHandlersKey) return element[reactEventHandlersKey]

    for (const key of Object.keys(element))
      if (key.startsWith("__reactEventHandlers$")) {
        reactEventHandlersKey = key
        return element[key]
      }
  }

  // sometimes I just don't want the script to depend on Lodash...
  function throttle(fn, timeout) {
    let timer = 0

    return function () {
      for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++)
        args[_key] = arguments[_key]
      if (timer) return

      timer = setTimeout(() => {
        fn.apply(null, args)

        timer = 0
      }, timeout)
    }
  }

  /**
   * Periodically calls given function until the return value is truthy.
   * @returns A CancelablePromise that resolves with the function's return value when truthy.
   */
  function until(fn) {
    let interval = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0
    let cancelOnReload = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : true
    let cancelled = false

    if (cancelOnReload);

    const STOP = Symbol()

    const promise = new Promise((resolve, reject) => {
      const run = () => {
        if (cancelled) return STOP

        const result = fn()

        if (result) {
          resolve(result)
          return STOP
        }
      }

      const timerId = setInterval(() => {
        try {
          if (run() === STOP) clearInterval(timerId)
        } catch (e) {
          reject(e)
          clearInterval(timerId)
        }
      }, interval)
    })
    promise.cancel = () => (cancelled = true)

    return promise
  }

  /**
   * Periodically calls given function until the returned jQuery object is not empty.
   * @returns A CancelablePromise that resolves with the jQuery object.
   */
  function until$(fn) {
    let interval = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0
    let cancelOnReload = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : true
    return until(
      () => {
        const result = fn()

        if (result.length) return result
      },
      interval,
      cancelOnReload
    )
  }

  // a partial structure of the video data defined in iwara's video page,
  // including only the properties we need

  const FILENAME_KEYWORDS = [
    "ID",
    "TITLE",
    "RES",
    "AUTHOR",
    "DATE",
    "UP_DATE",
    "DATE_TS",
    "UP_DATE_TS",
  ]
  const RESOLUTIONS = ["Source", "540p", "360p"]

  const autoEnabled = (0, external_Vue_namespaceObject.ref)(storage.get("auto_down_enabled"))
  const filenameTemplate = (0, external_Vue_namespaceObject.ref)(storage.get("filename"))
  const illegalCharReplacement = (0, external_Vue_namespaceObject.ref)(
    storage.get("illegal_char_replacement")
  )
  const resolution = (0, external_Vue_namespaceObject.ref)(storage.get("preferred_res"))

  const videoInfo = (0, external_Vue_namespaceObject.reactive)({
    id: "",
    title: "",
    author: "",
    created: 0,
    size: 0,
    error: "",
  })
  const sources = (0, external_Vue_namespaceObject.reactive)([])
  const source = (0, external_Vue_namespaceObject.computed)(
    () =>
      sources.find((_ref) => {
        let { label } = _ref
        return label === resolution.value
      }) || sources[0]
  )

  // indicates whether the sources belong to current page
  const hasFreshSources = (0, external_Vue_namespaceObject.ref)(false)

  const filename = (0, external_Vue_namespaceObject.computed)(() => {
    try {
      if (!source.value) throw "Please open a video"

      return resolveFilename(filenameTemplate.value, source.value)
    } catch (e) {
      return `Unable to resolve filename (${formatError(e)})`
    }
  })

  ;(0, external_Vue_namespaceObject.watchEffect)(() =>
    storage.set("preferred_res", resolution.value)
  )
  ;(0, external_Vue_namespaceObject.watchEffect)(() =>
    storage.set("filename", filenameTemplate.value)
  )
  ;(0, external_Vue_namespaceObject.watchEffect)(() =>
    storage.set("auto_down_enabled", autoEnabled.value)
  )
  ;(0, external_Vue_namespaceObject.watchEffect)(() =>
    convertDownloadDropdown(void 0, autoEnabled.value)
  )

  function useDownloaderSettings() {
    return {
      FILENAME_KEYWORDS,
      RESOLUTIONS,
      autoDownEnabled: autoEnabled,
      resolution,
      filenameTemplate,
      filenamePreview: filename,
      illegalCharReplacement,
    }
  }

  page("video", async (pageID, onLeave) => {
    const videoActions = $(".page-video__actions").get(0)

    if (!videoActions) {
      log("Could not find video actions.")
      return
    }

    onLeave(() => {
      // prevent unexpected downloads
      hasFreshSources.value = false
    })

    const $downloadButton = await cancelOnLeave(
      onLeave,
      until$(() => $(".page-video__actions .downloadButton"))
    )

    updateVideoInfo(videoActions)
    updateSources($downloadButton.closest(".dropdown").get(0))

    if (autoEnabled.value) convertDownloadDropdown($downloadButton.get(0), true)
  })

  function updateVideoInfo(videoActions) {
    try {
      // FIXME: reading the prop by a path is quite unreliable, any improvement?
      const video = getReactEventHandlers(videoActions).children[1].props.video

      videoInfo.id = video.id
      videoInfo.title = video.title
      videoInfo.created = new Date(video.createdAt).getTime()
      videoInfo.author = video.user.name
      videoInfo.size = video.file.size
    } catch (e) {
      log(e)
      videoInfo.error = e + ""
    }
  }

  function updateSources(downloadDropdown) {
    const newSources = $(downloadDropdown)
      .find(".dropdown__content a")
      .map(function () {
        const url = this.href
        const label = this.innerText

        return { url, label }
      })
      .get()

    if (!newSources.length) return

    sources.splice(0, sources.length, ...newSources)

    hasFreshSources.value = true
  }

  function convertDownloadDropdown(downloadButton, enabled) {
    const $button = downloadButton ? $(downloadButton) : $(".downloadButton")
    const $dropdown = $button.closest(".dropdown")

    if (!$dropdown.length) return

    const rawButtonText = $button.text().replace(/\s*\(.*\)/, "")

    if (enabled) {
      var _source$value
      if (!$dropdown.data("converted"))
        $dropdown
          .data("converted", true)
          .on("click", function () {
            download(this)
          })
          .children(".dropdown__content")
          .css("display", "none")

      const resolution =
        null === (_source$value = source.value) || void 0 === _source$value
          ? void 0
          : _source$value.label

      $button.text(rawButtonText + (resolution ? ` (${resolution})` : ""))
    } else {
      $dropdown
        .data("converted", false)
        .off("click")
        .children(".dropdown__content")
        .css("display", "")
      $button.text(rawButtonText)
    }
  }

  function download(downloadDropdown) {
    try {
      if (!hasFreshSources.value) throw new Error("No sources found in current page.")
      if (!source.value) throw new Error("Missing source.")

      const $downloadButton = $(downloadDropdown).find(".downloadButton")

      const filename = resolveFilename(filenameTemplate.value, source.value)

      log("Downloading:", filename, source.value.url, GM_info.downloadMode)

      if ("browser" === GM_info.downloadMode) {
        setDownloadButtonEnabled(false)

        if (false);

        GM_download({
          url: source.value.url,
          name: filename,
          onload: () => downloadEnded("onload"),
          onerror: (e) => downloadEnded("onerror", e),
          ontimeout: () => downloadEnded("ontimeout"),
        })
      } else {
        const a = document.createElement("a")
        a.href = source.value.url
        a.download = filename
        a.click()
      }

      function setDownloadButtonEnabled(enabled) {
        if (enabled) {
          // TODO: properly disable the button
          $(downloadDropdown).css("pointer-events", "")
          $downloadButton.css("background-color", "")
        } else {
          $(downloadDropdown).css("pointer-events", "none")
          $downloadButton.css("background-color", "var(--primary-dark)")
        }
      }

      function downloadEnded(type, e) {
        setDownloadButtonEnabled(true)

        if ("ontimeout" === type) e = { error: "timed_out" }

        if (e && e.error) {
          log(e)
          printDownloadMessage(
            `Download Error (${e.error}): ${(e.details && e.details.current) || "No info"}`
          )
        }
      }
    } catch (e) {
      log(e)
      printDownloadMessage(e + "")
    }
  }

  function resolveFilename(template, source) {
    if (videoInfo.error) throw new Error("Broken video info: " + videoInfo.error)

    const replacements = {
      ID: videoInfo.id,
      TITLE: videoInfo.title,
      RES: source.label,
      AUTHOR: videoInfo.author,
      DATE: formatDate(new Date(), ""),
      DATE_TS: new Date().getTime() + "",
      UP_DATE: formatDate(new Date(videoInfo.created), ""),
      UP_DATE_TS: videoInfo.created + "",
    }

    const wrappedKeywords = FILENAME_KEYWORDS.map((k) => `{${k}}`)
    const regex = new RegExp(`(${wrappedKeywords.join("|")})`, "g")

    const basename = template.replace(regex, (match) => {
      const keyword = match.slice(1, -1)
      const value = replacements[keyword]

      // remove path delimiters in keyword values
      return value.replace(/[/\\]/g, illegalCharReplacement.value)
    })

    const ext = source.url.slice(source.url.lastIndexOf(".")).replace(/[^A-Za-z0-9.]/g, "")

    const filename = basename + ext

    return sanitizePath(filename, illegalCharReplacement.value)
  }

  function printDownloadMessage(msg) {
    $(".page-video__bottom")
      .css("flex-wrap", "wrap")
      .append(`<div style="flex: 100% 0 0">${msg}</div>`)
  }

  const likeRateEnabled = (0, external_Vue_namespaceObject.ref)(storage.get("like_rates"))
  const highlightThreshold = (0, external_Vue_namespaceObject.ref)(
    storage.get("like_rate_highlight")
  )
  const highlightOpacity = (0, external_Vue_namespaceObject.ref)(
    storage.get("like_rate_highlight_opacity")
  )

  const likeRateClass = "enh-like-rate"
  const highlightClass = "enh-highlight"

  ready.then(updateHighlightOpacity)

  ;(0, external_Vue_namespaceObject.watchEffect)(() => {
    storage.set("like_rates", likeRateEnabled.value)

    if (likeRateEnabled.value) {
      document.body.classList.add("enh-show-like-rates")
      $(".videoTeaser, .imageTeaser").each((i, teaser) => processTeaser(teaser))
    } else {
      document.body.classList.remove("enh-show-like-rates")
      $("." + highlightClass).removeClass(highlightClass)
    }
  })

  ;(0, external_Vue_namespaceObject.watchEffect)(() => {
    storage.set("like_rate_highlight", highlightThreshold.value)

    $(".videoTeaser, .imageTeaser").each((i, teaser) => processTeaser(teaser))
  })

  ;(0, external_Vue_namespaceObject.watchEffect)(() => {
    storage.set("like_rate_highlight_opacity", highlightOpacity.value)

    updateHighlightOpacity()
  })

  function useTeaserSettings() {
    return { likeRateEnabled, highlightThreshold, highlightOpacity }
  }

  page(
    ["home", "videoList", "imageList", "subscriptions", "profile", "video", "image"],
    async (pageID, onLeave) => {
      const teaserObserver = new SimpleMutationObserver((mutation) =>
        mutation.addedNodes.forEach(detectColumn)
      )

      onLeave(() => {
        teaserObserver.disconnect()
      })

      const teaserBatcher = new TeaserBatcher()

      if (["home", "profile", "image"].includes(pageID))
        [".videoTeaser", ".imageTeaser"].forEach(async (selector) => {
          const $teasers = await until$(() => $(selector), 200)

          requestProcessTeasers($teasers.toArray())
        })
      else if ("video" === pageID) {
        const selectors = [".moreFromUser", ".moreLikeThis"].flatMap((parentCls) =>
          [".videoTeaser", ".imageTeaser"].map((cls) => `${parentCls} ${cls}`)
        )

        selectors.forEach(async (selector) => {
          const $teasers = await until$(() => $(selector), 200)

          requestProcessTeasers($teasers.toArray())
        })
      } else {
        const $teasers = await until$(
          () => $(".videoTeaser:first-of-type, .imageTeaser:first-of-type"),
          200
        )

        detectRow($teasers.closest(".row")[0])
      }

      function detectRow(row) {
        teaserObserver.observe(row, { childList: true, immediate: true })
      }

      function detectColumn(column) {
        const { firstChild } = column

        if (
          !!firstChild &&
          (hasClass(firstChild, "videoTeaser") || hasClass(firstChild, "imageTeaser"))
        )
          requestProcessTeasers([firstChild])
      }

      function requestProcessTeasers(teasers) {
        teasers.forEach((teaser) => teaserBatcher.add(teaser))
        teaserBatcher.run(processTeaser)
      }
    }
  )

  class TeaserBatcher {
    constructor() {
      this.teasers = []

      this.run = throttle((callback) => {
        let lastError

        try {
          this.teasers.forEach(callback)
        } catch (e) {
          // only record the last error so the console won't blow up
          lastError = e
        }

        if (lastError) log("Failed to process teasers", lastError)

        this.teasers.length = 0
      }, 0)
    }

    add(teaser) {
      this.teasers.push(teaser)
    }
  }

  function processTeaser(teaser) {
    const viewsLabel = $(teaser).find(".views")
    const likesLabel = $(teaser).find(".likes")

    let likePercentage

    const likeRateLabel = viewsLabel.children("." + likeRateClass)

    if (likeRateLabel.length) likePercentage = +likeRateLabel.text().trim().replace("%", "")
    else {
      const views = parseAbbreviatedNumber(viewsLabel.text().trim())
      const likes = parseAbbreviatedNumber(likesLabel.text().trim())

      likePercentage = Math.round((100 * likes) / views)

      const display = Number.isFinite(likePercentage) ? likePercentage + "%" : "/"

      // prettier-ignore
      viewsLabel.children().eq(0).clone().addClass(likeRateClass).text(display).prependTo(viewsLabel)
    }

    if (likePercentage >= highlightThreshold.value && likeRateEnabled.value)
      teaser.classList.add(highlightClass)
    else teaser.classList.remove(highlightClass)
  }

  function updateHighlightOpacity() {
    const color = getComputedStyle(document.body).getPropertyValue("--primary").trim()

    // color will be empty before the page is fully loaded
    if (color)
      document.body.style.setProperty("--ehg-hl-bg", adjustAlpha(color, highlightOpacity.value))
  }

  const widenContentEnabled = (0, external_Vue_namespaceObject.ref)(storage.get("widen_content"))
  const widenContentScale = (0, external_Vue_namespaceObject.ref)(
    storage.get("widen_content_scale")
  )

  ;(0, external_Vue_namespaceObject.watch)(widenContentEnabled, (enabled) =>
    storage.set("widen_content", enabled)
  )
  ;(0, external_Vue_namespaceObject.watch)(widenContentScale, (scale) =>
    storage.set("widen_content_scale", scale)
  )

  function useWidenContentSettings() {
    return { widenContentEnabled, widenContentScale }
  }

  page(["video", "image"], (pageID, onLeave) => {
    const mediaArea = $(".page-video__player, .page-video__slideshow").get(0)

    if (!mediaArea) {
      log(`${"video" === pageID ? "video" : "slideshow"} area not found.`)
      return
    }

    const sidebar = $(".page-video__sidebar").get(0)

    if (!sidebar) {
      log("sidebar not found.")
      return
    }

    const col = $(mediaArea).closest(".col-12").get(0)
    const row = $(mediaArea).closest(".row").get(0)
    const container = $(row).closest(".content").get(0)

    onLeave((0, external_Vue_namespaceObject.watchEffect)(() => updateResize()))

    function updateResize(entries) {
      if (widenContentEnabled.value) {
        let containerWidth = 0
        let rowWidth = 0
        let colWidth = 0
        let mediaHeight = 0

        if (entries) {
          for (const entry of entries)
            if (entry.target === mediaArea) mediaHeight = entry.contentRect.height
            else if (entry.target === col) colWidth = entry.contentRect.width
            else if (entry.target === row) rowWidth = entry.contentRect.width
            else if (entry.target === container) containerWidth = entry.contentRect.width
        } else {
          containerWidth = container.offsetWidth
          rowWidth = row.offsetWidth
          colWidth = col.offsetWidth
          mediaHeight = mediaArea.offsetHeight
        }

        if (containerWidth > 0 && rowWidth > 0 && colWidth > 0) {
          const scale = widenContentScale.value / 100
          const mediaWidth = Math.min(rowWidth * scale, containerWidth)

          mediaArea.style.marginLeft = `${(rowWidth - mediaWidth) / 2}px`
          mediaArea.style.marginRight = `${(rowWidth - mediaWidth) / 2 - (rowWidth - colWidth)}px`
        }

        if (mediaHeight > 0) sidebar.style.marginTop = `${mediaArea.offsetTop + mediaHeight}px`
      } else {
        mediaArea.style.marginLeft = ""
        mediaArea.style.marginRight = ""
        sidebar.style.marginTop = ""
      }
    }

    const observer = new ResizeObserver(updateResize)

    observer.observe(mediaArea)
    observer.observe(row)
    observer.observe(container)

    onLeave(() => {
      observer.disconnect()
    })
  })

  // extracted by mini-css-extract-plugin
  /* harmony default export */ const Settings_module = {
    switch: "Settings-module__switch--qcsG",
    settings: "Settings-module__settings--alpJ",
    active: "Settings-module__active--iMRv",
    disabled: "Settings-module__disabled--vvjv",
    header: "Settings-module__header--s2Rw",
    title: "Settings-module__title--aDU_",
    view: "Settings-module__view--dY2E",
    sectionHeader: "Settings-module__section-header--Xy_I",
    fieldLabel: "Settings-module__field-label--O5EA",
    labelBlock: "Settings-module__label-block--EYVa",
    labelInline: "Settings-module__label-inline--v3DK",
    panel: "Settings-module__panel--PuCY",
    warn: "Settings-module__warn--KbCV",
  }

  // recommended vscode plugin for syntax highlighting: https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html
  // language=HTML
  const template = /* html */ `
    <div class='text text--text text--bold'>E</div>



    <div v-if='visible' :class='css.settings' @click.stop>
        <header :class='css.header'>
            <h2 :class='css.title'>{{ $t('name') }} v${GM_info.script.version}</h2>
        </header>
        <nav>
            <ul>
                <li
                    v-for='tab, i in tabs'
                    :class='{ [css.active]: i === tabIndex }'
                    @click='tabIndex = i'
                >
                    {{ $t(tab.name) }}
                </li>
            </ul>
        </nav>
        <div v-if='tabVal === "ui"' :class='css.view'>
            <h2 :class='css.sectionHeader'>{{ $t('s.ui.label') }}</h2>



            <h3 :class='css.fieldLabel'>{{ $t('s.ui.like_rate.label') }}</h3>
            <p v-html='$t("s.ui.like_rate.desc")'></p>
            <p>
                <label :class='css.labelBlock'>
                    {{ $t('s.enabled') }}
                    <input type='checkbox' v-model='likeRateEnabled'>
                </label>
            </p>



            <h3 :class='css.fieldLabel'>{{ $t('s.ui.highlight_threshold.label') }}</h3>
            <p v-html='$t("s.ui.highlight_threshold.desc")'></p>
            <p>
                <input type='number' step='0.1' min='0' max='100' v-model='highlightThreshold'>
            </p>
            <h3 :class='css.fieldLabel'>{{ $t('s.ui.highlight_bg.label') }}</h3>
            <p>
                <input type="range" min="0" max="1" step="0.01" v-model='highlightOpacity'>
            </p>



            <h3 :class='css.fieldLabel'>{{ $t('s.ui.widen_content.label') }}</h3>
            <p v-html='$t("s.ui.widen_content.desc")'></p>
            <p>
                <label :class='css.labelBlock'>
                    {{ $t('s.enabled') }}
                    <input type='checkbox' v-model='widenContentEnabled'>
                </label>
            </p>
            <p>
                <label :class='css.labelBlock'>
                    {{ $t('s.ui.widen_content.scale') }}
                    <input type='number' step='1' min='10' max='500' :value='widenContentScale' @change='widenContentScale = Math.round($event.target.value)'>
                </label>
            </p>
        </div>
        <div v-else-if='tabVal === "download"' :class='css.view'>
            <h2 :class='css.sectionHeader'>{{ $t('s.download.label') }}</h2>



            <h3 :class='css.fieldLabel'>{{ $t('s.download.auto.label') }}</h3>
            <p v-html='$t("s.download.auto.desc")'></p>
            <p>
                <label :class='css.labelBlock'>
                    {{ $t('s.enabled') }}
                    <input type='checkbox' v-model='autoDownEnabled'>
                </label>
            </p>



            <h3 :class='css.fieldLabel'>{{ $t('s.download.resolution.label') }}</h3>
            <p>
                <label v-for='res in RESOLUTIONS' :class='css.labelInline'>
                    <input type='radio' name='res' :value='res' v-model='resolution'>
                    {{ res }}
                </label>
            </p>



            <h3 :class='css.fieldLabel'>{{ $t('s.download.filename.label') }}</h3>
            <p v-if='!downloadMode' v-html='$t("s.download.filename.warn")'></p>
            <div v-else-if='downloadMode !== "browser"' :class='[css.panel, css.warn]'>
                <p v-html='$t("s.download.filename.warn_tm.desc")'></p>
                <ol v-if='$tm("s.download.filename.warn_tm.steps").length'>
                    <li v-for='step in $tm("s.download.filename.warn_tm.steps")'><p v-html='step'></p></li>
                </ol>
            </div>



            <p v-html='$t("s.download.filename.desc")'></p>



            <div :class='css.keywords'>
                <table :class='css.keywordTable'>
                    <tr v-for='kw in FILENAME_KEYWORDS'>
                        <th>{{ kw }}</th>
                        <td>{{ $t('s.download.filename.key.' + kw.toLowerCase()) }}</td>
                    </tr>
                </table>
            </div>
            <details>
                <summary>{{ $t('s.extra') }}</summary>
                <p>
                    {{ $t('s.download.filename.replace_illegal_char') }}
                    <input type='text' v-model='illegalCharReplacement'>
                    {{ '*miku*miku:dance??.mp4 -> ' }} {{ sanitizePath('*miku*miku:dance??.mp4', illegalCharReplacement) }}
                </p>
            </details>
            <input type='text' v-model='filenameTemplate'>
            <p>{{ $t('s.download.filename.preview') + ': ' + filenamePreview }}</p>
            <div :class='css.panel'>
                <p><b>{{ $tm('s.download.filename.tips')[0] }}</b></p>
                <ul>
                    <li v-for='tip in $tm("s.download.filename.tips").slice(1)'><p v-html='tip'></p></li>
                </ul>
            </div>
        </div>
        <div v-if='tabVal === "script"' :class='css.view'>
            <h2 :class='css.sectionHeader'>{{ $t('s.script.label') }}</h2>



            <h3 :class='css.fieldLabel'>{{ $t('s.script.language.label') }}</h3>
            <p>
                <label v-for='loc in $i18n.availableLocales' :class='css.labelInline'>
                    <input type='radio' name='loc' :value='loc' :checked='activeLocale === loc' @change='locale = loc'>
                    {{ $t('language', loc) }}
                </label>
            </p>
        </div>
    </div>
`

  function setup() {
    const tabs = [
      { name: "s.ui.label", val: "ui" },
      { name: "s.download.label", val: "download" },
      { name: "s.script.label", val: "script" },
    ]
    const tabIndex = (0, external_Vue_namespaceObject.ref)(0)
    const tabVal = (0, external_Vue_namespaceObject.computed)(
      () => tabs[tabIndex.value] && tabs[tabIndex.value].val
    )
    const visible = (0, external_Vue_namespaceObject.ref)(false)

    const onClickContainer = () => {
      visible.value = !visible.value

      if (visible.value) onClickOutside(settingsContainer, () => (visible.value = false))
    }

    settingsContainer.addEventListener("click", onClickContainer)

    ;(0, external_Vue_namespaceObject.onBeforeUnmount)(() => {
      settingsContainer.removeEventListener("click", onClickContainer)
    })

    return {
      css: Settings_module,
      tabs,
      tabIndex,
      tabVal,
      visible,
      downloadMode: GM_info.downloadMode,
      sanitizePath,
      ...useDownloaderSettings(),
      ...useConfigSettings(),
      ...useTeaserSettings(),
      ...useWidenContentSettings(),
    }
  }

  const SETTINGS_ID = "enh-settings"

  const settingsContainer = $(
    `<div id="${SETTINGS_ID}" class='header__link ${Settings_module["switch"]}'></div>`
  )[0]

  let app

  page(ALL, (pageID, onLeave) => {
    const destination = $(
      ".page .header__content:first-of-type .dropdown:last-of-type, a[href='/register']"
    )[0]

    if (destination) {
      // destination element will be destroyed everytime the page changes,
      // so we need to insert the container after every page change
      destination.before(settingsContainer)

      // lazy-init the app
      if (!app) {
        app = (0, external_Vue_namespaceObject.createApp)({ template, setup })

        app.use(i18n_i18n)

        if (true)
          // pending fix https://github.com/vuejs/core/pull/5197
          // @ts-ignore
          unsafeWindow.Vue = Vue

        app.mount(settingsContainer)

        log("Settings view initialized")
      }
    } else log("Could not insert settings view: container not found.")
  })

  // prevent Sentry from tracking the logging
  setLogger(console.log.__sentry_original__ || console.log)

  const patchedFlag = "__enhPatched"

  page(["video"], async (pageID, onLeave) => {
    const timerPromise = until(() => {
      const player = getPlayer()

      if (player) {
        fixResolution(player)

        if (!(patchedFlag in player)) {
          player[patchedFlag] = true
          preventVolumeScrolling(player)
        }
      }
    }, 500)

    onLeave(() => timerPromise.cancel())

    function getPlayer() {
      var _$$get
      return null === (_$$get = $(".page-video__player .video-js").get(0)) || void 0 === _$$get
        ? void 0
        : _$$get.player
    }

    function preventVolumeScrolling(player) {
      const originalGet = WeakMap.prototype.get

      // hook WeakMap.get() to get the event data
      // https://github.com/videojs/video.js/blob/2b0df25df332dceaab375327887f0721ca8d21d0/src/js/utils/events.js#L271
      WeakMap.prototype.get = function (key) {
        const value = originalGet.call(this, key)

        try {
          var _data$handlers
          const data = value

          if (
            null !== data &&
            void 0 !== data &&
            null !== (_data$handlers = data.handlers) &&
            void 0 !== _data$handlers &&
            _data$handlers.mousewheel
          ) {
            log(`removing ${data.handlers.mousewheel.length} mousewheel handler(s) from Player`)

            // the listeners are bound functions and cannot be checked with toString(),
            // so we have to remove all mousewheel handlers
            delete data.handlers.mousewheel
          }
        } catch (e) {
          log("error:", e)
        } finally {
          return value
        }
      }

      // trigger the hook by adding an arbitrary event listener
      player.on("__dummy", () => {})
      player.off("__dummy")

      WeakMap.prototype.get = originalGet

      const originalOn = player.on

      // prevent adding new mousewheel listeners
      player.on = function (targetOrType) {
        if ("mousewheel" === targetOrType) {
          log("prevented adding mousewheel listener")
          return
        }

        for (
          var _len = arguments.length, rest = new Array(_len > 1 ? _len - 1 : 0), _key = 1;
          _key < _len;
          _key++
        )
          rest[_key - 1] = arguments[_key]
        return originalOn.call(this, targetOrType, ...rest)
      }
    }

    function fixResolution(player) {
      const targetSource = getTargetSource(player)

      if (targetSource && targetSource.src !== player.src()) {
        log(`setting resolution to ${targetSource.name}: ${targetSource.src}`)
        player.src(targetSource)
      }
    }

    function getTargetSource(player) {
      const sources = player.currentSources()

      if (!sources.length) return

      const selectedResName = localStorage.getItem("player-resolution")
      const source = sources.find((s) => s.name === selectedResName)

      if (source) return source

      log(`error: source not found for ${selectedResName}`)

      return
    }
  })

  // this "bg" is covering the video player and preventing player from entering fullscreen mode by double-clicks
  GM_addStyle(`
.videoPlayer__bg {
  pointer-events: none;
}
`)

  const state = (0, external_Vue_namespaceObject.reactive)({ theme: "light" })

  setInterval(() => {
    state.theme = localStorage.theme
  }, 1e3)

  ;(0, external_Vue_namespaceObject.watchEffect)(updateTheme)

  function updateTheme() {
    const theme = state.theme
    const adjustmentSign = "light" === theme ? -1 : 1
    const bodyColor = getComputedStyle(document.body).getPropertyValue("--body")

    document.body.style.setProperty(
      "--enh-body-focus",
      adjustHexColor(bodyColor, 15 * adjustmentSign)
    )
    document.body.style.setProperty(
      "--enh-body-highlight",
      adjustHexColor(bodyColor, 30 * adjustmentSign)
    )

    const darkClass = "enh-dark"

    if ("dark" === theme) document.body.classList.add(darkClass)
    else document.body.classList.remove(darkClass)
  }

  async function main() {
    document.body.classList.add("enh-body")

    setupPaging()
  }

  main()
})()

GM_addStyle(`
.Settings-module__switch--qcsG {
  cursor: pointer;
}

.Settings-module__settings--alpJ {
  position: absolute;
  z-index: 1000;
  top: 100%;
  right: 0;
  width: 400px;
  max-height: calc(100vh - 65px);
  overflow: auto;
  background: var(--body);
  font-size: 14px;
  border: 2px solid var(--primary);
  border-top: none;
  cursor: default;
}

.Settings-module__settings--alpJ nav {
    padding: 0 16px;
    border-bottom: 1px solid var(--enh-body-highlight);
  }

.Settings-module__settings--alpJ nav ul {
      margin: 0;
      padding: 0;
      display: flex;
      flex-wrap: wrap;
    }

.Settings-module__settings--alpJ nav li {
      padding: 8px 16px;
      list-style-type: none;
      cursor: pointer;
    }

.Settings-module__settings--alpJ nav li:hover {
        background: var(--enh-body-focus);
      }

.Settings-module__settings--alpJ nav li.Settings-module__active--iMRv {
        background: var(--enh-body-highlight);
      }

.Settings-module__settings--alpJ details {
    border: 1px solid var(--enh-body-highlight);
  }

.Settings-module__settings--alpJ details > * {
      padding: 0 8px;
    }

.Settings-module__settings--alpJ p,
  .Settings-module__settings--alpJ summary {
    color: var(--muted);
  }

.Settings-module__settings--alpJ p {
    margin-top: 0;
    margin-bottom: 8px;
  }

.Settings-module__settings--alpJ a {
    font-weight: bold;
    cursor: pointer;
  }

.Settings-module__settings--alpJ ol,
  .Settings-module__settings--alpJ ul {
    padding-left: 20px;
  }

.Settings-module__settings--alpJ table {
    margin: 8px 0;
    width: 100%;
    background: var(--enh-body-focus);
    border: 1px solid var(--enh-body-highlight);
    border-collapse: collapse;
  }

.Settings-module__settings--alpJ th {
    text-align: right;
  }

.Settings-module__settings--alpJ th,
  .Settings-module__settings--alpJ td {
    padding: 4px 8px;
    border: 1px solid var(--enh-body-highlight);
  }

.Settings-module__settings--alpJ label,
  .Settings-module__settings--alpJ summary {
    cursor: pointer;
  }

.Settings-module__settings--alpJ label:hover, .Settings-module__settings--alpJ summary:hover {
      background: var(--enh-body-focus);
    }

.Settings-module__settings--alpJ label input, .Settings-module__settings--alpJ summary input {
      cursor: pointer;
    }

.Settings-module__settings--alpJ label.Settings-module__disabled--vvjv, .Settings-module__settings--alpJ summary.Settings-module__disabled--vvjv {
      cursor: not-allowed;
    }

.Settings-module__settings--alpJ label.Settings-module__disabled--vvjv input, .Settings-module__settings--alpJ summary.Settings-module__disabled--vvjv input {
        cursor: not-allowed;
      }

.Settings-module__settings--alpJ input[type="text"] {
    outline: none !important;
  }

.Settings-module__settings--alpJ input[type="text"] {
    margin: 8px 0;
    width: 100%;
    padding: 8px;
    background: var(--enh-body-focus);
    color: var(--text);
    border: 2px solid var(--enh-body-highlight);
    border-radius: 3px;
  }

.Settings-module__settings--alpJ input[type="text"]:hover,
    .Settings-module__settings--alpJ input[type="text"]:focus {
      background: var(--enh-body-highlight);
    }

.Settings-module__header--s2Rw {
  padding: 0 16px;
}

.Settings-module__title--aDU_ {
  margin-top: 4px;
}

.Settings-module__view--dY2E {
  padding: 16px;
}

.Settings-module__section-header--Xy_I {
  margin-bottom: 16px;
}

.Settings-module__field-label--O5EA {
  position: relative;
  margin: 16px 0;
  padding-top: 16px;
}

.Settings-module__field-label--O5EA:not(:first-of-type) {
    border-top: 1px solid var(--enh-body-highlight);
  }

.Settings-module__label-block--EYVa {
  display: flex;
  padding: 8px 8px 8px 0;
}

.Settings-module__label-block--EYVa input {
    margin-left: auto;
  }

.Settings-module__label-inline--v3DK {
  display: inline-flex;
  padding: 8px 8px 8px 0;
}

.Settings-module__label-inline--v3DK:not(:first-child) {
    padding-left: 8px;
  }

.Settings-module__label-inline--v3DK:not(:last-child) {
    margin-right: 8px;
  }

.Settings-module__panel--PuCY {
  margin-bottom: 8px;
  padding: 8px;
  background: var(--enh-body-focus);
}

.Settings-module__warn--KbCV {
  background-color: #e9db89;
}

.enh-dark .Settings-module__warn--KbCV {
    background-color: #594c00;
  }

.enh-body {
  --ehg-hl-bg: rbga(0, 0, 0, 0);
}

#enh-settings {
  position: relative;
}

#enh-settings * {
    box-sizing: border-box;
  }

.enh-like-rate {
  display: none;
}

.enh-show-like-rates .videoTeaser .views, .enh-show-like-rates .imageTeaser .views {
    }

.enh-show-like-rates .videoTeaser .enh-like-rate, .enh-show-like-rates .imageTeaser .enh-like-rate {
      display: block;
    }

.enh-show-like-rates .videoTeaser .enh-like-rate + .text, .enh-show-like-rates .imageTeaser .enh-like-rate + .text {
        display: none;
      }

.enh-show-like-rates .page-start__subscriptions,
  .enh-show-like-rates .page-start__videos,
  .enh-show-like-rates .page-start__images {
    position: relative;
    z-index: 0;
  }

/* for all the affected pages, check out process-teaser.ts */
.enh-highlight:before {
    content: "";
    position: absolute;
    z-index: -1;
    top: -8px;
    bottom: 7px;
    left: 7px;
    right: 7px;
    background: var(--ehg-hl-bg);
  }
.page-video .enh-highlight:before,
    .page-image .enh-highlight:before {
      content: none;
    }
.page-profile .enh-highlight,
  .page-subscriptions .enh-highlight {
    position: relative;
  }
.page-profile .enh-highlight:before, .page-subscriptions .enh-highlight:before {
      top: -6px;
      bottom: -6px;
      left: -6px;
      right: -6px;
    }
.page-video .enh-highlight,
  .page-image .enh-highlight {
    background: var(--ehg-hl-bg);
  }

`)