您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
请参考脚本的主页以获取更多信息
当前为
// ==UserScript== // @name Iwara Enhancement // @name:zh-CN Iwara增强 // @description Please refer to the script's homepage for more information. // @description:zh-CN 请参考脚本的主页以获取更多信息 // @noframes // @grant unsafeWindow // @grant GM_setValue // @grant GM_getValue // @grant GM_download // @grant GM_info // @grant GM_addStyle // @require https://unpkg.com/[email protected] // @require https://unpkg.com/[email protected] // @require https://unpkg.com/[email protected] // @match *://*.iwara.tv/* // @namespace https://github.com/guansss/userscripts // @version 1.0 // @author guansss // @source https://github.com/guansss/userscripts // @runAt document-start // @supportURL https://github.com/guansss/userscripts/issues // ==/UserScript== ;(() => { "use strict" $ const external_Vue_namespaceObject = Vue function onClickOutside(el, callback) { document.addEventListener("click", handler) function handler(e) { if (!el.contains(e.target)) { document.removeEventListener("click", handler) callback(e) } } } /** * MutationObserver that calls callback with just a single mutation. */ class SimpleMutationObserver extends MutationObserver { // since calling `new NodeList()` is illegal, this is the only way to create an empty NodeList constructor(callback) { super((mutations) => { for (const mutation of mutations) if (this.callback(mutation)) break }) this.callback = callback } /** * @param options.immediate - When observing "childList", immediately trigger a mutation with existing nodes. */ observe(target, options) { super.observe(target, options) if (options && options.immediate && options.childList && target.childNodes.length) this.callback({ target, type: "childList", addedNodes: target.childNodes, removedNodes: SimpleMutationObserver.emptyNodeList, }) } } SimpleMutationObserver.emptyNodeList = document.querySelectorAll("#__absolutely_nonexisting") function hasClass(node, className) { return !!node.classList?.contains(className) } let log setLogger(console.log) function setLogger(logger) { log = logger.bind(console, `[${GM_info.script.name}]`) } const external_VueI18n_namespaceObject = VueI18n const en_namespaceObject = JSON.parse( '{"language":"English","name":"Iwara Enhancement","description":"Multiple UI enhancements for better experience.","ui":{"show_list_options":"Show options","hide_list_options":"Hide options"},"s":{"enabled":"Enabled","download":{"label":"Download","auto":{"label":"One-click Download","desc":"Automatically starts download when clicking the download button.","warn":"This feature requires Tampermonkey.","warn_tm":["This feature requires Tampermonkey\'s <b>Browser API</b> download mode, please follow these steps:","Navigate to Tampermonkey\'s settings panel and then the <b>Settings</b> tab.","In <b>General</b> section, set <b>Config Mode</b> to <b>Advanced</b> (or <b>Beginner</b>).","In <b>Downloads BETA</b> section, set <b>Download Mode</b> to <b>Browser API</b>.","In <b>Downloads BETA</b> section, click <b>Save</b>.","Grant permission if requested.","Refresh this page."]},"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"}}},"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"}},"script":{"label":"Script","language":{"label":"Language"}}}}' ) const zh_namespaceObject = JSON.parse( '{"language":"中文","name":"Iwara增强","description":"多种增强体验的界面优化","s":{"enabled":"启用","download":{"label":"下载","auto":{"label":"一键下载","desc":"点击下载按钮时自动开始下载","warn":"该功能仅在 Tampermonkey 中可用","warn_tm":["该功能需要启用 Tampermonkey 的<b>浏览器 API</b>下载模式,请按照以下步骤启用:","进入 Tampermonkey 的设置面板,选择<b>设置</b>标签页","在<b>通用</b>里,设置<b>配置模式</b>为<b>高级<b>(或者<b>初学者</b>)","在<b>下载 BETA</b>里,设置<b>下载模式</b>为<b>浏览器 API</b>","在<b>下载 BETA</b>里,点击<b>保存</b>","如果请求权限的话,选择同意","刷新当前页面"]},"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 的时间戳格式"}}},"ui":{"label":"界面","like_rate":{"label":"喜爱率","desc":"在视频和图片列表里显示喜爱率"},"highlight_threshold":{"label":"高亮分界点","desc":"喜爱率高于此值的视频和图片将会被高亮显示"},"highlight_bg":{"label":"高亮透明度"}},"script":{"label":"脚本","language":{"label":"语言"}}}}' ) /* harmony default export */ const i18n = { zh: zh_namespaceObject, en: en_namespaceObject } function createStorage(prefix, schema) { if (prefix) prefix += "." return { get(key) { return GM_getValue(prefix + key, schema[key]) }, set(key, val) { if ("function" === typeof val) val = val(this.get(key)) GM_setValue(prefix + key, val) }, } } const storage = createStorage("", { v: GM_info.script.version, locale: navigator.language, volume: 0.5, auto_down_enabled: true, preferred_res: "Source", filename: "{DATE} {TITLE} - {AUTHOR} ({ID})", dark: false, like_rates: true, like_rate_highlight: 4, like_rate_highlight_opacity: 0.2, player_size: 100, hide_list_options: false, }) 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" } // shorthand helper making TypeScript happy function localize(message) { // @ts-ignore TS2589: Type instantiation is excessively deep and possibly infinite. return i18n_i18n.global.t(message) } const locale = (0, external_Vue_namespaceObject.ref)(storage.get("locale")) ;(0, external_Vue_namespaceObject.watchEffect)(() => { i18n_i18n.global.locale = locale.value storage.set("locale", locale.value) }) function useConfigSettings() { // locale that will actually be used, with fallback applied const activeLocale = (0, external_Vue_namespaceObject.computed)(() => matchLocale(locale.value)) return { locale, activeLocale } } /* harmony default export */ function mitt(n) { return { all: (n = n || new Map()), on: function (t, e) { var i = n.get(t) i ? i.push(e) : n.set(t, [e]) }, off: function (t, e) { var i = n.get(t) i && (e ? i.splice(i.indexOf(e) >>> 0, 1) : n.set(t, [])) }, emit: function (t, e) { var i = n.get(t) i && i.slice().map(function (n) { n(e) }), (i = n.get("*")) && i.slice().map(function (n) { n(t, e) }) }, } } const ready = new Promise((resolve) => { if ("loading" === document.readyState) document.addEventListener("DOMContentLoaded", () => resolve) else resolve() }) function once(emitter, event, listener) { const fn = (data) => { emitter.off(event, fn) listener(data) } emitter.on(event, fn) return fn } function setupPaging() { ready.then(() => { const appDiv = document.getElementById("app") if (!appDiv) { log("Missing app div.") return } log("Start observing pages.") const appObserver = new SimpleMutationObserver((mutation) => { detectPageChange(appDiv, mutation.addedNodes, "pageEnter") detectPageChange(appDiv, mutation.removedNodes, "pageLeave") }) appObserver.observe(appDiv, { childList: true, immediate: true }) }) } const emitter = mitt() let currentClassName = "" emitter.on("pageEnter", (className) => (currentClassName = className)) const ALL = "*" // page listener for iwara function page(id, enter) { const match = (() => { if (id === ALL) return () => id const ids = "string" === typeof id ? [id] : id const classes = ids.map((id) => `page-${id}`) return (className) => { const split = className.split(" ") const index = classes.findIndex((cls) => split.includes(cls)) return ids[index] } })() function callIfMatch(listener) { return (className) => { const matchedID = match(className) if (void 0 !== matchedID) try { listener(matchedID) } catch (e) { log("Error executing page listener", e) } } } const onPageEnter = callIfMatch((matchedID) => { enter(matchedID, (onLeave) => { once(emitter, "pageLeave", callIfMatch(onLeave)) }) }) emitter.on("pageEnter", onPageEnter) } function detectPageChange(appDiv, nodes, event) { if (nodes.length) // a valid class name will be like "page page-videoList", where "videoList" is the ID for (const node of nodes) if (hasClass(node, "page")) { // sometimes there are two (maybe more) "page" elements, and one of them contains only the "page" class, // we ignore it in this case const hasOtherPageElements = $(appDiv) .children(".page") .filter((_, e) => e !== node).length > 0 if (!hasOtherPageElements) emitter.emit(event, node.className) break } } let reactEventHandlersKey = "" function getReactEventHandlers(element) { if (reactEventHandlersKey) return element[reactEventHandlersKey] for (const key of Object.keys(element)) if (key.startsWith("__reactEventHandlers$")) { reactEventHandlersKey = key return element[key] } } function clamp(val, min, max) { return val < min ? min : val > max ? max : val } function formatDate(date) { let delimiter = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : "/" return [ date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), ] .map((num) => String(num).padStart(2, "0")) .join(delimiter) } function adjustHexColor(color, amount) { return color.replace(/\w\w/g, (color) => clamp(parseInt(color, 16) + amount, 0, 255) .toString(16) .padStart(2, "0") ) } // 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 it returns true. */ function repeat(fn) { let interval = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 200 if (fn()) return 0 const id = setInterval(() => { try { fn() && clearInterval(id) } catch (e) { log(e) clearInterval(id) } }, interval) return id } /** * Periodically calls given function until the return value is truthy. * @returns A CancelablePromise that resolves with the function's return value when truthy. */ function until(fn) { let interval = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : 0 let cancelled = false const promise = new Promise((resolve, reject) => repeat(() => { if (cancelled) return true try { const result = fn() if (result) { resolve(result) // break the repeat() loop return true } } catch (e) { reject(e) return true } }, interval) ) promise.cancel = () => (cancelled = true) return promise } // a partial structure of the video data defined in iwara's video page, // including only the properties we need const FILENAME_KEYWORDS = [ "ID", "TITLE", "RES", "AUTHOR", "DATE", "UP_DATE", "DATE_TS", "UP_DATE_TS", ] const RESOLUTIONS = ["Source", "540p", "360p"] const autoEnabled = "browser" === GM_info.downloadMode ? (0, external_Vue_namespaceObject.ref)(storage.get("auto_down_enabled")) : false const filenameTemplate = (0, external_Vue_namespaceObject.ref)(storage.get("filename")) 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 (${e.message || e})` } }) ;(0, external_Vue_namespaceObject.watchEffect)(() => storage.set("preferred_res", resolution.value) ) ;(0, external_Vue_namespaceObject.watchEffect)(() => storage.set("filename", filenameTemplate.value) ) if ("boolean" !== typeof autoEnabled) { ;(0, external_Vue_namespaceObject.watchEffect)(() => storage.set("auto_down_enabled", autoEnabled.value) ) ;(0, external_Vue_namespaceObject.watchEffect)(() => convertDownloadDropdown(void 0, autoEnabled.value, source.value) ) } function useDownloaderSettings() { return { FILENAME_KEYWORDS, RESOLUTIONS, autoDownEnabled: autoEnabled, resolution, filenameTemplate, filenamePreview: filename, } } page("video", (pageID, onLeave) => { const videoActions = $(".page-video__actions").get(0) if (!videoActions) { log("Could not find video actions.") return } const actionsObserver = new SimpleMutationObserver((mutation) => mutation.addedNodes.forEach((node) => { if (hasClass(node, "dropdown")) { updateVideoInfo(videoActions) updateSources(node) if (autoEnabled && autoEnabled.value) convertDownloadDropdown(node, true, source.value) } }) ) actionsObserver.observe(videoActions, { childList: true, immediate: true }) onLeave(() => { // prevent unexpected downloads hasFreshSources.value = false actionsObserver.disconnect() }) }) function updateVideoInfo(videoActions) { try { // FIXME: reading the prop by a path is quite unreliable, any improvement? const video = getReactEventHandlers(videoActions).children[1].props.video videoInfo.id = video.id videoInfo.title = video.title videoInfo.created = new Date(video.createdAt).getTime() videoInfo.author = video.user.name videoInfo.size = video.file.size } catch (e) { log(e) videoInfo.error = e + "" } } function updateSources(downloadDropdown) { const newSources = $(downloadDropdown) .find(".dropdown__content a") .map(function () { const url = this.href const label = this.innerText return { url, label } }) .get() if (!newSources.length) return sources.splice(0, sources.length, ...newSources) hasFreshSources.value = true } function convertDownloadDropdown(downloadDropdown, enabled, source) { // @ts-ignore The parameter is valid but TS doesn't recognize it const $dropdown = $(downloadDropdown || ".page-video__actions > .dropdown") const $button = $dropdown.find(".downloadButton") const rawButtonText = $button.text().replace(/\s*\(.*\)/, "") if (enabled) { if (!$dropdown.data("converted")) $dropdown .data("converted", true) .on("click", function () { download(this) }) .children(".dropdown__content") .css("display", "none") $button.text(rawButtonText + (source ? ` (${source.label})` : "")) } 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.") if ("browser" !== GM_info.downloadMode) throw new Error(`Invalid download mode "${GM_info.downloadMode}".`) const $downloadButton = $(downloadDropdown).find(".downloadButton") // TODO: properly disable the button $(downloadDropdown).css("pointer-events", "none") $downloadButton.css("background-color", "var(--primary-dark)") const filename = resolveFilename(filenameTemplate.value, source.value) log("Downloading:", filename, source.value.url) if (false); GM_download({ url: source.value.url, name: filename, saveAs: true, onload: () => downloadEnded("onload"), onerror: (e) => downloadEnded("onerror", e), ontimeout: () => downloadEnded("ontimeout"), }) function downloadEnded(type, e) { $(downloadDropdown).css("pointer-events", "") $downloadButton.css("background-color", "") if ("ontimeout" === type) e = { error: "timed_out" } if (e && e.error) { log(e) printDownloadMessage( `Download Error (${e.error}): ${(e.details && e.details.current) || "No info"}` ) } } } catch (e) { log(e) printDownloadMessage(e + "") } } function resolveFilename(template, source) { if (videoInfo.error) throw new Error("Broken video info: " + videoInfo.error) const replacements = { ID: videoInfo.id, TITLE: videoInfo.title, RES: source.label, AUTHOR: videoInfo.author, DATE: formatDate(new Date()), DATE_TS: new Date().getTime() + "", UP_DATE: formatDate(new Date(videoInfo.created)), UP_DATE_TS: videoInfo.created + "", } let basename = template for (const [key, value] of Object.entries(replacements)) basename = basename.replace(new RegExp(`{${key}}`, "g"), value) // strip characters disallowed in file path basename = basename.replace(/[*/:<>?\\|]/g, "") const ext = source.url.slice(source.url.lastIndexOf(".")) return basename + ext } 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" ;(0, external_Vue_namespaceObject.watchEffect)(() => { storage.set("like_rates", likeRateEnabled.value) if (likeRateEnabled.value) document.body.classList.add("enh-show-like-rates") else document.body.classList.remove("enh-show-like-rates") }) ;(0, external_Vue_namespaceObject.watchEffect)(() => { storage.set("like_rate_highlight", highlightThreshold.value) $(".videoTeaser, .imageTeaser") .parent() .each((i, teaser) => processTeaser(teaser)) }) ;(0, external_Vue_namespaceObject.watchEffect)(() => { storage.set("like_rate_highlight_opacity", highlightOpacity.value) document.body.style.setProperty("--ehg-hl-op", highlightOpacity.value + "") }) function useTeaserSettings() { return { likeRateEnabled, highlightThreshold, highlightOpacity } } page(["home", "videoList", "imageList", "subscriptions"], async (pageID, onLeave) => { const teaserObserver = new SimpleMutationObserver((mutation) => mutation.addedNodes.forEach(detectTeaser) ) onLeave(() => { teaserObserver.disconnect() }) const teaserBatcher = new TeaserBatcher() if ("home" === pageID) { const rows = $(".videoTeaser, .imageTeaser").closest(".row") if (!rows.length) { log("Could not find teaser rows.") return } rows.each((i, row) => detectRow(row)) } else { const rowPromise = until(() => $(".videoTeaser, .imageTeaser").closest(".row")[0], 200) onLeave(() => rowPromise.cancel()) detectRow(await rowPromise) } function detectRow(row) { teaserObserver.observe(row, { childList: true, immediate: true }) } function detectTeaser(teaser) { if (isTeaser(teaser)) { teaserBatcher.add(teaser) teaserBatcher.run(processTeaser) } } }) class TeaserBatcher { constructor() { this.teasers = [] this.run = throttle((callback) => { let lastError try { this.teasers.forEach(callback) } catch (e) { // only record the last error so the console won't blow up lastError = e } if (lastError) log("Failed to process teasers", lastError) this.teasers.length = 0 }, 0) } add(teaser) { this.teasers.push(teaser) } } function processTeaser(teaser) { const viewsLabel = $(teaser).find(".views") const likesLabel = $(teaser).find(".likes") let likePercentage const likeRateLabel = viewsLabel.children("." + likeRateClass) if (likeRateLabel.length) likePercentage = +likeRateLabel.text().trim().replace("%", "") else { let [views, likes] = [viewsLabel, likesLabel].map((icon) => { const value = icon.text().trim() return value.includes("k") ? 1e3 * +value.slice(0, -1) : +value }) likePercentage = 0 === views ? 0 : Math.round((1e3 * likes) / views) / 10 // prettier-ignore viewsLabel.children().eq(0).clone().addClass(likeRateClass).text(likePercentage+"%").appendTo(viewsLabel) } if (likePercentage >= highlightThreshold.value) teaser.classList.add("enh-highlight") else teaser.classList.remove("enh-highlight") } function isTeaser(node) { return ( !!node.firstChild && (hasClass(node.firstChild, "videoTeaser") || hasClass(node.firstChild, "imageTeaser")) ) } // 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", warn: "Settings-module__warn--KbCV", } // recommended vscode plugin for syntax highlighting: https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html // language=HTML const template = /* html */ ` <div class='text text--text text--bold'>E</div> <div v-if='visible' :class='css.settings' @click.stop> <header :class='css.header'> <h2 :class='css.title'>{{ $t('name') }} v${GM_info.script.version}</h2> </header> <nav> <ul> <li v-for='tab, i in tabs' :class='{ [css.active]: i === tabIndex }' @click='tabIndex = i' > {{ $t(tab.name) }} </li> </ul> </nav> <div v-if='tabVal === "ui"' :class='css.view'> <h2 :class='css.sectionHeader'>{{ $t('s.ui.label') }}</h2> <h3 :class='css.fieldLabel'>{{ $t('s.ui.like_rate.label') }}</h3> <p v-html='$t("s.ui.like_rate.desc")'></p> <p> <label :class='css.labelBlock'> {{ $t('s.enabled') }} <input type='checkbox' v-model='likeRateEnabled'> </label> </p> <h3 :class='css.fieldLabel'>{{ $t('s.ui.highlight_threshold.label') }}</h3> <p v-html='$t("s.ui.highlight_threshold.desc")'></p> <p> <input type='number' step='0.1' min='0' max='100' :value='highlightThreshold' @change='highlightThreshold = +$event.target.value'> </p> <h3 :class='css.fieldLabel'>{{ $t('s.ui.highlight_bg.label') }}</h3> <p> <input type="range" min="0" max="1" step="0.01" v-model='highlightOpacity'> </p> </div> <div v-else-if='tabVal === "download"' :class='css.view'> <h2 :class='css.sectionHeader'>{{ $t('s.download.label') }}</h2> <h3 :class='css.fieldLabel'>{{ $t('s.download.auto.label') }}</h3> <p v-html='$t("s.download.auto.desc")'></p> <p v-if='!downloadMode' v-html='$t("s.download.auto.warn")'></p> <section v-else-if='downloadMode !== "browser"' :class='css.warn'> <p v-html='$tm("s.download.auto.warn_tm")[0]'></p> <ol> <li v-for='line in $tm("s.download.auto.warn_tm").slice(1)'><p v-html='line'></p></li> </ol> </section> <p> <label :class='[css.labelBlock, { [css.disabled]: downloadMode !== "browser" }]'> {{ $t('s.enabled') }} <input type='checkbox' :disabled='downloadMode !== "browser"' v-model='autoDownEnabled'> </label> </p> <h3 :class='css.fieldLabel'>{{ $t('s.download.resolution.label') }}</h3> <p> <label v-for='res in RESOLUTIONS' :class='css.labelInline'> <input type='radio' name='res' :value='res' v-model='resolution'> {{ res }} </label> </p> <h3 :class='css.fieldLabel'>{{ $t('s.download.filename.label') }}</h3> <p v-html='$t("s.download.filename.desc")'></p> <div :class='css.keywords'> <table :class='css.keywordTable'> <tr v-for='kw in FILENAME_KEYWORDS'> <th>{{ kw }}</th> <td>{{ $t('s.download.filename.key.' + kw.toLowerCase()) }}</td> </tr> </table> </div> <input type='text' v-model='filenameTemplate'> <p>{{ $t('s.download.filename.preview') + ': ' + filenamePreview }}</p> </div> <div v-if='tabVal === "script"' :class='css.view'> <h2 :class='css.sectionHeader'>{{ $t('s.script.label') }}</h2> <h3 :class='css.fieldLabel'>{{ $t('s.script.language.label') }}</h3> <p> <label v-for='loc in $i18n.availableLocales' :class='css.labelBlock'> <input type='radio' name='loc' :value='loc' :checked='activeLocale === loc' @change='locale = loc'> {{ $t('language', loc) }} </label> </p> </div> </div> ` function setup() { const tabs = [ { name: "s.ui.label", val: "ui" }, { name: "s.download.label", val: "download" }, { name: "s.script.label", val: "script" }, ] const tabIndex = (0, external_Vue_namespaceObject.ref)(0) const tabVal = (0, external_Vue_namespaceObject.computed)( () => tabs[tabIndex.value] && tabs[tabIndex.value].val ) const visible = (0, external_Vue_namespaceObject.ref)(false) const onClickContainer = () => { visible.value = !visible.value if (visible.value) onClickOutside(settingsContainer, () => (visible.value = false)) } settingsContainer.addEventListener("click", onClickContainer) ;(0, external_Vue_namespaceObject.onBeforeUnmount)(() => { settingsContainer.removeEventListener("click", onClickContainer) }) return { css: Settings_module, tabs, tabIndex, tabVal, visible, downloadMode: GM_info.downloadMode, ...useDownloaderSettings(), ...useConfigSettings(), ...useTeaserSettings(), } } const SETTINGS_ID = "enh-settings" const settingsContainer = $( `<div id="${SETTINGS_ID}" class='header__link ${Settings_module["switch"]}'></div>` )[0] let app page(ALL, (pageID, onLeave) => { const destination = $(".page .header__content:first-of-type .dropdown:last-of-type")[0] if (destination) { // destination element will be destroyed everytime the page changes, // so we need to insert the container after every page change destination.before(settingsContainer) // lazy-init the app if (!app) { app = (0, external_Vue_namespaceObject.createApp)({ template, setup }) app.use(i18n_i18n) if (true) // pending fix https://github.com/vuejs/core/pull/5197 // @ts-ignore unsafeWindow.Vue = Vue app.mount(settingsContainer) log("Settings view initialized") } } else log("Could not insert settings view: container not found.") }) // prevent Sentry from tracking the logging setLogger(console.log.__sentry_original__ || console.log) const toggleButtonID = "enh-hide-options-btn" page(["videoList", "imageList"], (pageID, onLeave) => { const hideOptions = (0, external_Vue_namespaceObject.ref)(storage.get("hide_list_options")) const toggleText = (0, external_Vue_namespaceObject.computed)(() => localize(hideOptions.value ? "ui.show_list_options" : "ui.hide_list_options") ) const optionsContainer = $(".sortFilter").eq(0).closest(".col-lg-3") const toggleButton = $( `<button id="${toggleButtonID}" class="button button--primary button--ghost d-lg-none" type="button"></button>` ) .insertBefore(optionsContainer) .on("click", () => (hideOptions.value = !hideOptions.value)) ;(0, external_Vue_namespaceObject.watchEffect)(() => { storage.set("hide_list_options", hideOptions.value) optionsContainer.toggleClass("d-none", hideOptions.value) }) ;(0, external_Vue_namespaceObject.watchEffect)(() => { toggleButton.text(toggleText.value) }) }) const state = (0, external_Vue_namespaceObject.reactive)({ theme: "light" }) setInterval(() => { state.theme = localStorage.theme }, 1e3) ;(0, external_Vue_namespaceObject.watchEffect)(updateTheme) function updateTheme() { const theme = state.theme const adjustmentSign = "light" === theme ? -1 : 1 const bodyColor = getComputedStyle(document.body).getPropertyValue("--body") document.body.style.setProperty( "--enh-body-focus", adjustHexColor(bodyColor, 15 * adjustmentSign) ) document.body.style.setProperty( "--enh-body-highlight", adjustHexColor(bodyColor, 30 * adjustmentSign) ) } 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; 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 p { color: var(--muted); } .Settings-module__settings--alpJ p, .Settings-module__settings--alpJ section { margin-bottom: 8px; } .Settings-module__settings--alpJ a { font-weight: bold; cursor: pointer; } .Settings-module__settings--alpJ ol { padding-left: 20px; } .Settings-module__settings--alpJ table { margin: 16px 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 { cursor: pointer; } .Settings-module__settings--alpJ label:hover { background: var(--enh-body-focus); } .Settings-module__settings--alpJ label input { cursor: pointer; } .Settings-module__settings--alpJ label.Settings-module__disabled--vvjv { cursor: not-allowed; } .Settings-module__settings--alpJ label.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-bottom: 16px; 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[type="checkbox"] { margin-left: auto; } .Settings-module__label-inline--v3DK { 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__warn--KbCV { padding: 8px; background-color: #594c00; } .enh-body { --ehg-hl-op: 0.2; } #enh-settings { position: relative; } #enh-settings * { box-sizing: border-box; } .enh-like-rate { display: none; } .enh-show-like-rates .videoTeaser .views .text, .enh-show-like-rates .imageTeaser .views .text { display: none; } .enh-show-like-rates .videoTeaser .enh-like-rate.text, .enh-show-like-rates .imageTeaser .enh-like-rate.text { display: block; } .enh-highlight:before { content: ''; position: absolute; z-index: -1; top: -15px; bottom: 0; left: 0; right: 0; background: var(--primary); opacity: var(--ehg-hl-op); } `)