Iwara 향상 — 사용자 정의 파일명 다운로드, 좋아요 비율 표시, 와이드 스크린, 더블클릭 전체화면, 비디오 플레이어 수정 등. Tampermonkey 및 Violentmonkey 지원.
// ==UserScript== // @name Iwara Enhancement // @name:en Iwara Enhancement // @name:zh-CN Iwara增强 // @name:ja Iwara 強化 // @name:ko Iwara 향상 // @description Iwara Enhancement — one-click download with customizable filenames, like ratio display, wide-screen layout, double-click fullscreen, and video player fixes. Supports Tampermonkey and Violentmonkey. // @description:en Iwara Enhancement — one-click download with customizable filenames, like ratio display, wide-screen layout, double-click fullscreen, and video player fixes. Supports Tampermonkey and Violentmonkey. // @description:zh-CN Iwara增强 — 一键下载与自定义文件名、显示喜爱率、宽屏布局、双击全屏、修复视频播放器等。支持 Tampermonkey 和 Violentmonkey。 // @description:ja Iwara 強化 — カスタムファイル名でのダウンロード、高評価率表示、ワイド画面、ダブルクリック全画面、動画プレイヤー修正など。Tampermonkey・Violentmonkey 対応。 // @description:ko Iwara 향상 — 사용자 정의 파일명 다운로드, 좋아요 비율 표시, 와이드 스크린, 더블클릭 전체화면, 비디오 플레이어 수정 등. Tampermonkey 및 Violentmonkey 지원. // @noframes // @grant unsafeWindow // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_download // @grant GM_info // @require https://unpkg.com/jquery // @require https://unpkg.com/[email protected] // @require https://unpkg.com/vue-i18n // @match *://*.iwara.tv/* // @connect *.iwara.tv // @namespace https://github.com/a1156883061/userscripts-iwara-enhancement // @version 1.5 // @author a1156883061 // @source https://github.com/a1156883061/userscripts-iwara-enhancement // @supportURL https://github.com/a1156883061/userscripts-iwara-enhancement/issues // @license MIT // ==/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 static emptyNodeList = document.querySelectorAll("#__absolutely_nonexisting") 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) options && options.immediate && options.childList && target.childNodes.length && this.callback({ target, type: "childList", addedNodes: target.childNodes, removedNodes: SimpleMutationObserver.emptyNodeList, }) } } function hasClass(node, className) { return !!node.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 && arguments[1] !== void 0 ? 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 formatDateReadable(date) { let includeTime = !(arguments.length > 1 && arguments[1] !== void 0) || arguments[1] const d = [date.getFullYear(), date.getMonth() + 1, date.getDate()] .map((num) => String(num).padStart(2, "0")) .join("-") if (!includeTime) return d const t = [date.getHours(), date.getMinutes(), date.getSeconds()] .map((num) => String(num).padStart(2, "0")) .join("") return `${d}T${t}` } function formatError(e) { let fallback = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : "Unknown error" if (typeof e === "string") return e || fallback if (e !== null && typeof e === "object") { if (e instanceof Event && e.type === "error") return "Failed to load resource" if (e.message) return e.message const str = String(e) return str === "[object Object]" ? 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 (color.length !== 7) 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")) throw new Error("Invalid color format") { 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) } } return `rgba(${r}, ${g}, ${b}, ${alpha})` } /** * Replaces characters that are forbidden in file systems. */ function sanitizePath(path, illegalCharReplacement) { let keepDelimiters = !(arguments.length > 2 && arguments[2] !== void 0) || arguments[2] path = path.replace(/[*:<>?|]/g, illegalCharReplacement) 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","date_fmt":"current date & time (e.g. 2026-06-10T143000)","up_date_fmt":"upload date & time (e.g. 2026-06-10T143000)"},"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."]}},"ui":{"label":"UI","like_rate":{"label":"Like rate","desc":"Displays like rates in video/image lists."},"highlight_threshold":{"label":"Highlight threshold","desc":"Highlights video/image items over certain like rate."},"highlight_bg":{"label":"Highlight opacity"},"widen_content":{"label":"Widen content","desc":"Widen the content area in video/image pages.","scale":"Additional scale (%)"},"widen_list":{"label":"Widen list","desc":"Widen image/video lists.","scale":"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 的时间戳格式","date_fmt":"当前日期和时间(如 2026-06-10T143000)","up_date_fmt":"视频发布日期和时间(如 2026-06-10T143000)"},"replace_illegal_char":"将文件名中的非法字符替换为:","tips":["提示","可以在文件名里使用\\"/\\"来创建文件夹,例如{AUTHOR}/{DATE}-{TITLE}","如果文件名不起作用,检查一下是否安装了与下载相关的浏览器插件,比如 Aria2 插件"]}},"ui":{"label":"界面","like_rate":{"label":"喜爱率","desc":"在视频和图片列表里显示喜爱率"},"highlight_threshold":{"label":"高亮分界点","desc":"喜爱率高于此值的视频和图片将会被高亮显示"},"highlight_bg":{"label":"高亮透明度"},"widen_content":{"label":"加宽内容区域","desc":"加宽视频页和图片页的内容区域","scale":"额外缩放(%)"},"widen_list":{"label":"加宽列表","desc":"加宽视频列表和图片列表","scale":"缩放(%)"}},"script":{"label":"脚本","language":{"label":"语言"}}}}' ) /* harmony default export */ const i18n = { zh: zh_namespaceObject, en: en_namespaceObject } function createStorage(prefix, schema) { prefix && (prefix += ".") return { get(key) { return GM_getValue(prefix + key, schema[key]) }, set(key, val) { typeof val === "function" && (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_list: true, widen_list_scale: window.innerWidth < // container-fluid's default max-width is 1200px, leave 100px for tolerance 1200 + 100 ? 100 : ~~(100 * ((window.innerWidth - /* sidebar width */ 250 - /* spacing */ 100) / 1200)), 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) => { document.readyState === "loading" ? document.addEventListener("DOMContentLoaded", () => resolve()) : 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 = typeof id === "string" ? [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 (matchedID !== void 0) 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 hasOtherPageElements || emitter.emit(event, node.className) break } } function cancelOnLeave(onLeave, promise) { onLeave(() => promise.cancel()) return promise } // cached keys, since there will most likely be only one React instance in the page let reactInstanceKey = "" let reactEventHandlersKey = "" function getReactInstance(element) { const reactElement = element if (reactInstanceKey) return reactElement[reactInstanceKey] for (const key of Object.keys(element)) if (key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")) { reactInstanceKey = key return reactElement[key] } } function getReactEventHandlers(element) { const reactElement = element if (reactEventHandlersKey) return reactElement[reactEventHandlersKey] // 先尝试 React 16/17 的 __reactEventHandlers$ for (const key of Object.keys(element)) if (key.startsWith("__reactEventHandlers$")) { reactEventHandlersKey = key return reactElement[key] } // 再尝试 React 18 的 __reactProps$ for (const key of Object.keys(element)) if (key.startsWith("__reactProps$")) { reactEventHandlersKey = key return reactElement[key] } return } // 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 && arguments[1] !== void 0 ? arguments[1] : 0 !(arguments.length > 2 && arguments[2] !== void 0) || arguments[2] let cancelled = false 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 { 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 && arguments[1] !== void 0 ? arguments[1] : 0 let cancelOnReload = !(arguments.length > 2 && arguments[2] !== void 0) || arguments[2] 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", "DATE_FMT", "UP_DATE_FMT", ] 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)) 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 = findVideoInReactProps(getReactEventHandlers(videoActions)) || findVideoInFiber(videoActions) if (!video) throw new Error("Cannot extract video info from page (React internal API changed)") 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 findVideoInReactProps(props) { if (!props) return if (props.video) return props.video const children = Array.isArray(props.children) ? props.children : [props.children] for (const child of children) { const video = findVideoInReactProps(child?.props) if (video) return video } return } function findVideoInFiber(element) { let fiber = getReactInstance(element) while (fiber) { const video = findVideoInReactProps(fiber.memoizedProps) if (video) return video fiber = fiber.return } return } 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) { $dropdown.data("converted") || $dropdown .data("converted", true) .on("click", function () { download(this) }) .children(".dropdown__content") .css("display", "none") const resolution = 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 (GM_info.downloadMode === "browser") { setDownloadButtonEnabled(false) const downloadUrl = resolveDownloadUrl(source.value.url, filename) GM_download({ url: downloadUrl, name: filename, onload: () => downloadEnded("onload"), onerror: (e) => { downloadEnded("onerror", e) openNativeDownload(downloadUrl, filename) }, ontimeout: () => downloadEnded("ontimeout"), }) } else openNativeDownload(resolveDownloadUrl(source.value.url, filename), filename) 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) type === "ontimeout" && (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 + "", DATE_FMT: formatDateReadable(new Date()), UP_DATE_FMT: formatDateReadable(new Date(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 = getFilenameExtension(source.url) const filename = basename + ext return sanitizePath(filename, illegalCharReplacement.value) } function getFilenameExtension(sourceUrl) { try { const url = new URL(sourceUrl) const queryFilename = url.searchParams.get("filename") const path = queryFilename || url.pathname const match = path.match(/\.([A-Za-z0-9]+)$/) return match ? `.${match[1]}` : "" } catch { const path = sourceUrl.split(/[?#]/)[0] || sourceUrl const match = path.match(/\.([A-Za-z0-9]+)$/) return match ? `.${match[1]}` : "" } } function openNativeDownload(url, filename) { const a = document.createElement("a") a.href = url a.download = filename a.click() } function resolveDownloadUrl(sourceUrl, filename) { try { const url = new URL(sourceUrl) url.hostname.endsWith("iwara.tv") && url.searchParams.has("download") && url.searchParams.set("download", getDownloadFilenameLeaf(filename)) return url.href } catch { return sourceUrl } } function getDownloadFilenameLeaf(filename) { return filename.split(/[\\/]/).pop() || filename } 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 (pageID === "video") { 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 !firstChild || (!hasClass(firstChild, "videoTeaser") && !hasClass(firstChild, "imageTeaser")) || requestProcessTeasers([firstChild]) } function requestProcessTeasers(teasers) { teasers.forEach((teaser) => teaserBatcher.add(teaser)) teaserBatcher.run(processTeaser) } } ) class TeaserBatcher { teasers = [] add(teaser) { this.teasers.push(teaser) } 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 } lastError && log("Failed to process teasers", lastError) this.teasers.length = 0 }, 0) } 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) } likePercentage >= highlightThreshold.value && likeRateEnabled.value ? teaser.classList.add(highlightClass) : teaser.classList.remove(highlightClass) } function updateHighlightOpacity() { const color = getComputedStyle(document.body).getPropertyValue("--primary").trim() // color will be empty before the page is fully loaded color && document.body.style.setProperty("--ehg-hl-bg", adjustAlpha(color, highlightOpacity.value)) } const widenListEnabled = (0, external_Vue_namespaceObject.ref)(storage.get("widen_list")) const widenListScale = (0, external_Vue_namespaceObject.ref)(storage.get("widen_list_scale")) 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)(widenListEnabled, (enabled) => storage.set("widen_list", enabled) ) ;(0, external_Vue_namespaceObject.watch)(widenListScale, (scale) => storage.set("widen_list_scale", 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) ) ;(0, external_Vue_namespaceObject.watch)( [widenListEnabled, widenListScale], throttle(updateListScale, 100) ) function useWidenContentSettings() { return { widenListEnabled, widenListScale, widenContentEnabled, widenContentScale } } let widenListStyleEl function updateListScale() { widenListStyleEl?.remove() widenListStyleEl = void 0 if (widenListEnabled.value) { const pageIds = [ "home", "videoList", "imageList", "subscriptions", "profile", "video", "image", ] const classes = pageIds.map((pageId) => `.page-${pageId} .container-fluid`) widenListStyleEl = GM_addStyle(`${classes.join(",")} { max-width: ${1200 * (widenListScale.value / 100)}px; }`) } } updateListScale() page(["video", "image"], (pageID, onLeave) => { const mediaArea = $(".page-video__player, .page-video__slideshow").get(0) if (!mediaArea) { log(`${pageID === "video" ? "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) entry.target === mediaArea ? (mediaHeight = entry.contentRect.height) : entry.target === col ? (colWidth = entry.contentRect.width) : entry.target === row ? (rowWidth = entry.contentRect.width) : entry.target === container && (containerWidth = entry.contentRect.width) else { containerWidth = container.offsetWidth rowWidth = row.offsetWidth colWidth = col.offsetWidth mediaHeight = mediaArea.offsetHeight } // iwara uses a polyfilled ResizeObserver, which reports an error when resizing DOMs in the callback immediately, // this is so stupid that I have to use setTimeout to avoid the error // see: https://github.com/juggle/resize-observer/issues/103 setTimeout(() => { 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` } mediaHeight > 0 && (sidebar.style.marginTop = `${~~(mediaArea.offsetTop + mediaHeight)}px`) }, 0) } 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", } // recommended vscode plugin for syntax highlighting: https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html // language=HTML // 由于Iwara的CSP限制,不能运行时渲染Vue模板,需要构建时预编译渲染函数(原模板请参见 git 中的源码) function render(_ctx, _cache) { return ( (0, external_Vue_namespaceObject.openBlock)(), (0, external_Vue_namespaceObject.createElementBlock)( external_Vue_namespaceObject.Fragment, null, [ (0, external_Vue_namespaceObject.createElementVNode)( "div", { class: "text text--text text--bold" }, "E" ), _ctx.visible ? ((0, external_Vue_namespaceObject.openBlock)(), (0, external_Vue_namespaceObject.createElementBlock)( "div", { key: 0, class: (0, external_Vue_namespaceObject.normalizeClass)(_ctx.css.settings), onClick: (0, external_Vue_namespaceObject.withModifiers)(() => {}, ["stop"]), }, [ (0, external_Vue_namespaceObject.createElementVNode)( "header", { class: (0, external_Vue_namespaceObject.normalizeClass)(_ctx.css.header) }, [ (0, external_Vue_namespaceObject.createElementVNode)( "h2", { class: (0, external_Vue_namespaceObject.normalizeClass)(_ctx.css.title) }, (0, external_Vue_namespaceObject.toDisplayString)(_ctx.$t("name")) + " v${GM_info.script.version}", 3 ), ], 2 ), (0, external_Vue_namespaceObject.createElementVNode)("nav", null, [ (0, external_Vue_namespaceObject.createElementVNode)("ul", null, [ ((0, external_Vue_namespaceObject.openBlock)(true), (0, external_Vue_namespaceObject.createElementBlock)( external_Vue_namespaceObject.Fragment, null, (0, external_Vue_namespaceObject.renderList)( _ctx.tabs, (tab, i) => ( (0, external_Vue_namespaceObject.openBlock)(), (0, external_Vue_namespaceObject.createElementBlock)( "li", { class: (0, external_Vue_namespaceObject.normalizeClass)({ [_ctx.css.active]: i === _ctx.tabIndex, }), onClick: ($event) => (_ctx.tabIndex = i), }, (0, external_Vue_namespaceObject.toDisplayString)(_ctx.$t(tab.name)), 11, ["onClick"] ) ) ), 256 )), ]), ]), _ctx.tabVal === "ui" ? ((0, external_Vue_namespaceObject.openBlock)(), (0, external_Vue_namespaceObject.createElementBlock)( "div", { key: 0, class: (0, external_Vue_namespaceObject.normalizeClass)(_ctx.css.view), }, [ (0, external_Vue_namespaceObject.createElementVNode)( "h2", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.sectionHeader ), }, (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.ui.label") ), 3 ), (0, external_Vue_namespaceObject.createElementVNode)( "h3", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.fieldLabel ), }, (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.ui.like_rate.label") ), 3 ), (0, external_Vue_namespaceObject.createElementVNode)( "p", { innerHTML: _ctx.$t("s.ui.like_rate.desc") }, null, 8, ["innerHTML"] ), (0, external_Vue_namespaceObject.createElementVNode)("p", null, [ (0, external_Vue_namespaceObject.createElementVNode)( "label", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.labelBlock ), }, [ (0, external_Vue_namespaceObject.createTextVNode)( (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.enabled") ) + " ", 1 ), (0, external_Vue_namespaceObject.withDirectives)( (0, external_Vue_namespaceObject.createElementVNode)( "input", { type: "checkbox", "onUpdate:modelValue": ($event) => (_ctx.likeRateEnabled = $event), }, null, 8, ["onUpdate:modelValue"] ), [ [ external_Vue_namespaceObject.vModelCheckbox, _ctx.likeRateEnabled, ], ] ), ], 2 ), ]), (0, external_Vue_namespaceObject.createElementVNode)( "h3", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.fieldLabel ), }, (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.ui.highlight_threshold.label") ), 3 ), (0, external_Vue_namespaceObject.createElementVNode)( "p", { innerHTML: _ctx.$t("s.ui.highlight_threshold.desc") }, null, 8, ["innerHTML"] ), (0, external_Vue_namespaceObject.createElementVNode)("p", null, [ (0, external_Vue_namespaceObject.withDirectives)( (0, external_Vue_namespaceObject.createElementVNode)( "input", { type: "number", step: "0.1", min: "0", max: "100", "onUpdate:modelValue": ($event) => (_ctx.highlightThreshold = $event), }, null, 8, ["onUpdate:modelValue"] ), [[external_Vue_namespaceObject.vModelText, _ctx.highlightThreshold]] ), ]), (0, external_Vue_namespaceObject.createElementVNode)( "h3", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.fieldLabel ), }, (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.ui.highlight_bg.label") ), 3 ), (0, external_Vue_namespaceObject.createElementVNode)("p", null, [ (0, external_Vue_namespaceObject.withDirectives)( (0, external_Vue_namespaceObject.createElementVNode)( "input", { type: "range", min: "0", max: "1", step: "0.01", "onUpdate:modelValue": ($event) => (_ctx.highlightOpacity = $event), }, null, 8, ["onUpdate:modelValue"] ), [[external_Vue_namespaceObject.vModelText, _ctx.highlightOpacity]] ), ]), (0, external_Vue_namespaceObject.createElementVNode)( "h3", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.fieldLabel ), }, (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.ui.widen_content.label") ), 3 ), (0, external_Vue_namespaceObject.createElementVNode)( "p", { innerHTML: _ctx.$t("s.ui.widen_content.desc") }, null, 8, ["innerHTML"] ), (0, external_Vue_namespaceObject.createElementVNode)("p", null, [ (0, external_Vue_namespaceObject.createElementVNode)( "label", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.labelBlock ), }, [ (0, external_Vue_namespaceObject.createTextVNode)( (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.enabled") ) + " ", 1 ), (0, external_Vue_namespaceObject.withDirectives)( (0, external_Vue_namespaceObject.createElementVNode)( "input", { type: "checkbox", "onUpdate:modelValue": ($event) => (_ctx.widenContentEnabled = $event), }, null, 8, ["onUpdate:modelValue"] ), [ [ external_Vue_namespaceObject.vModelCheckbox, _ctx.widenContentEnabled, ], ] ), ], 2 ), ]), (0, external_Vue_namespaceObject.createElementVNode)("p", null, [ (0, external_Vue_namespaceObject.createElementVNode)( "label", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.labelBlock ), }, [ (0, external_Vue_namespaceObject.createTextVNode)( (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.ui.widen_content.scale") ) + " ", 1 ), (0, external_Vue_namespaceObject.createElementVNode)( "input", { type: "number", step: "1", min: "10", max: "500", value: _ctx.widenContentScale, onChange: ($event) => (_ctx.widenContentScale = Math.round($event.target.value)), }, null, 40, ["value", "onChange"] ), ], 2 ), ]), (0, external_Vue_namespaceObject.createElementVNode)( "h3", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.fieldLabel ), }, (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.ui.widen_list.label") ), 3 ), (0, external_Vue_namespaceObject.createElementVNode)( "p", { innerHTML: _ctx.$t("s.ui.widen_list.desc") }, null, 8, ["innerHTML"] ), (0, external_Vue_namespaceObject.createElementVNode)("p", null, [ (0, external_Vue_namespaceObject.createElementVNode)( "label", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.labelBlock ), }, [ (0, external_Vue_namespaceObject.createTextVNode)( (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.enabled") ) + " ", 1 ), (0, external_Vue_namespaceObject.withDirectives)( (0, external_Vue_namespaceObject.createElementVNode)( "input", { type: "checkbox", "onUpdate:modelValue": ($event) => (_ctx.widenListEnabled = $event), }, null, 8, ["onUpdate:modelValue"] ), [ [ external_Vue_namespaceObject.vModelCheckbox, _ctx.widenListEnabled, ], ] ), ], 2 ), ]), (0, external_Vue_namespaceObject.createElementVNode)("p", null, [ (0, external_Vue_namespaceObject.createElementVNode)( "label", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.labelBlock ), }, [ (0, external_Vue_namespaceObject.createTextVNode)( (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.ui.widen_list.scale") ) + " ", 1 ), (0, external_Vue_namespaceObject.createElementVNode)( "input", { type: "number", step: "1", min: "10", max: "500", value: _ctx.widenListScale, onChange: ($event) => (_ctx.widenListScale = Math.round($event.target.value)), }, null, 40, ["value", "onChange"] ), ], 2 ), ]), ], 2 )) : _ctx.tabVal === "download" ? ((0, external_Vue_namespaceObject.openBlock)(), (0, external_Vue_namespaceObject.createElementBlock)( "div", { key: 1, class: (0, external_Vue_namespaceObject.normalizeClass)(_ctx.css.view), }, [ (0, external_Vue_namespaceObject.createElementVNode)( "h2", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.sectionHeader ), }, (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.download.label") ), 3 ), (0, external_Vue_namespaceObject.createElementVNode)( "h3", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.fieldLabel ), }, (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.download.auto.label") ), 3 ), (0, external_Vue_namespaceObject.createElementVNode)( "p", { innerHTML: _ctx.$t("s.download.auto.desc") }, null, 8, ["innerHTML"] ), (0, external_Vue_namespaceObject.createElementVNode)("p", null, [ (0, external_Vue_namespaceObject.createElementVNode)( "label", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.labelBlock ), }, [ (0, external_Vue_namespaceObject.createTextVNode)( (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.enabled") ) + " ", 1 ), (0, external_Vue_namespaceObject.withDirectives)( (0, external_Vue_namespaceObject.createElementVNode)( "input", { type: "checkbox", "onUpdate:modelValue": ($event) => (_ctx.autoDownEnabled = $event), }, null, 8, ["onUpdate:modelValue"] ), [ [ external_Vue_namespaceObject.vModelCheckbox, _ctx.autoDownEnabled, ], ] ), ], 2 ), ]), (0, external_Vue_namespaceObject.createElementVNode)( "h3", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.fieldLabel ), }, (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.download.resolution.label") ), 3 ), (0, external_Vue_namespaceObject.createElementVNode)("p", null, [ ((0, external_Vue_namespaceObject.openBlock)(true), (0, external_Vue_namespaceObject.createElementBlock)( external_Vue_namespaceObject.Fragment, null, (0, external_Vue_namespaceObject.renderList)( _ctx.RESOLUTIONS, (res) => ( (0, external_Vue_namespaceObject.openBlock)(), (0, external_Vue_namespaceObject.createElementBlock)( "label", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.labelInline ), }, [ (0, external_Vue_namespaceObject.withDirectives)( (0, external_Vue_namespaceObject.createElementVNode)( "input", { type: "radio", name: "res", value: res, "onUpdate:modelValue": ($event) => (_ctx.resolution = $event), }, null, 8, ["value", "onUpdate:modelValue"] ), [ [ external_Vue_namespaceObject.vModelRadio, _ctx.resolution, ], ] ), (0, external_Vue_namespaceObject.createTextVNode)( " " + (0, external_Vue_namespaceObject.toDisplayString)(res), 1 ), ], 2 ) ) ), 256 )), ]), (0, external_Vue_namespaceObject.createElementVNode)( "h3", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.fieldLabel ), }, (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.download.filename.label") ), 3 ), (0, external_Vue_namespaceObject.createElementVNode)( "p", { innerHTML: _ctx.$t("s.download.filename.desc") }, null, 8, ["innerHTML"] ), (0, external_Vue_namespaceObject.createElementVNode)( "div", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.keywords ), }, [ (0, external_Vue_namespaceObject.createElementVNode)( "table", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.keywordTable ), }, [ ((0, external_Vue_namespaceObject.openBlock)(true), (0, external_Vue_namespaceObject.createElementBlock)( external_Vue_namespaceObject.Fragment, null, (0, external_Vue_namespaceObject.renderList)( _ctx.FILENAME_KEYWORDS, (kw) => ( (0, external_Vue_namespaceObject.openBlock)(), (0, external_Vue_namespaceObject.createElementBlock)( "tr", null, [ (0, external_Vue_namespaceObject.createElementVNode)( "th", null, (0, external_Vue_namespaceObject.toDisplayString)(kw), 1 ), (0, external_Vue_namespaceObject.createElementVNode)( "td", null, (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t( "s.download.filename.key." + kw.toLowerCase() ) ), 1 ), ] ) ) ), 256 )), ], 2 ), ], 2 ), (0, external_Vue_namespaceObject.createElementVNode)("details", null, [ (0, external_Vue_namespaceObject.createElementVNode)( "summary", null, (0, external_Vue_namespaceObject.toDisplayString)(_ctx.$t("s.extra")), 1 ), (0, external_Vue_namespaceObject.createElementVNode)("p", null, [ (0, external_Vue_namespaceObject.createTextVNode)( (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.download.filename.replace_illegal_char") ) + " ", 1 ), (0, external_Vue_namespaceObject.withDirectives)( (0, external_Vue_namespaceObject.createElementVNode)( "input", { type: "text", "onUpdate:modelValue": ($event) => (_ctx.illegalCharReplacement = $event), }, null, 8, ["onUpdate:modelValue"] ), [ [ external_Vue_namespaceObject.vModelText, _ctx.illegalCharReplacement, ], ] ), (0, external_Vue_namespaceObject.createTextVNode)( " " + (0, external_Vue_namespaceObject.toDisplayString)( "*miku*miku:dance??.mp4 -> " ) + " " + (0, external_Vue_namespaceObject.toDisplayString)( _ctx.sanitizePath( "*miku*miku:dance??.mp4", _ctx.illegalCharReplacement ) ), 1 ), ]), ]), (0, external_Vue_namespaceObject.withDirectives)( (0, external_Vue_namespaceObject.createElementVNode)( "input", { type: "text", "onUpdate:modelValue": ($event) => (_ctx.filenameTemplate = $event), }, null, 8, ["onUpdate:modelValue"] ), [[external_Vue_namespaceObject.vModelText, _ctx.filenameTemplate]] ), (0, external_Vue_namespaceObject.createElementVNode)( "p", null, (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.download.filename.preview") + ": " + _ctx.filenamePreview ), 1 ), (0, external_Vue_namespaceObject.createElementVNode)( "div", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.panel ), }, [ (0, external_Vue_namespaceObject.createElementVNode)("p", null, [ (0, external_Vue_namespaceObject.createElementVNode)( "b", null, (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$tm("s.download.filename.tips")[0] ), 1 ), ]), (0, external_Vue_namespaceObject.createElementVNode)("ul", null, [ ((0, external_Vue_namespaceObject.openBlock)(true), (0, external_Vue_namespaceObject.createElementBlock)( external_Vue_namespaceObject.Fragment, null, (0, external_Vue_namespaceObject.renderList)( _ctx.$tm("s.download.filename.tips").slice(1), (tip) => ( (0, external_Vue_namespaceObject.openBlock)(), (0, external_Vue_namespaceObject.createElementBlock)( "li", null, [ (0, external_Vue_namespaceObject.createElementVNode)( "p", { innerHTML: tip }, null, 8, ["innerHTML"] ), ] ) ) ), 256 )), ]), ], 2 ), ], 2 )) : (0, external_Vue_namespaceObject.createCommentVNode)("v-if", true), _ctx.tabVal === "script" ? ((0, external_Vue_namespaceObject.openBlock)(), (0, external_Vue_namespaceObject.createElementBlock)( "div", { key: 2, class: (0, external_Vue_namespaceObject.normalizeClass)(_ctx.css.view), }, [ (0, external_Vue_namespaceObject.createElementVNode)( "h2", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.sectionHeader ), }, (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.script.label") ), 3 ), (0, external_Vue_namespaceObject.createElementVNode)( "h3", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.fieldLabel ), }, (0, external_Vue_namespaceObject.toDisplayString)( _ctx.$t("s.script.language.label") ), 3 ), (0, external_Vue_namespaceObject.createElementVNode)("p", null, [ ((0, external_Vue_namespaceObject.openBlock)(true), (0, external_Vue_namespaceObject.createElementBlock)( external_Vue_namespaceObject.Fragment, null, (0, external_Vue_namespaceObject.renderList)( _ctx.$i18n.availableLocales, (loc) => ( (0, external_Vue_namespaceObject.openBlock)(), (0, external_Vue_namespaceObject.createElementBlock)( "label", { class: (0, external_Vue_namespaceObject.normalizeClass)( _ctx.css.labelInline ), }, [ (0, external_Vue_namespaceObject.createElementVNode)( "input", { type: "radio", name: "loc", value: loc, checked: _ctx.activeLocale === loc, onChange: ($event) => (_ctx.locale = loc), }, null, 40, ["value", "checked", "onChange"] ), (0, external_Vue_namespaceObject.createTextVNode)( " " + (0, external_Vue_namespaceObject.toDisplayString)( _ctx.localeName(loc) ), 1 ), ], 2 ) ) ), 256 )), ]), ], 2 )) : (0, external_Vue_namespaceObject.createCommentVNode)("v-if", true), ], 10, ["onClick"] )) : (0, external_Vue_namespaceObject.createCommentVNode)("v-if", true), ], 64 ) ) } 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 visible.value && onClickOutside(settingsContainer, () => (visible.value = false)) } settingsContainer.addEventListener("click", onClickContainer) ;(0, external_Vue_namespaceObject.onBeforeUnmount)(() => { settingsContainer.removeEventListener("click", onClickContainer) }) function localeName(loc) { const msg = i18n_i18n.global.getLocaleMessage(loc) return msg?.language || loc } return { css: Settings_module, tabs, tabIndex, tabVal, visible, downloadMode: GM_info.downloadMode, sanitizePath, localeName, ...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 .dropdown--bottomLeft, 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) try { app = (0, external_Vue_namespaceObject.createApp)({ render, setup }) app.use(i18n_i18n) true && // pending fix https://github.com/vuejs/core/pull/5197 // @ts-ignore (unsafeWindow.Vue = Vue) app.mount(settingsContainer) log("Settings view initialized") } catch (e) { log("Settings view init failed: " + (e?.message || e)) app = void 0 settingsContainer.textContent = "E" } } 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() { return $(".page-video__player .video-js").get(0)?.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 { const data = value if (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 (targetOrType === "mousewheel") { 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 = theme === "light" ? -1 : 1 const bodyColor = getComputedStyle(document.body).getPropertyValue("--body") document.body.style.setProperty( "--enh-body-focus", adjustHexColor(bodyColor, adjustmentSign * 15) ) document.body.style.setProperty( "--enh-body-highlight", adjustHexColor(bodyColor, adjustmentSign * 30) ) const darkClass = "enh-dark" theme === "dark" ? document.body.classList.add(darkClass) : 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); } .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); } `)