Sleazy Fork is available in English.

Iwara Enhancement

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

  1. // ==UserScript==
  2. // @name Iwara Enhancement
  3. // @name:zh-CN Iwara增强
  4. // @description Please refer to the script's homepage for more information.
  5. // @description:zh-CN 请参考脚本的主页以获取更多信息
  6. // @noframes
  7. // @grant unsafeWindow
  8. // @grant GM_addStyle
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_download
  12. // @grant GM_info
  13. // @grant GM_addStyle
  14. // @require https://unpkg.com/jquery@3.6.0
  15. // @require https://unpkg.com/vue@3.2.20
  16. // @require https://unpkg.com/vue-i18n@9.2.0-beta.26
  17. // @match *://*.iwara.tv/*
  18. // @namespace https://github.com/guansss/userscripts
  19. // @version 1.5
  20. // @author guansss
  21. // @source https://github.com/guansss/userscripts
  22. // @supportURL https://github.com/guansss/userscripts/issues
  23. // ==/UserScript==
  24. ;(() => {
  25. "use strict"
  26.  
  27. $
  28.  
  29. const external_Vue_namespaceObject = Vue
  30.  
  31. function onClickOutside(el, callback) {
  32. document.addEventListener("click", handler)
  33.  
  34. function handler(e) {
  35. if (!el.contains(e.target)) {
  36. document.removeEventListener("click", handler)
  37. callback(e)
  38. }
  39. }
  40. }
  41.  
  42. /**
  43. * MutationObserver that calls callback with just a single mutation.
  44. */
  45. class SimpleMutationObserver extends MutationObserver {
  46. // since calling `new NodeList()` is illegal, this is the only way to create an empty NodeList
  47.  
  48. constructor(callback) {
  49. super((mutations) => {
  50. for (const mutation of mutations) if (this.callback(mutation)) break
  51. })
  52. this.callback = callback
  53. }
  54.  
  55. /**
  56. * @param options.immediate - When observing "childList", immediately trigger a mutation with existing nodes.
  57. */
  58. observe(target, options) {
  59. super.observe(target, options)
  60.  
  61. if (options && options.immediate && options.childList && target.childNodes.length)
  62. this.callback({
  63. target,
  64. type: "childList",
  65. addedNodes: target.childNodes,
  66. removedNodes: SimpleMutationObserver.emptyNodeList,
  67. })
  68. }
  69. }
  70.  
  71. SimpleMutationObserver.emptyNodeList = document.querySelectorAll("#__absolutely_nonexisting")
  72. function hasClass(node, className) {
  73. var _classList
  74. return !!(
  75. null !== (_classList = node.classList) &&
  76. void 0 !== _classList &&
  77. _classList.contains(className)
  78. )
  79. }
  80.  
  81. let log
  82.  
  83. setLogger(console.log)
  84.  
  85. function setLogger(logger) {
  86. log = logger.bind(console, `[${GM_info.script.name}]`)
  87. }
  88.  
  89. function clamp(val, min, max) {
  90. return val < min ? min : val > max ? max : val
  91. }
  92.  
  93. function parseAbbreviatedNumber(str) {
  94. const units = { k: 1e3, m: 1e6, b: 1e9 }
  95.  
  96. let number = parseFloat(str)
  97.  
  98. if (!isNaN(number)) {
  99. const unit = str.trim().slice(-1).toLowerCase()
  100.  
  101. return number * (units[unit] || 1)
  102. }
  103.  
  104. return NaN
  105. }
  106.  
  107. function formatDate(date) {
  108. let delimiter = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : "/"
  109. return [
  110. date.getFullYear(),
  111. date.getMonth() + 1,
  112. date.getDate(),
  113. date.getHours(),
  114. date.getMinutes(),
  115. date.getSeconds(),
  116. ]
  117. .map((num) => String(num).padStart(2, "0"))
  118. .join(delimiter)
  119. }
  120.  
  121. function formatError(e) {
  122. let fallback = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : "Unknown error"
  123. if ("string" === typeof e) return e || fallback
  124.  
  125. if (null !== e && "object" === typeof e) {
  126. if (e instanceof Event && "error" === e.type) return "Failed to load resource"
  127.  
  128. if (e.message) return e.message
  129.  
  130. const str = String(e)
  131. return "[object Object]" === str ? fallback : str
  132. }
  133.  
  134. return fallback
  135. }
  136.  
  137. function adjustHexColor(color, amount) {
  138. return color.replace(/\w\w/g, (color) =>
  139. clamp(parseInt(color, 16) + amount, 0, 255)
  140. .toString(16)
  141. .padStart(2, "0")
  142. )
  143. }
  144.  
  145. // written by ChatGPT
  146. function adjustAlpha(color, alpha) {
  147. if (alpha < 0 || alpha > 1) throw new Error("Alpha value must be between 0 and 1")
  148.  
  149. let r, g, b
  150.  
  151. if (color.startsWith("#")) {
  152. if (7 !== color.length) throw new Error("Invalid color format")
  153.  
  154. r = parseInt(color.slice(1, 3), 16)
  155. g = parseInt(color.slice(3, 5), 16)
  156. b = parseInt(color.slice(5, 7), 16)
  157. } else if (color.startsWith("rgb")) {
  158. const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/)
  159.  
  160. if (!match) throw new Error("Invalid color format")
  161.  
  162. r = parseInt(match[1], 10)
  163. g = parseInt(match[2], 10)
  164. b = parseInt(match[3], 10)
  165. } else throw new Error("Invalid color format")
  166.  
  167. return `rgba(${r}, ${g}, ${b}, ${alpha})`
  168. }
  169.  
  170. /**
  171. * Replaces characters that are forbidden in file systems.
  172. */
  173. function sanitizePath(path, illegalCharReplacement) {
  174. let keepDelimiters = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : true
  175. path = path.replace(/[*:<>?|]/g, illegalCharReplacement)
  176.  
  177. if (!keepDelimiters) path = path.replace(/[\\/]/g, illegalCharReplacement)
  178.  
  179. return path
  180. }
  181.  
  182. const external_VueI18n_namespaceObject = VueI18n
  183.  
  184. const en_namespaceObject = JSON.parse(
  185. '{"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"}}}}'
  186. )
  187.  
  188. const zh_namespaceObject = JSON.parse(
  189. '{"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":"语言"}}}}'
  190. )
  191.  
  192. /* harmony default export */ const i18n = { zh: zh_namespaceObject, en: en_namespaceObject }
  193.  
  194. function createStorage(prefix, schema) {
  195. if (prefix) prefix += "."
  196.  
  197. return {
  198. get(key) {
  199. return GM_getValue(prefix + key, schema[key])
  200. },
  201. set(key, val) {
  202. if ("function" === typeof val) val = val(this.get(key))
  203.  
  204. GM_setValue(prefix + key, val)
  205. },
  206. }
  207. }
  208.  
  209. const storage = createStorage("", {
  210. v: GM_info.script.version,
  211. locale: navigator.language,
  212. volume: 0.5,
  213. auto_down_enabled: true,
  214. preferred_res: "Source",
  215. filename: "{DATE} {TITLE} - {AUTHOR} ({ID})",
  216. illegal_char_replacement: "_",
  217. dark: false,
  218. like_rates: true,
  219. like_rate_highlight: 4,
  220. like_rate_highlight_opacity: 0.2,
  221. widen_content: true,
  222. widen_content_scale: 100,
  223. })
  224.  
  225. const i18n_i18n = (0, external_VueI18n_namespaceObject.createI18n)({
  226. locale: storage.get("locale"),
  227. fallbackLocale: "en",
  228. messages: i18n,
  229.  
  230. // disable warnings - I know what I'm doing!!
  231. silentFallbackWarn: true,
  232. silentTranslationWarn: true,
  233. warnHtmlInMessage: "off",
  234. })
  235.  
  236. function matchLocale(locale) {
  237. return i18n_i18n.global.availableLocales.includes(locale)
  238. ? locale
  239. : i18n_i18n.global.availableLocales.find((loc) => locale.startsWith(loc)) || "en"
  240. }
  241.  
  242. const locale = (0, external_Vue_namespaceObject.ref)(storage.get("locale"))
  243.  
  244. ;(0, external_Vue_namespaceObject.watchEffect)(() => {
  245. i18n_i18n.global.locale = locale.value
  246.  
  247. storage.set("locale", locale.value)
  248. })
  249.  
  250. function useConfigSettings() {
  251. // locale that will actually be used, with fallback applied
  252. const activeLocale = (0, external_Vue_namespaceObject.computed)(() => matchLocale(locale.value))
  253.  
  254. return { locale, activeLocale }
  255. }
  256.  
  257. /* harmony default export */ function mitt(n) {
  258. return {
  259. all: (n = n || new Map()),
  260. on: function (t, e) {
  261. var i = n.get(t)
  262. i ? i.push(e) : n.set(t, [e])
  263. },
  264. off: function (t, e) {
  265. var i = n.get(t)
  266. i && (e ? i.splice(i.indexOf(e) >>> 0, 1) : n.set(t, []))
  267. },
  268. emit: function (t, e) {
  269. var i = n.get(t)
  270. i &&
  271. i.slice().map(function (n) {
  272. n(e)
  273. }),
  274. (i = n.get("*")) &&
  275. i.slice().map(function (n) {
  276. n(t, e)
  277. })
  278. },
  279. }
  280. }
  281.  
  282. const ready = new Promise((resolve) => {
  283. if ("loading" === document.readyState)
  284. document.addEventListener("DOMContentLoaded", () => resolve())
  285. else resolve()
  286. })
  287.  
  288. function once(emitter, event, listener) {
  289. const fn = (data) => {
  290. emitter.off(event, fn)
  291. listener(data)
  292. }
  293.  
  294. emitter.on(event, fn)
  295.  
  296. return fn
  297. }
  298.  
  299. function setupPaging() {
  300. ready.then(() => {
  301. const appDiv = document.getElementById("app")
  302.  
  303. if (!appDiv) {
  304. log("Missing app div.")
  305. return
  306. }
  307.  
  308. log("Start observing pages.")
  309.  
  310. const appObserver = new SimpleMutationObserver((mutation) => {
  311. detectPageChange(appDiv, mutation.addedNodes, "pageEnter")
  312. detectPageChange(appDiv, mutation.removedNodes, "pageLeave")
  313. })
  314. appObserver.observe(appDiv, { childList: true, immediate: true })
  315. })
  316. }
  317.  
  318. const emitter = mitt()
  319.  
  320. let currentClassName = ""
  321.  
  322. emitter.on("pageEnter", (className) => (currentClassName = className))
  323.  
  324. const ALL = "*"
  325.  
  326. // page listener for iwara
  327. function page(id, enter) {
  328. const match = (() => {
  329. if (id === ALL) return () => id
  330.  
  331. const ids = "string" === typeof id ? [id] : id
  332. const classes = ids.map((id) => `page-${id}`)
  333.  
  334. return (className) => {
  335. const split = className.split(" ")
  336. const index = classes.findIndex((cls) => split.includes(cls))
  337.  
  338. return ids[index]
  339. }
  340. })()
  341.  
  342. function callIfMatch(listener) {
  343. return (className) => {
  344. const matchedID = match(className)
  345.  
  346. if (void 0 !== matchedID)
  347. try {
  348. listener(matchedID)
  349. } catch (e) {
  350. log("Error executing page listener", e)
  351. }
  352. }
  353. }
  354.  
  355. const onPageEnter = callIfMatch((matchedID) => {
  356. enter(matchedID, (onLeave) => {
  357. once(emitter, "pageLeave", callIfMatch(onLeave))
  358. })
  359. })
  360.  
  361. emitter.on("pageEnter", onPageEnter)
  362. }
  363.  
  364. function detectPageChange(appDiv, nodes, event) {
  365. if (nodes.length)
  366. // a valid class name will be like "page page-videoList", where "videoList" is the ID
  367. for (const node of nodes)
  368. if (hasClass(node, "page")) {
  369. // sometimes there are two (maybe more) "page" elements, and one of them contains only the "page" class,
  370. // we ignore it in this case
  371. const hasOtherPageElements =
  372. $(appDiv)
  373. .children(".page")
  374. .filter((_, e) => e !== node && !node.className.includes("page-")).length > 0
  375.  
  376. if (!hasOtherPageElements) emitter.emit(event, node.className)
  377. break
  378. }
  379. }
  380.  
  381. function cancelOnLeave(onLeave, promise) {
  382. onLeave(() => promise.cancel())
  383. return promise
  384. }
  385. let reactEventHandlersKey = ""
  386.  
  387. function getReactEventHandlers(element) {
  388. if (reactEventHandlersKey) return element[reactEventHandlersKey]
  389.  
  390. for (const key of Object.keys(element))
  391. if (key.startsWith("__reactEventHandlers$")) {
  392. reactEventHandlersKey = key
  393. return element[key]
  394. }
  395. }
  396.  
  397. // sometimes I just don't want the script to depend on Lodash...
  398. function throttle(fn, timeout) {
  399. let timer = 0
  400.  
  401. return function () {
  402. for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++)
  403. args[_key] = arguments[_key]
  404. if (timer) return
  405.  
  406. timer = setTimeout(() => {
  407. fn.apply(null, args)
  408.  
  409. timer = 0
  410. }, timeout)
  411. }
  412. }
  413.  
  414. /**
  415. * Periodically calls given function until the return value is truthy.
  416. * @returns A CancelablePromise that resolves with the function's return value when truthy.
  417. */
  418. function until(fn) {
  419. let interval = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0
  420. let cancelOnReload = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : true
  421. let cancelled = false
  422.  
  423. if (cancelOnReload);
  424.  
  425. const STOP = Symbol()
  426.  
  427. const promise = new Promise((resolve, reject) => {
  428. const run = () => {
  429. if (cancelled) return STOP
  430.  
  431. const result = fn()
  432.  
  433. if (result) {
  434. resolve(result)
  435. return STOP
  436. }
  437. }
  438.  
  439. const timerId = setInterval(() => {
  440. try {
  441. if (run() === STOP) clearInterval(timerId)
  442. } catch (e) {
  443. reject(e)
  444. clearInterval(timerId)
  445. }
  446. }, interval)
  447. })
  448. promise.cancel = () => (cancelled = true)
  449.  
  450. return promise
  451. }
  452.  
  453. /**
  454. * Periodically calls given function until the returned jQuery object is not empty.
  455. * @returns A CancelablePromise that resolves with the jQuery object.
  456. */
  457. function until$(fn) {
  458. let interval = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0
  459. let cancelOnReload = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : true
  460. return until(
  461. () => {
  462. const result = fn()
  463.  
  464. if (result.length) return result
  465. },
  466. interval,
  467. cancelOnReload
  468. )
  469. }
  470.  
  471. // a partial structure of the video data defined in iwara's video page,
  472. // including only the properties we need
  473.  
  474. const FILENAME_KEYWORDS = [
  475. "ID",
  476. "TITLE",
  477. "RES",
  478. "AUTHOR",
  479. "DATE",
  480. "UP_DATE",
  481. "DATE_TS",
  482. "UP_DATE_TS",
  483. ]
  484. const RESOLUTIONS = ["Source", "540p", "360p"]
  485.  
  486. const autoEnabled = (0, external_Vue_namespaceObject.ref)(storage.get("auto_down_enabled"))
  487. const filenameTemplate = (0, external_Vue_namespaceObject.ref)(storage.get("filename"))
  488. const illegalCharReplacement = (0, external_Vue_namespaceObject.ref)(
  489. storage.get("illegal_char_replacement")
  490. )
  491. const resolution = (0, external_Vue_namespaceObject.ref)(storage.get("preferred_res"))
  492.  
  493. const videoInfo = (0, external_Vue_namespaceObject.reactive)({
  494. id: "",
  495. title: "",
  496. author: "",
  497. created: 0,
  498. size: 0,
  499. error: "",
  500. })
  501. const sources = (0, external_Vue_namespaceObject.reactive)([])
  502. const source = (0, external_Vue_namespaceObject.computed)(
  503. () =>
  504. sources.find((_ref) => {
  505. let { label } = _ref
  506. return label === resolution.value
  507. }) || sources[0]
  508. )
  509.  
  510. // indicates whether the sources belong to current page
  511. const hasFreshSources = (0, external_Vue_namespaceObject.ref)(false)
  512.  
  513. const filename = (0, external_Vue_namespaceObject.computed)(() => {
  514. try {
  515. if (!source.value) throw "Please open a video"
  516.  
  517. return resolveFilename(filenameTemplate.value, source.value)
  518. } catch (e) {
  519. return `Unable to resolve filename (${formatError(e)})`
  520. }
  521. })
  522.  
  523. ;(0, external_Vue_namespaceObject.watchEffect)(() =>
  524. storage.set("preferred_res", resolution.value)
  525. )
  526. ;(0, external_Vue_namespaceObject.watchEffect)(() =>
  527. storage.set("filename", filenameTemplate.value)
  528. )
  529. ;(0, external_Vue_namespaceObject.watchEffect)(() =>
  530. storage.set("auto_down_enabled", autoEnabled.value)
  531. )
  532. ;(0, external_Vue_namespaceObject.watchEffect)(() =>
  533. convertDownloadDropdown(void 0, autoEnabled.value)
  534. )
  535.  
  536. function useDownloaderSettings() {
  537. return {
  538. FILENAME_KEYWORDS,
  539. RESOLUTIONS,
  540. autoDownEnabled: autoEnabled,
  541. resolution,
  542. filenameTemplate,
  543. filenamePreview: filename,
  544. illegalCharReplacement,
  545. }
  546. }
  547.  
  548. page("video", async (pageID, onLeave) => {
  549. const videoActions = $(".page-video__actions").get(0)
  550.  
  551. if (!videoActions) {
  552. log("Could not find video actions.")
  553. return
  554. }
  555.  
  556. onLeave(() => {
  557. // prevent unexpected downloads
  558. hasFreshSources.value = false
  559. })
  560.  
  561. const $downloadButton = await cancelOnLeave(
  562. onLeave,
  563. until$(() => $(".page-video__actions .downloadButton"))
  564. )
  565.  
  566. updateVideoInfo(videoActions)
  567. updateSources($downloadButton.closest(".dropdown").get(0))
  568.  
  569. if (autoEnabled.value) convertDownloadDropdown($downloadButton.get(0), true)
  570. })
  571.  
  572. function updateVideoInfo(videoActions) {
  573. try {
  574. // FIXME: reading the prop by a path is quite unreliable, any improvement?
  575. const video = getReactEventHandlers(videoActions).children[1].props.video
  576.  
  577. videoInfo.id = video.id
  578. videoInfo.title = video.title
  579. videoInfo.created = new Date(video.createdAt).getTime()
  580. videoInfo.author = video.user.name
  581. videoInfo.size = video.file.size
  582. } catch (e) {
  583. log(e)
  584. videoInfo.error = e + ""
  585. }
  586. }
  587.  
  588. function updateSources(downloadDropdown) {
  589. const newSources = $(downloadDropdown)
  590. .find(".dropdown__content a")
  591. .map(function () {
  592. const url = this.href
  593. const label = this.innerText
  594.  
  595. return { url, label }
  596. })
  597. .get()
  598.  
  599. if (!newSources.length) return
  600.  
  601. sources.splice(0, sources.length, ...newSources)
  602.  
  603. hasFreshSources.value = true
  604. }
  605.  
  606. function convertDownloadDropdown(downloadButton, enabled) {
  607. const $button = downloadButton ? $(downloadButton) : $(".downloadButton")
  608. const $dropdown = $button.closest(".dropdown")
  609.  
  610. if (!$dropdown.length) return
  611.  
  612. const rawButtonText = $button.text().replace(/\s*\(.*\)/, "")
  613.  
  614. if (enabled) {
  615. var _source$value
  616. if (!$dropdown.data("converted"))
  617. $dropdown
  618. .data("converted", true)
  619. .on("click", function () {
  620. download(this)
  621. })
  622. .children(".dropdown__content")
  623. .css("display", "none")
  624.  
  625. const resolution =
  626. null === (_source$value = source.value) || void 0 === _source$value
  627. ? void 0
  628. : _source$value.label
  629.  
  630. $button.text(rawButtonText + (resolution ? ` (${resolution})` : ""))
  631. } else {
  632. $dropdown
  633. .data("converted", false)
  634. .off("click")
  635. .children(".dropdown__content")
  636. .css("display", "")
  637. $button.text(rawButtonText)
  638. }
  639. }
  640.  
  641. function download(downloadDropdown) {
  642. try {
  643. if (!hasFreshSources.value) throw new Error("No sources found in current page.")
  644. if (!source.value) throw new Error("Missing source.")
  645.  
  646. const $downloadButton = $(downloadDropdown).find(".downloadButton")
  647.  
  648. const filename = resolveFilename(filenameTemplate.value, source.value)
  649.  
  650. log("Downloading:", filename, source.value.url, GM_info.downloadMode)
  651.  
  652. if ("browser" === GM_info.downloadMode) {
  653. setDownloadButtonEnabled(false)
  654.  
  655. if (false);
  656.  
  657. GM_download({
  658. url: source.value.url,
  659. name: filename,
  660. onload: () => downloadEnded("onload"),
  661. onerror: (e) => downloadEnded("onerror", e),
  662. ontimeout: () => downloadEnded("ontimeout"),
  663. })
  664. } else {
  665. const a = document.createElement("a")
  666. a.href = source.value.url
  667. a.download = filename
  668. a.click()
  669. }
  670.  
  671. function setDownloadButtonEnabled(enabled) {
  672. if (enabled) {
  673. // TODO: properly disable the button
  674. $(downloadDropdown).css("pointer-events", "")
  675. $downloadButton.css("background-color", "")
  676. } else {
  677. $(downloadDropdown).css("pointer-events", "none")
  678. $downloadButton.css("background-color", "var(--primary-dark)")
  679. }
  680. }
  681.  
  682. function downloadEnded(type, e) {
  683. setDownloadButtonEnabled(true)
  684.  
  685. if ("ontimeout" === type) e = { error: "timed_out" }
  686.  
  687. if (e && e.error) {
  688. log(e)
  689. printDownloadMessage(
  690. `Download Error (${e.error}): ${(e.details && e.details.current) || "No info"}`
  691. )
  692. }
  693. }
  694. } catch (e) {
  695. log(e)
  696. printDownloadMessage(e + "")
  697. }
  698. }
  699.  
  700. function resolveFilename(template, source) {
  701. if (videoInfo.error) throw new Error("Broken video info: " + videoInfo.error)
  702.  
  703. const replacements = {
  704. ID: videoInfo.id,
  705. TITLE: videoInfo.title,
  706. RES: source.label,
  707. AUTHOR: videoInfo.author,
  708. DATE: formatDate(new Date(), ""),
  709. DATE_TS: new Date().getTime() + "",
  710. UP_DATE: formatDate(new Date(videoInfo.created), ""),
  711. UP_DATE_TS: videoInfo.created + "",
  712. }
  713.  
  714. const wrappedKeywords = FILENAME_KEYWORDS.map((k) => `{${k}}`)
  715. const regex = new RegExp(`(${wrappedKeywords.join("|")})`, "g")
  716.  
  717. const basename = template.replace(regex, (match) => {
  718. const keyword = match.slice(1, -1)
  719. const value = replacements[keyword]
  720.  
  721. // remove path delimiters in keyword values
  722. return value.replace(/[/\\]/g, illegalCharReplacement.value)
  723. })
  724.  
  725. const ext = source.url.slice(source.url.lastIndexOf(".")).replace(/[^A-Za-z0-9.]/g, "")
  726.  
  727. const filename = basename + ext
  728.  
  729. return sanitizePath(filename, illegalCharReplacement.value)
  730. }
  731.  
  732. function printDownloadMessage(msg) {
  733. $(".page-video__bottom")
  734. .css("flex-wrap", "wrap")
  735. .append(`<div style="flex: 100% 0 0">${msg}</div>`)
  736. }
  737.  
  738. const likeRateEnabled = (0, external_Vue_namespaceObject.ref)(storage.get("like_rates"))
  739. const highlightThreshold = (0, external_Vue_namespaceObject.ref)(
  740. storage.get("like_rate_highlight")
  741. )
  742. const highlightOpacity = (0, external_Vue_namespaceObject.ref)(
  743. storage.get("like_rate_highlight_opacity")
  744. )
  745.  
  746. const likeRateClass = "enh-like-rate"
  747. const highlightClass = "enh-highlight"
  748.  
  749. ready.then(updateHighlightOpacity)
  750.  
  751. ;(0, external_Vue_namespaceObject.watchEffect)(() => {
  752. storage.set("like_rates", likeRateEnabled.value)
  753.  
  754. if (likeRateEnabled.value) {
  755. document.body.classList.add("enh-show-like-rates")
  756. $(".videoTeaser, .imageTeaser").each((i, teaser) => processTeaser(teaser))
  757. } else {
  758. document.body.classList.remove("enh-show-like-rates")
  759. $("." + highlightClass).removeClass(highlightClass)
  760. }
  761. })
  762.  
  763. ;(0, external_Vue_namespaceObject.watchEffect)(() => {
  764. storage.set("like_rate_highlight", highlightThreshold.value)
  765.  
  766. $(".videoTeaser, .imageTeaser").each((i, teaser) => processTeaser(teaser))
  767. })
  768.  
  769. ;(0, external_Vue_namespaceObject.watchEffect)(() => {
  770. storage.set("like_rate_highlight_opacity", highlightOpacity.value)
  771.  
  772. updateHighlightOpacity()
  773. })
  774.  
  775. function useTeaserSettings() {
  776. return { likeRateEnabled, highlightThreshold, highlightOpacity }
  777. }
  778.  
  779. page(
  780. ["home", "videoList", "imageList", "subscriptions", "profile", "video", "image"],
  781. async (pageID, onLeave) => {
  782. const teaserObserver = new SimpleMutationObserver((mutation) =>
  783. mutation.addedNodes.forEach(detectColumn)
  784. )
  785.  
  786. onLeave(() => {
  787. teaserObserver.disconnect()
  788. })
  789.  
  790. const teaserBatcher = new TeaserBatcher()
  791.  
  792. if (["home", "profile", "image"].includes(pageID))
  793. [".videoTeaser", ".imageTeaser"].forEach(async (selector) => {
  794. const $teasers = await until$(() => $(selector), 200)
  795.  
  796. requestProcessTeasers($teasers.toArray())
  797. })
  798. else if ("video" === pageID) {
  799. const selectors = [".moreFromUser", ".moreLikeThis"].flatMap((parentCls) =>
  800. [".videoTeaser", ".imageTeaser"].map((cls) => `${parentCls} ${cls}`)
  801. )
  802.  
  803. selectors.forEach(async (selector) => {
  804. const $teasers = await until$(() => $(selector), 200)
  805.  
  806. requestProcessTeasers($teasers.toArray())
  807. })
  808. } else {
  809. const $teasers = await until$(
  810. () => $(".videoTeaser:first-of-type, .imageTeaser:first-of-type"),
  811. 200
  812. )
  813.  
  814. detectRow($teasers.closest(".row")[0])
  815. }
  816.  
  817. function detectRow(row) {
  818. teaserObserver.observe(row, { childList: true, immediate: true })
  819. }
  820.  
  821. function detectColumn(column) {
  822. const { firstChild } = column
  823.  
  824. if (
  825. !!firstChild &&
  826. (hasClass(firstChild, "videoTeaser") || hasClass(firstChild, "imageTeaser"))
  827. )
  828. requestProcessTeasers([firstChild])
  829. }
  830.  
  831. function requestProcessTeasers(teasers) {
  832. teasers.forEach((teaser) => teaserBatcher.add(teaser))
  833. teaserBatcher.run(processTeaser)
  834. }
  835. }
  836. )
  837.  
  838. class TeaserBatcher {
  839. constructor() {
  840. this.teasers = []
  841.  
  842. this.run = throttle((callback) => {
  843. let lastError
  844.  
  845. try {
  846. this.teasers.forEach(callback)
  847. } catch (e) {
  848. // only record the last error so the console won't blow up
  849. lastError = e
  850. }
  851.  
  852. if (lastError) log("Failed to process teasers", lastError)
  853.  
  854. this.teasers.length = 0
  855. }, 0)
  856. }
  857.  
  858. add(teaser) {
  859. this.teasers.push(teaser)
  860. }
  861. }
  862.  
  863. function processTeaser(teaser) {
  864. const viewsLabel = $(teaser).find(".views")
  865. const likesLabel = $(teaser).find(".likes")
  866.  
  867. let likePercentage
  868.  
  869. const likeRateLabel = viewsLabel.children("." + likeRateClass)
  870.  
  871. if (likeRateLabel.length) likePercentage = +likeRateLabel.text().trim().replace("%", "")
  872. else {
  873. const views = parseAbbreviatedNumber(viewsLabel.text().trim())
  874. const likes = parseAbbreviatedNumber(likesLabel.text().trim())
  875.  
  876. likePercentage = Math.round((100 * likes) / views)
  877.  
  878. const display = Number.isFinite(likePercentage) ? likePercentage + "%" : "/"
  879.  
  880. // prettier-ignore
  881. viewsLabel.children().eq(0).clone().addClass(likeRateClass).text(display).prependTo(viewsLabel)
  882. }
  883.  
  884. if (likePercentage >= highlightThreshold.value && likeRateEnabled.value)
  885. teaser.classList.add(highlightClass)
  886. else teaser.classList.remove(highlightClass)
  887. }
  888.  
  889. function updateHighlightOpacity() {
  890. const color = getComputedStyle(document.body).getPropertyValue("--primary").trim()
  891.  
  892. // color will be empty before the page is fully loaded
  893. if (color)
  894. document.body.style.setProperty("--ehg-hl-bg", adjustAlpha(color, highlightOpacity.value))
  895. }
  896.  
  897. const widenContentEnabled = (0, external_Vue_namespaceObject.ref)(storage.get("widen_content"))
  898. const widenContentScale = (0, external_Vue_namespaceObject.ref)(
  899. storage.get("widen_content_scale")
  900. )
  901.  
  902. ;(0, external_Vue_namespaceObject.watch)(widenContentEnabled, (enabled) =>
  903. storage.set("widen_content", enabled)
  904. )
  905. ;(0, external_Vue_namespaceObject.watch)(widenContentScale, (scale) =>
  906. storage.set("widen_content_scale", scale)
  907. )
  908.  
  909. function useWidenContentSettings() {
  910. return { widenContentEnabled, widenContentScale }
  911. }
  912.  
  913. page(["video", "image"], (pageID, onLeave) => {
  914. const mediaArea = $(".page-video__player, .page-video__slideshow").get(0)
  915.  
  916. if (!mediaArea) {
  917. log(`${"video" === pageID ? "video" : "slideshow"} area not found.`)
  918. return
  919. }
  920.  
  921. const sidebar = $(".page-video__sidebar").get(0)
  922.  
  923. if (!sidebar) {
  924. log("sidebar not found.")
  925. return
  926. }
  927.  
  928. const col = $(mediaArea).closest(".col-12").get(0)
  929. const row = $(mediaArea).closest(".row").get(0)
  930. const container = $(row).closest(".content").get(0)
  931.  
  932. onLeave((0, external_Vue_namespaceObject.watchEffect)(() => updateResize()))
  933.  
  934. function updateResize(entries) {
  935. if (widenContentEnabled.value) {
  936. let containerWidth = 0
  937. let rowWidth = 0
  938. let colWidth = 0
  939. let mediaHeight = 0
  940.  
  941. if (entries) {
  942. for (const entry of entries)
  943. if (entry.target === mediaArea) mediaHeight = entry.contentRect.height
  944. else if (entry.target === col) colWidth = entry.contentRect.width
  945. else if (entry.target === row) rowWidth = entry.contentRect.width
  946. else if (entry.target === container) containerWidth = entry.contentRect.width
  947. } else {
  948. containerWidth = container.offsetWidth
  949. rowWidth = row.offsetWidth
  950. colWidth = col.offsetWidth
  951. mediaHeight = mediaArea.offsetHeight
  952. }
  953.  
  954. if (containerWidth > 0 && rowWidth > 0 && colWidth > 0) {
  955. const scale = widenContentScale.value / 100
  956. const mediaWidth = Math.min(rowWidth * scale, containerWidth)
  957.  
  958. mediaArea.style.marginLeft = `${(rowWidth - mediaWidth) / 2}px`
  959. mediaArea.style.marginRight = `${(rowWidth - mediaWidth) / 2 - (rowWidth - colWidth)}px`
  960. }
  961.  
  962. if (mediaHeight > 0) sidebar.style.marginTop = `${mediaArea.offsetTop + mediaHeight}px`
  963. } else {
  964. mediaArea.style.marginLeft = ""
  965. mediaArea.style.marginRight = ""
  966. sidebar.style.marginTop = ""
  967. }
  968. }
  969.  
  970. const observer = new ResizeObserver(updateResize)
  971.  
  972. observer.observe(mediaArea)
  973. observer.observe(row)
  974. observer.observe(container)
  975.  
  976. onLeave(() => {
  977. observer.disconnect()
  978. })
  979. })
  980.  
  981. // extracted by mini-css-extract-plugin
  982. /* harmony default export */ const Settings_module = {
  983. switch: "Settings-module__switch--qcsG",
  984. settings: "Settings-module__settings--alpJ",
  985. active: "Settings-module__active--iMRv",
  986. disabled: "Settings-module__disabled--vvjv",
  987. header: "Settings-module__header--s2Rw",
  988. title: "Settings-module__title--aDU_",
  989. view: "Settings-module__view--dY2E",
  990. sectionHeader: "Settings-module__section-header--Xy_I",
  991. fieldLabel: "Settings-module__field-label--O5EA",
  992. labelBlock: "Settings-module__label-block--EYVa",
  993. labelInline: "Settings-module__label-inline--v3DK",
  994. panel: "Settings-module__panel--PuCY",
  995. warn: "Settings-module__warn--KbCV",
  996. }
  997.  
  998. // recommended vscode plugin for syntax highlighting: https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html
  999. // language=HTML
  1000. const template = /* html */ `
  1001. <div class='text text--text text--bold'>E</div>
  1002.  
  1003.  
  1004.  
  1005. <div v-if='visible' :class='css.settings' @click.stop>
  1006. <header :class='css.header'>
  1007. <h2 :class='css.title'>{{ $t('name') }} v${GM_info.script.version}</h2>
  1008. </header>
  1009. <nav>
  1010. <ul>
  1011. <li
  1012. v-for='tab, i in tabs'
  1013. :class='{ [css.active]: i === tabIndex }'
  1014. @click='tabIndex = i'
  1015. >
  1016. {{ $t(tab.name) }}
  1017. </li>
  1018. </ul>
  1019. </nav>
  1020. <div v-if='tabVal === "ui"' :class='css.view'>
  1021. <h2 :class='css.sectionHeader'>{{ $t('s.ui.label') }}</h2>
  1022.  
  1023.  
  1024.  
  1025. <h3 :class='css.fieldLabel'>{{ $t('s.ui.like_rate.label') }}</h3>
  1026. <p v-html='$t("s.ui.like_rate.desc")'></p>
  1027. <p>
  1028. <label :class='css.labelBlock'>
  1029. {{ $t('s.enabled') }}
  1030. <input type='checkbox' v-model='likeRateEnabled'>
  1031. </label>
  1032. </p>
  1033.  
  1034.  
  1035.  
  1036. <h3 :class='css.fieldLabel'>{{ $t('s.ui.highlight_threshold.label') }}</h3>
  1037. <p v-html='$t("s.ui.highlight_threshold.desc")'></p>
  1038. <p>
  1039. <input type='number' step='0.1' min='0' max='100' v-model='highlightThreshold'>
  1040. </p>
  1041. <h3 :class='css.fieldLabel'>{{ $t('s.ui.highlight_bg.label') }}</h3>
  1042. <p>
  1043. <input type="range" min="0" max="1" step="0.01" v-model='highlightOpacity'>
  1044. </p>
  1045.  
  1046.  
  1047.  
  1048. <h3 :class='css.fieldLabel'>{{ $t('s.ui.widen_content.label') }}</h3>
  1049. <p v-html='$t("s.ui.widen_content.desc")'></p>
  1050. <p>
  1051. <label :class='css.labelBlock'>
  1052. {{ $t('s.enabled') }}
  1053. <input type='checkbox' v-model='widenContentEnabled'>
  1054. </label>
  1055. </p>
  1056. <p>
  1057. <label :class='css.labelBlock'>
  1058. {{ $t('s.ui.widen_content.scale') }}
  1059. <input type='number' step='1' min='10' max='500' :value='widenContentScale' @change='widenContentScale = Math.round($event.target.value)'>
  1060. </label>
  1061. </p>
  1062. </div>
  1063. <div v-else-if='tabVal === "download"' :class='css.view'>
  1064. <h2 :class='css.sectionHeader'>{{ $t('s.download.label') }}</h2>
  1065.  
  1066.  
  1067.  
  1068. <h3 :class='css.fieldLabel'>{{ $t('s.download.auto.label') }}</h3>
  1069. <p v-html='$t("s.download.auto.desc")'></p>
  1070. <p>
  1071. <label :class='css.labelBlock'>
  1072. {{ $t('s.enabled') }}
  1073. <input type='checkbox' v-model='autoDownEnabled'>
  1074. </label>
  1075. </p>
  1076.  
  1077.  
  1078.  
  1079. <h3 :class='css.fieldLabel'>{{ $t('s.download.resolution.label') }}</h3>
  1080. <p>
  1081. <label v-for='res in RESOLUTIONS' :class='css.labelInline'>
  1082. <input type='radio' name='res' :value='res' v-model='resolution'>
  1083. {{ res }}
  1084. </label>
  1085. </p>
  1086.  
  1087.  
  1088.  
  1089. <h3 :class='css.fieldLabel'>{{ $t('s.download.filename.label') }}</h3>
  1090. <p v-if='!downloadMode' v-html='$t("s.download.filename.warn")'></p>
  1091. <div v-else-if='downloadMode !== "browser"' :class='[css.panel, css.warn]'>
  1092. <p v-html='$t("s.download.filename.warn_tm.desc")'></p>
  1093. <ol v-if='$tm("s.download.filename.warn_tm.steps").length'>
  1094. <li v-for='step in $tm("s.download.filename.warn_tm.steps")'><p v-html='step'></p></li>
  1095. </ol>
  1096. </div>
  1097.  
  1098.  
  1099.  
  1100. <p v-html='$t("s.download.filename.desc")'></p>
  1101.  
  1102.  
  1103.  
  1104. <div :class='css.keywords'>
  1105. <table :class='css.keywordTable'>
  1106. <tr v-for='kw in FILENAME_KEYWORDS'>
  1107. <th>{{ kw }}</th>
  1108. <td>{{ $t('s.download.filename.key.' + kw.toLowerCase()) }}</td>
  1109. </tr>
  1110. </table>
  1111. </div>
  1112. <details>
  1113. <summary>{{ $t('s.extra') }}</summary>
  1114. <p>
  1115. {{ $t('s.download.filename.replace_illegal_char') }}
  1116. <input type='text' v-model='illegalCharReplacement'>
  1117. {{ '*miku*miku:dance??.mp4 -> ' }} {{ sanitizePath('*miku*miku:dance??.mp4', illegalCharReplacement) }}
  1118. </p>
  1119. </details>
  1120. <input type='text' v-model='filenameTemplate'>
  1121. <p>{{ $t('s.download.filename.preview') + ': ' + filenamePreview }}</p>
  1122. <div :class='css.panel'>
  1123. <p><b>{{ $tm('s.download.filename.tips')[0] }}</b></p>
  1124. <ul>
  1125. <li v-for='tip in $tm("s.download.filename.tips").slice(1)'><p v-html='tip'></p></li>
  1126. </ul>
  1127. </div>
  1128. </div>
  1129. <div v-if='tabVal === "script"' :class='css.view'>
  1130. <h2 :class='css.sectionHeader'>{{ $t('s.script.label') }}</h2>
  1131.  
  1132.  
  1133.  
  1134. <h3 :class='css.fieldLabel'>{{ $t('s.script.language.label') }}</h3>
  1135. <p>
  1136. <label v-for='loc in $i18n.availableLocales' :class='css.labelInline'>
  1137. <input type='radio' name='loc' :value='loc' :checked='activeLocale === loc' @change='locale = loc'>
  1138. {{ $t('language', loc) }}
  1139. </label>
  1140. </p>
  1141. </div>
  1142. </div>
  1143. `
  1144.  
  1145. function setup() {
  1146. const tabs = [
  1147. { name: "s.ui.label", val: "ui" },
  1148. { name: "s.download.label", val: "download" },
  1149. { name: "s.script.label", val: "script" },
  1150. ]
  1151. const tabIndex = (0, external_Vue_namespaceObject.ref)(0)
  1152. const tabVal = (0, external_Vue_namespaceObject.computed)(
  1153. () => tabs[tabIndex.value] && tabs[tabIndex.value].val
  1154. )
  1155. const visible = (0, external_Vue_namespaceObject.ref)(false)
  1156.  
  1157. const onClickContainer = () => {
  1158. visible.value = !visible.value
  1159.  
  1160. if (visible.value) onClickOutside(settingsContainer, () => (visible.value = false))
  1161. }
  1162.  
  1163. settingsContainer.addEventListener("click", onClickContainer)
  1164.  
  1165. ;(0, external_Vue_namespaceObject.onBeforeUnmount)(() => {
  1166. settingsContainer.removeEventListener("click", onClickContainer)
  1167. })
  1168.  
  1169. return {
  1170. css: Settings_module,
  1171. tabs,
  1172. tabIndex,
  1173. tabVal,
  1174. visible,
  1175. downloadMode: GM_info.downloadMode,
  1176. sanitizePath,
  1177. ...useDownloaderSettings(),
  1178. ...useConfigSettings(),
  1179. ...useTeaserSettings(),
  1180. ...useWidenContentSettings(),
  1181. }
  1182. }
  1183.  
  1184. const SETTINGS_ID = "enh-settings"
  1185.  
  1186. const settingsContainer = $(
  1187. `<div id="${SETTINGS_ID}" class='header__link ${Settings_module["switch"]}'></div>`
  1188. )[0]
  1189.  
  1190. let app
  1191.  
  1192. page(ALL, (pageID, onLeave) => {
  1193. const destination = $(
  1194. ".page .header__content:first-of-type .dropdown:last-of-type, a[href='/register']"
  1195. )[0]
  1196.  
  1197. if (destination) {
  1198. // destination element will be destroyed everytime the page changes,
  1199. // so we need to insert the container after every page change
  1200. destination.before(settingsContainer)
  1201.  
  1202. // lazy-init the app
  1203. if (!app) {
  1204. app = (0, external_Vue_namespaceObject.createApp)({ template, setup })
  1205.  
  1206. app.use(i18n_i18n)
  1207.  
  1208. if (true)
  1209. // pending fix https://github.com/vuejs/core/pull/5197
  1210. // @ts-ignore
  1211. unsafeWindow.Vue = Vue
  1212.  
  1213. app.mount(settingsContainer)
  1214.  
  1215. log("Settings view initialized")
  1216. }
  1217. } else log("Could not insert settings view: container not found.")
  1218. })
  1219.  
  1220. // prevent Sentry from tracking the logging
  1221. setLogger(console.log.__sentry_original__ || console.log)
  1222.  
  1223. const patchedFlag = "__enhPatched"
  1224.  
  1225. page(["video"], async (pageID, onLeave) => {
  1226. const timerPromise = until(() => {
  1227. const player = getPlayer()
  1228.  
  1229. if (player) {
  1230. fixResolution(player)
  1231.  
  1232. if (!(patchedFlag in player)) {
  1233. player[patchedFlag] = true
  1234. preventVolumeScrolling(player)
  1235. }
  1236. }
  1237. }, 500)
  1238.  
  1239. onLeave(() => timerPromise.cancel())
  1240.  
  1241. function getPlayer() {
  1242. var _$$get
  1243. return null === (_$$get = $(".page-video__player .video-js").get(0)) || void 0 === _$$get
  1244. ? void 0
  1245. : _$$get.player
  1246. }
  1247.  
  1248. function preventVolumeScrolling(player) {
  1249. const originalGet = WeakMap.prototype.get
  1250.  
  1251. // hook WeakMap.get() to get the event data
  1252. // https://github.com/videojs/video.js/blob/2b0df25df332dceaab375327887f0721ca8d21d0/src/js/utils/events.js#L271
  1253. WeakMap.prototype.get = function (key) {
  1254. const value = originalGet.call(this, key)
  1255.  
  1256. try {
  1257. var _data$handlers
  1258. const data = value
  1259.  
  1260. if (
  1261. null !== data &&
  1262. void 0 !== data &&
  1263. null !== (_data$handlers = data.handlers) &&
  1264. void 0 !== _data$handlers &&
  1265. _data$handlers.mousewheel
  1266. ) {
  1267. log(`removing ${data.handlers.mousewheel.length} mousewheel handler(s) from Player`)
  1268.  
  1269. // the listeners are bound functions and cannot be checked with toString(),
  1270. // so we have to remove all mousewheel handlers
  1271. delete data.handlers.mousewheel
  1272. }
  1273. } catch (e) {
  1274. log("error:", e)
  1275. } finally {
  1276. return value
  1277. }
  1278. }
  1279.  
  1280. // trigger the hook by adding an arbitrary event listener
  1281. player.on("__dummy", () => {})
  1282. player.off("__dummy")
  1283.  
  1284. WeakMap.prototype.get = originalGet
  1285.  
  1286. const originalOn = player.on
  1287.  
  1288. // prevent adding new mousewheel listeners
  1289. player.on = function (targetOrType) {
  1290. if ("mousewheel" === targetOrType) {
  1291. log("prevented adding mousewheel listener")
  1292. return
  1293. }
  1294.  
  1295. for (
  1296. var _len = arguments.length, rest = new Array(_len > 1 ? _len - 1 : 0), _key = 1;
  1297. _key < _len;
  1298. _key++
  1299. )
  1300. rest[_key - 1] = arguments[_key]
  1301. return originalOn.call(this, targetOrType, ...rest)
  1302. }
  1303. }
  1304.  
  1305. function fixResolution(player) {
  1306. const targetSource = getTargetSource(player)
  1307.  
  1308. if (targetSource && targetSource.src !== player.src()) {
  1309. log(`setting resolution to ${targetSource.name}: ${targetSource.src}`)
  1310. player.src(targetSource)
  1311. }
  1312. }
  1313.  
  1314. function getTargetSource(player) {
  1315. const sources = player.currentSources()
  1316.  
  1317. if (!sources.length) return
  1318.  
  1319. const selectedResName = localStorage.getItem("player-resolution")
  1320. const source = sources.find((s) => s.name === selectedResName)
  1321.  
  1322. if (source) return source
  1323.  
  1324. log(`error: source not found for ${selectedResName}`)
  1325.  
  1326. return
  1327. }
  1328. })
  1329.  
  1330. // this "bg" is covering the video player and preventing player from entering fullscreen mode by double-clicks
  1331. GM_addStyle(`
  1332. .videoPlayer__bg {
  1333. pointer-events: none;
  1334. }
  1335. `)
  1336.  
  1337. const state = (0, external_Vue_namespaceObject.reactive)({ theme: "light" })
  1338.  
  1339. setInterval(() => {
  1340. state.theme = localStorage.theme
  1341. }, 1e3)
  1342.  
  1343. ;(0, external_Vue_namespaceObject.watchEffect)(updateTheme)
  1344.  
  1345. function updateTheme() {
  1346. const theme = state.theme
  1347. const adjustmentSign = "light" === theme ? -1 : 1
  1348. const bodyColor = getComputedStyle(document.body).getPropertyValue("--body")
  1349.  
  1350. document.body.style.setProperty(
  1351. "--enh-body-focus",
  1352. adjustHexColor(bodyColor, 15 * adjustmentSign)
  1353. )
  1354. document.body.style.setProperty(
  1355. "--enh-body-highlight",
  1356. adjustHexColor(bodyColor, 30 * adjustmentSign)
  1357. )
  1358.  
  1359. const darkClass = "enh-dark"
  1360.  
  1361. if ("dark" === theme) document.body.classList.add(darkClass)
  1362. else document.body.classList.remove(darkClass)
  1363. }
  1364.  
  1365. async function main() {
  1366. document.body.classList.add("enh-body")
  1367.  
  1368. setupPaging()
  1369. }
  1370.  
  1371. main()
  1372. })()
  1373.  
  1374. GM_addStyle(`
  1375. .Settings-module__switch--qcsG {
  1376. cursor: pointer;
  1377. }
  1378.  
  1379. .Settings-module__settings--alpJ {
  1380. position: absolute;
  1381. z-index: 1000;
  1382. top: 100%;
  1383. right: 0;
  1384. width: 400px;
  1385. max-height: calc(100vh - 65px);
  1386. overflow: auto;
  1387. background: var(--body);
  1388. font-size: 14px;
  1389. border: 2px solid var(--primary);
  1390. border-top: none;
  1391. cursor: default;
  1392. }
  1393.  
  1394. .Settings-module__settings--alpJ nav {
  1395. padding: 0 16px;
  1396. border-bottom: 1px solid var(--enh-body-highlight);
  1397. }
  1398.  
  1399. .Settings-module__settings--alpJ nav ul {
  1400. margin: 0;
  1401. padding: 0;
  1402. display: flex;
  1403. flex-wrap: wrap;
  1404. }
  1405.  
  1406. .Settings-module__settings--alpJ nav li {
  1407. padding: 8px 16px;
  1408. list-style-type: none;
  1409. cursor: pointer;
  1410. }
  1411.  
  1412. .Settings-module__settings--alpJ nav li:hover {
  1413. background: var(--enh-body-focus);
  1414. }
  1415.  
  1416. .Settings-module__settings--alpJ nav li.Settings-module__active--iMRv {
  1417. background: var(--enh-body-highlight);
  1418. }
  1419.  
  1420. .Settings-module__settings--alpJ details {
  1421. border: 1px solid var(--enh-body-highlight);
  1422. }
  1423.  
  1424. .Settings-module__settings--alpJ details > * {
  1425. padding: 0 8px;
  1426. }
  1427.  
  1428. .Settings-module__settings--alpJ p,
  1429. .Settings-module__settings--alpJ summary {
  1430. color: var(--muted);
  1431. }
  1432.  
  1433. .Settings-module__settings--alpJ p {
  1434. margin-top: 0;
  1435. margin-bottom: 8px;
  1436. }
  1437.  
  1438. .Settings-module__settings--alpJ a {
  1439. font-weight: bold;
  1440. cursor: pointer;
  1441. }
  1442.  
  1443. .Settings-module__settings--alpJ ol,
  1444. .Settings-module__settings--alpJ ul {
  1445. padding-left: 20px;
  1446. }
  1447.  
  1448. .Settings-module__settings--alpJ table {
  1449. margin: 8px 0;
  1450. width: 100%;
  1451. background: var(--enh-body-focus);
  1452. border: 1px solid var(--enh-body-highlight);
  1453. border-collapse: collapse;
  1454. }
  1455.  
  1456. .Settings-module__settings--alpJ th {
  1457. text-align: right;
  1458. }
  1459.  
  1460. .Settings-module__settings--alpJ th,
  1461. .Settings-module__settings--alpJ td {
  1462. padding: 4px 8px;
  1463. border: 1px solid var(--enh-body-highlight);
  1464. }
  1465.  
  1466. .Settings-module__settings--alpJ label,
  1467. .Settings-module__settings--alpJ summary {
  1468. cursor: pointer;
  1469. }
  1470.  
  1471. .Settings-module__settings--alpJ label:hover, .Settings-module__settings--alpJ summary:hover {
  1472. background: var(--enh-body-focus);
  1473. }
  1474.  
  1475. .Settings-module__settings--alpJ label input, .Settings-module__settings--alpJ summary input {
  1476. cursor: pointer;
  1477. }
  1478.  
  1479. .Settings-module__settings--alpJ label.Settings-module__disabled--vvjv, .Settings-module__settings--alpJ summary.Settings-module__disabled--vvjv {
  1480. cursor: not-allowed;
  1481. }
  1482.  
  1483. .Settings-module__settings--alpJ label.Settings-module__disabled--vvjv input, .Settings-module__settings--alpJ summary.Settings-module__disabled--vvjv input {
  1484. cursor: not-allowed;
  1485. }
  1486.  
  1487. .Settings-module__settings--alpJ input[type="text"] {
  1488. outline: none !important;
  1489. }
  1490.  
  1491. .Settings-module__settings--alpJ input[type="text"] {
  1492. margin: 8px 0;
  1493. width: 100%;
  1494. padding: 8px;
  1495. background: var(--enh-body-focus);
  1496. color: var(--text);
  1497. border: 2px solid var(--enh-body-highlight);
  1498. border-radius: 3px;
  1499. }
  1500.  
  1501. .Settings-module__settings--alpJ input[type="text"]:hover,
  1502. .Settings-module__settings--alpJ input[type="text"]:focus {
  1503. background: var(--enh-body-highlight);
  1504. }
  1505.  
  1506. .Settings-module__header--s2Rw {
  1507. padding: 0 16px;
  1508. }
  1509.  
  1510. .Settings-module__title--aDU_ {
  1511. margin-top: 4px;
  1512. }
  1513.  
  1514. .Settings-module__view--dY2E {
  1515. padding: 16px;
  1516. }
  1517.  
  1518. .Settings-module__section-header--Xy_I {
  1519. margin-bottom: 16px;
  1520. }
  1521.  
  1522. .Settings-module__field-label--O5EA {
  1523. position: relative;
  1524. margin: 16px 0;
  1525. padding-top: 16px;
  1526. }
  1527.  
  1528. .Settings-module__field-label--O5EA:not(:first-of-type) {
  1529. border-top: 1px solid var(--enh-body-highlight);
  1530. }
  1531.  
  1532. .Settings-module__label-block--EYVa {
  1533. display: flex;
  1534. padding: 8px 8px 8px 0;
  1535. }
  1536.  
  1537. .Settings-module__label-block--EYVa input {
  1538. margin-left: auto;
  1539. }
  1540.  
  1541. .Settings-module__label-inline--v3DK {
  1542. display: inline-flex;
  1543. padding: 8px 8px 8px 0;
  1544. }
  1545.  
  1546. .Settings-module__label-inline--v3DK:not(:first-child) {
  1547. padding-left: 8px;
  1548. }
  1549.  
  1550. .Settings-module__label-inline--v3DK:not(:last-child) {
  1551. margin-right: 8px;
  1552. }
  1553.  
  1554. .Settings-module__panel--PuCY {
  1555. margin-bottom: 8px;
  1556. padding: 8px;
  1557. background: var(--enh-body-focus);
  1558. }
  1559.  
  1560. .Settings-module__warn--KbCV {
  1561. background-color: #e9db89;
  1562. }
  1563.  
  1564. .enh-dark .Settings-module__warn--KbCV {
  1565. background-color: #594c00;
  1566. }
  1567.  
  1568. .enh-body {
  1569. --ehg-hl-bg: rbga(0, 0, 0, 0);
  1570. }
  1571.  
  1572. #enh-settings {
  1573. position: relative;
  1574. }
  1575.  
  1576. #enh-settings * {
  1577. box-sizing: border-box;
  1578. }
  1579.  
  1580. .enh-like-rate {
  1581. display: none;
  1582. }
  1583.  
  1584. .enh-show-like-rates .videoTeaser .views, .enh-show-like-rates .imageTeaser .views {
  1585. }
  1586.  
  1587. .enh-show-like-rates .videoTeaser .enh-like-rate, .enh-show-like-rates .imageTeaser .enh-like-rate {
  1588. display: block;
  1589. }
  1590.  
  1591. .enh-show-like-rates .videoTeaser .enh-like-rate + .text, .enh-show-like-rates .imageTeaser .enh-like-rate + .text {
  1592. display: none;
  1593. }
  1594.  
  1595. .enh-show-like-rates .page-start__subscriptions,
  1596. .enh-show-like-rates .page-start__videos,
  1597. .enh-show-like-rates .page-start__images {
  1598. position: relative;
  1599. z-index: 0;
  1600. }
  1601.  
  1602. /* for all the affected pages, check out process-teaser.ts */
  1603. .enh-highlight:before {
  1604. content: "";
  1605. position: absolute;
  1606. z-index: -1;
  1607. top: -8px;
  1608. bottom: 7px;
  1609. left: 7px;
  1610. right: 7px;
  1611. background: var(--ehg-hl-bg);
  1612. }
  1613. .page-video .enh-highlight:before,
  1614. .page-image .enh-highlight:before {
  1615. content: none;
  1616. }
  1617. .page-profile .enh-highlight,
  1618. .page-subscriptions .enh-highlight {
  1619. position: relative;
  1620. }
  1621. .page-profile .enh-highlight:before, .page-subscriptions .enh-highlight:before {
  1622. top: -6px;
  1623. bottom: -6px;
  1624. left: -6px;
  1625. right: -6px;
  1626. }
  1627. .page-video .enh-highlight,
  1628. .page-image .enh-highlight {
  1629. background: var(--ehg-hl-bg);
  1630. }
  1631.  
  1632. `)