// ==UserScript== // @name V2EX.REP - 专注提升 V2EX 主题回复浏览体验 // @name:zh-CN V2EX.REP - 专注提升 V2EX 主题回复浏览体验 // @namespace https://github.com/v2hot/v2ex.rep // @homepageURL https://github.com/v2hot/v2ex.rep#readme // @supportURL https://github.com/v2hot/v2ex.rep/issues // @version 1.5.4 // @description 专注提升 V2EX 主题回复浏览体验的浏览器扩展/用户脚本。主要功能有 ✅ 修复有被 block 的用户时错位的楼层号;✅ 回复时自动带上楼层号;✅ 显示热门回复;✅ 显示被引用的回复;✅ 查看用户在当前主题下的所有回复与被提及的回复;✅ 自动预加载所有分页,支持解析显示跨页面引用;✅ 回复时上传图片;✅ 无感自动签到;✅ 懒加载用户头像图片;✅ 一直显示感谢按钮 🙏;✅ 一直显示隐藏回复按钮 🙈;✅ 快速发送感谢/快速隐藏回复(no confirm)等。 // @description:zh-CN 专注提升 V2EX 主题回复浏览体验的浏览器扩展/用户脚本。主要功能有 ✅ 修复有被 block 的用户时错位的楼层号;✅ 回复时自动带上楼层号;✅ 显示热门回复;✅ 显示被引用的回复;✅ 查看用户在当前主题下的所有回复与被提及的回复;✅ 自动预加载所有分页,支持解析显示跨页面引用;✅ 回复时上传图片;✅ 无感自动签到;✅ 懒加载用户头像图片;✅ 一直显示感谢按钮 🙏;✅ 一直显示隐藏回复按钮 🙈;✅ 快速发送感谢/快速隐藏回复(no confirm)等。 // @icon https://www.v2ex.com/favicon.ico // @author Pipecraft // @license MIT // @match https://*.v2ex.com/* // @run-at document-start // @grant GM.getValue // @grant GM.setValue // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM_addElement // @grant GM.registerMenuCommand // @downloadURL https://update.greasyfork.icu/scripts/466589/V2EXREP%20-%20%E4%B8%93%E6%B3%A8%E6%8F%90%E5%8D%87%20V2EX%20%E4%B8%BB%E9%A2%98%E5%9B%9E%E5%A4%8D%E6%B5%8F%E8%A7%88%E4%BD%93%E9%AA%8C.user.js // @updateURL https://update.greasyfork.icu/scripts/466589/V2EXREP%20-%20%E4%B8%93%E6%B3%A8%E6%8F%90%E5%8D%87%20V2EX%20%E4%B8%BB%E9%A2%98%E5%9B%9E%E5%A4%8D%E6%B5%8F%E8%A7%88%E4%BD%93%E9%AA%8C.meta.js // ==/UserScript== // ;(() => { "use strict" var listeners = {} var getValue = async (key) => { const value = await GM.getValue(key) return value && value !== "undefined" ? JSON.parse(value) : void 0 } var setValue = async (key, value) => { if (value !== void 0) { const newValue = JSON.stringify(value) if (listeners[key]) { const oldValue = await GM.getValue(key) await GM.setValue(key, newValue) if (newValue !== oldValue) { for (const func of listeners[key]) { func(key, oldValue, newValue) } } } else { await GM.setValue(key, newValue) } } } var _addValueChangeListener = (key, func) => { listeners[key] = listeners[key] || [] listeners[key].push(func) return () => { if (listeners[key] && listeners[key].length > 0) { for (let i2 = listeners[key].length - 1; i2 >= 0; i2--) { if (listeners[key][i2] === func) { listeners[key].splice(i2, 1) } } } } } var addValueChangeListener = (key, func) => { if (typeof GM_addValueChangeListener !== "function") { console.warn("Do not support GM_addValueChangeListener!") return _addValueChangeListener(key, func) } const listenerId = GM_addValueChangeListener(key, func) return () => { GM_removeValueChangeListener(listenerId) } } var doc = document var win = window if (typeof String.prototype.replaceAll !== "function") { String.prototype.replaceAll = String.prototype.replace } var $ = (selectors, element) => (element || doc).querySelector(selectors) var $$ = (selectors, element) => [ ...(element || doc).querySelectorAll(selectors), ] var getRootElement = (type) => type === 1 ? doc.head || doc.body || doc.documentElement : type === 2 ? doc.body || doc.documentElement : doc.documentElement var createElement = (tagName, attributes) => setAttributes(doc.createElement(tagName), attributes) var addElement = (parentNode, tagName, attributes) => { if (typeof parentNode === "string") { return addElement(null, parentNode, tagName) } if (!tagName) { return } if (!parentNode) { parentNode = /^(script|link|style|meta)$/.test(tagName) ? getRootElement(1) : getRootElement(2) } if (typeof tagName === "string") { const element = createElement(tagName, attributes) parentNode.append(element) return element } setAttributes(tagName, attributes) parentNode.append(tagName) return tagName } var addEventListener = (element, type, listener, options) => { if (!element) { return } if (typeof type === "object") { for (const type1 in type) { if (Object.hasOwn(type, type1)) { element.addEventListener(type1, type[type1]) } } } else if (typeof type === "string" && typeof listener === "function") { element.addEventListener(type, listener, options) } } var removeEventListener = (element, type, listener, options) => { if (!element) { return } if (typeof type === "object") { for (const type1 in type) { if (Object.hasOwn(type, type1)) { element.removeEventListener(type1, type[type1]) } } } else if (typeof type === "string" && typeof listener === "function") { element.removeEventListener(type, listener, options) } } var getAttribute = (element, name) => element ? element.getAttribute(name) : null var setAttribute = (element, name, value) => element ? element.setAttribute(name, value) : void 0 var setAttributes = (element, attributes) => { if (element && attributes) { for (const name in attributes) { if (Object.hasOwn(attributes, name)) { const value = attributes[name] if (value === void 0) { continue } if (/^(value|textContent|innerText)$/.test(name)) { element[name] = value } else if (/^(innerHTML)$/.test(name)) { element[name] = createHTML(value) } else if (name === "style") { setStyle(element, value, true) } else if (/on\w+/.test(name)) { const type = name.slice(2) addEventListener(element, type, value) } else { setAttribute(element, name, value) } } } } return element } var addAttribute = (element, name, value) => { const orgValue = getAttribute(element, name) if (!orgValue) { setAttribute(element, name, value) } else if (!orgValue.includes(value)) { setAttribute(element, name, orgValue + " " + value) } } var addClass = (element, className) => { if (!element || !element.classList) { return } element.classList.add(className) } var removeClass = (element, className) => { if (!element || !element.classList) { return } element.classList.remove(className) } var hasClass = (element, className) => { if (!element || !element.classList) { return false } return element.classList.contains(className) } var setStyle = (element, values, overwrite) => { if (!element) { return } const style = element.style if (typeof values === "string") { style.cssText = overwrite ? values : style.cssText + ";" + values return } if (overwrite) { style.cssText = "" } for (const key in values) { if (Object.hasOwn(values, key)) { style[key] = values[key].replace("!important", "") } } } var throttle = (func, interval) => { let timeoutId2 = null let next = false const handler = (...args) => { if (timeoutId2) { next = true } else { func.apply(void 0, args) timeoutId2 = setTimeout(() => { timeoutId2 = null if (next) { next = false handler() } }, interval) } } return handler } if (typeof Object.hasOwn !== "function") { Object.hasOwn = (instance, prop) => Object.prototype.hasOwnProperty.call(instance, prop) } var actionHref = "javascript:;" var getOffsetPosition = (element, referElement) => { const position = { top: 0, left: 0 } referElement = referElement || doc.body while (element && element !== referElement) { position.top += element.offsetTop position.left += element.offsetLeft element = element.offsetParent } return position } var runOnceCache = {} var runOnce = async (key, func) => { if (Object.hasOwn(runOnceCache, key)) { return runOnceCache[key] } const result = await func() if (key) { runOnceCache[key] = result } return result } var cacheStore = {} var makeKey = (key) => (Array.isArray(key) ? key.join(":") : key) var cache = { get: (key) => cacheStore[makeKey(key)], add(key, value) { cacheStore[makeKey(key)] = value }, } var sleep = async (time) => { return new Promise((resolve) => { setTimeout(() => { resolve(1) }, time) }) } var parseInt10 = (number, defaultValue) => { if (typeof number === "number" && !Number.isNaN(number)) { return number } if (typeof defaultValue !== "number") { defaultValue = Number.NaN } if (!number) { return defaultValue } const result = Number.parseInt(number, 10) return Number.isNaN(result) ? defaultValue : result } var rootFuncArray = [] var headFuncArray = [] var bodyFuncArray = [] var headBodyObserver var startObserveHeadBodyExists = () => { if (headBodyObserver) { return } headBodyObserver = new MutationObserver(() => { if (doc.head && doc.body) { headBodyObserver.disconnect() } if (doc.documentElement && rootFuncArray.length > 0) { for (const func of rootFuncArray) { func() } rootFuncArray.length = 0 } if (doc.head && headFuncArray.length > 0) { for (const func of headFuncArray) { func() } headFuncArray.length = 0 } if (doc.body && bodyFuncArray.length > 0) { for (const func of bodyFuncArray) { func() } bodyFuncArray.length = 0 } }) headBodyObserver.observe(doc, { childList: true, subtree: true, }) } var runWhenHeadExists = (func) => { if (!doc.head) { headFuncArray.push(func) startObserveHeadBodyExists() return } func() } var runWhenBodyExists = (func) => { if (!doc.body) { bodyFuncArray.push(func) startObserveHeadBodyExists() return } func() } var runWhenDomReady = (func) => { if (doc.readyState === "interactive" || doc.readyState === "complete") { return func() } const handler = () => { if (doc.readyState === "interactive" || doc.readyState === "complete") { func() removeEventListener(doc, "readystatechange", handler) } } addEventListener(doc, "readystatechange", handler) } var isVisible = (element) => { if (typeof element.checkVisibility === "function") { return element.checkVisibility() } return element.offsetParent !== null } var isTouchScreen = () => "ontouchstart" in win var escapeHTMLPolicy = typeof trustedTypes !== "undefined" && typeof trustedTypes.createPolicy === "function" ? trustedTypes.createPolicy("beuEscapePolicy", { createHTML: (string) => string, }) : void 0 var createHTML = (html) => { return escapeHTMLPolicy ? escapeHTMLPolicy.createHTML(html) : html } var addElement2 = typeof GM_addElement === "function" ? (parentNode, tagName, attributes) => { if (typeof parentNode === "string") { return addElement2(null, parentNode, tagName) } if (!tagName) { return } if (!parentNode) { parentNode = /^(script|link|style|meta)$/.test(tagName) ? getRootElement(1) : getRootElement(2) } if (typeof tagName === "string") { let attributes2 if (attributes) { const entries1 = [] const entries2 = [] for (const entry of Object.entries(attributes)) { if (/^(on\w+|innerHTML)$/.test(entry[0])) { entries2.push(entry) } else { entries1.push(entry) } } attributes = Object.fromEntries(entries1) attributes2 = Object.fromEntries(entries2) } const element = GM_addElement(null, tagName, attributes) setAttributes(element, attributes2) parentNode.append(element) return element } setAttributes(tagName, attributes) parentNode.append(tagName) return tagName } : addElement var addStyle = (styleText) => addElement2(null, "style", { textContent: styleText }) var registerMenuCommand = (name, callback, accessKey) => { if (window !== top) { return } if (typeof GM.registerMenuCommand !== "function") { console.warn("Do not support GM.registerMenuCommand!") return } GM.registerMenuCommand(name, callback, accessKey) } var style_default = '#browser_extension_settings_container{--browser-extension-settings-background-color: #f2f2f7;--browser-extension-settings-text-color: #444444;--browser-extension-settings-link-color: #217dfc;--sb-track-color: #00000000;--sb-thumb-color: #33334480;--sb-size: 2px;--font-family: "helvetica neue", "microsoft yahei", arial, sans-serif;position:fixed;top:10px;right:30px;max-height:90%;height:600px;overflow:hidden;display:none;z-index:100000;border-radius:5px;-webkit-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);-moz-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);box-shadow:0px 10px 39px 10px rgba(62,66,66,.22) !important}#browser_extension_settings_container .browser_extension_settings_wrapper{display:flex;height:100%;overflow:hidden;background-color:var(--browser-extension-settings-background-color);font-family:var(--font-family)}#browser_extension_settings_container .browser_extension_settings_wrapper h1,#browser_extension_settings_container .browser_extension_settings_wrapper h2{border:none;color:var(--browser-extension-settings-text-color);padding:0;font-family:var(--font-family);line-height:normal;letter-spacing:normal}#browser_extension_settings_container .browser_extension_settings_wrapper h1{font-size:26px;font-weight:800;margin:18px 0}#browser_extension_settings_container .browser_extension_settings_wrapper h2{font-size:18px;font-weight:600;margin:14px 0}#browser_extension_settings_container .browser_extension_settings_wrapper footer{display:flex;justify-content:center;flex-direction:column;font-size:11px;margin:10px auto 0px;background-color:var(--browser-extension-settings-background-color);color:var(--browser-extension-settings-text-color);font-family:var(--font-family)}#browser_extension_settings_container .browser_extension_settings_wrapper footer a{color:var(--browser-extension-settings-link-color) !important;font-family:var(--font-family);text-decoration:none;padding:0}#browser_extension_settings_container .browser_extension_settings_wrapper footer p{text-align:center;padding:0;margin:2px;line-height:13px;font-size:11px;color:var(--browser-extension-settings-text-color);font-family:var(--font-family)}#browser_extension_settings_container .browser_extension_settings_wrapper a.navigation_go_previous{color:var(--browser-extension-settings-link-color);cursor:pointer;display:none}#browser_extension_settings_container .browser_extension_settings_wrapper a.navigation_go_previous::before{content:"< "}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container{overflow-x:auto;box-sizing:border-box;padding:10px 15px;background-color:var(--browser-extension-settings-background-color);color:var(--browser-extension-settings-text-color)}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div{background-color:#fff;font-size:14px;border-top:1px solid #ccc;padding:6px 15px 6px 15px}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a:visited,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a:visited{display:flex;justify-content:space-between;align-items:center;cursor:pointer;text-decoration:none;color:var(--browser-extension-settings-text-color);font-family:var(--font-family)}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a:hover,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a:visited:hover,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a:hover,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a:visited:hover{text-decoration:none;color:var(--browser-extension-settings-text-color)}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a span,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a:visited span,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a span,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a:visited span{margin-right:10px;line-height:24px;font-family:var(--font-family)}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div.active,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div:hover,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div.active,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div:hover{background-color:#e4e4e6}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div.active a,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div.active a{cursor:default}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div:first-of-type,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div:first-of-type{border-top:none;border-top-right-radius:10px;border-top-left-radius:10px}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div:last-of-type,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div:last-of-type{border-bottom-right-radius:10px;border-bottom-left-radius:10px}#browser_extension_settings_container .thin_scrollbar{scrollbar-color:var(--sb-thumb-color) var(--sb-track-color);scrollbar-width:thin}#browser_extension_settings_container .thin_scrollbar::-webkit-scrollbar{width:var(--sb-size)}#browser_extension_settings_container .thin_scrollbar::-webkit-scrollbar-track{background:var(--sb-track-color);border-radius:10px}#browser_extension_settings_container .thin_scrollbar::-webkit-scrollbar-thumb{background:var(--sb-thumb-color);border-radius:10px}#browser_extension_settings_main{min-width:250px;overflow-y:auto;overflow-x:hidden;box-sizing:border-box;padding:10px 15px;background-color:var(--browser-extension-settings-background-color);color:var(--browser-extension-settings-text-color);font-family:var(--font-family)}#browser_extension_settings_main h2{text-align:center;margin:5px 0 0}#browser_extension_settings_main .option_groups{background-color:#fff;padding:6px 15px 6px 15px;border-radius:10px;display:flex;flex-direction:column;margin:10px 0 0}#browser_extension_settings_main .option_groups .action{font-size:14px;padding:6px 0 6px 0;color:var(--browser-extension-settings-link-color);cursor:pointer}#browser_extension_settings_main .bes_external_link{font-size:14px;padding:6px 0 6px 0}#browser_extension_settings_main .bes_external_link a,#browser_extension_settings_main .bes_external_link a:visited,#browser_extension_settings_main .bes_external_link a:hover{color:var(--browser-extension-settings-link-color);font-family:var(--font-family);text-decoration:none;cursor:pointer}#browser_extension_settings_main .option_groups textarea{font-size:12px;margin:10px 0 10px 0;height:100px;width:100%;border:1px solid #a9a9a9;border-radius:4px;box-sizing:border-box}#browser_extension_settings_main .switch_option,#browser_extension_settings_main .select_option{display:flex;justify-content:space-between;align-items:center;padding:6px 0 6px 0;font-size:14px}#browser_extension_settings_main .option_groups>*{border-top:1px solid #ccc}#browser_extension_settings_main .option_groups>*:first-child{border-top:none}#browser_extension_settings_main .bes_option>.bes_icon{width:24px;height:24px;margin-right:10px}#browser_extension_settings_main .bes_option>.bes_title{margin-right:10px;flex-grow:1}#browser_extension_settings_main .bes_option>.bes_select{box-sizing:border-box;background-color:#fff;height:24px;padding:0 2px 0 2px;margin:0;border-radius:6px;border:1px solid #ccc}#browser_extension_settings_main .option_groups .bes_tip{position:relative;margin:0;padding:0 15px 0 0;border:none;max-width:none;font-size:14px}#browser_extension_settings_main .option_groups .bes_tip .bes_tip_anchor{cursor:help;text-decoration:underline}#browser_extension_settings_main .option_groups .bes_tip .bes_tip_content{position:absolute;bottom:15px;left:0;background-color:#fff;color:var(--browser-extension-settings-text-color);text-align:left;padding:10px;display:none;border-radius:5px;-webkit-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);-moz-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);box-shadow:0px 10px 39px 10px rgba(62,66,66,.22) !important}#browser_extension_settings_main .option_groups .bes_tip .bes_tip_anchor:hover+.bes_tip_content,#browser_extension_settings_main .option_groups .bes_tip .bes_tip_content:hover{display:block}#browser_extension_settings_main .option_groups .bes_tip p,#browser_extension_settings_main .option_groups .bes_tip pre{margin:revert;padding:revert}#browser_extension_settings_main .option_groups .bes_tip pre{font-family:Consolas,panic sans,bitstream vera sans mono,Menlo,microsoft yahei,monospace;font-size:13px;letter-spacing:.015em;line-height:120%;white-space:pre;overflow:auto;background-color:#f5f5f5;word-break:normal;overflow-wrap:normal;padding:.5em;border:none}#browser_extension_settings_main .container{--button-width: 51px;--button-height: 24px;--toggle-diameter: 20px;--color-off: #e9e9eb;--color-on: #34c759;width:var(--button-width);height:var(--button-height);position:relative;padding:0;margin:0;flex:none;user-select:none}#browser_extension_settings_main input[type=checkbox]{opacity:0;width:0;height:0;position:absolute}#browser_extension_settings_main .switch{width:100%;height:100%;display:block;background-color:var(--color-off);border-radius:calc(var(--button-height)/2);border:none;cursor:pointer;transition:all .2s ease-out}#browser_extension_settings_main .switch::before{display:none}#browser_extension_settings_main .slider{width:var(--toggle-diameter);height:var(--toggle-diameter);position:absolute;left:2px;top:calc(50% - var(--toggle-diameter)/2);border-radius:50%;background:#fff;box-shadow:0px 3px 8px rgba(0,0,0,.15),0px 3px 1px rgba(0,0,0,.06);transition:all .2s ease-out;cursor:pointer}#browser_extension_settings_main input[type=checkbox]:checked+.switch{background-color:var(--color-on)}#browser_extension_settings_main input[type=checkbox]:checked+.switch .slider{left:calc(var(--button-width) - var(--toggle-diameter) - 2px)}#browser_extension_side_menu{min-height:80px;width:30px;opacity:0;position:fixed;top:80px;right:0;padding-top:20px;z-index:10000}#browser_extension_side_menu:hover{opacity:1}#browser_extension_side_menu button{cursor:pointer;width:24px;height:24px;padding:0;border:none;background-color:rgba(0,0,0,0);background-image:none}#browser_extension_side_menu button svg{width:24px;height:24px}#browser_extension_side_menu button:hover{opacity:70%}#browser_extension_side_menu button:active{opacity:100%}@media(max-width: 500px){#browser_extension_settings_container{right:10px}#browser_extension_settings_container .extension_list_container{display:none}#browser_extension_settings_container .extension_list_container.bes_active{display:block}#browser_extension_settings_container .extension_list_container.bes_active+div{display:none}#browser_extension_settings_main a.navigation_go_previous{display:block}}' function createSwitch(options = {}) { const container = createElement("label", { class: "container" }) const checkbox = createElement( "input", options.checked ? { type: "checkbox", checked: "" } : { type: "checkbox" } ) addElement2(container, checkbox) const switchElm = createElement("span", { class: "switch" }) addElement2(switchElm, "span", { class: "slider" }) addElement2(container, switchElm) if (options.onchange) { addEventListener(checkbox, "change", options.onchange) } return container } function createSwitchOption(icon, text, options) { if (typeof text !== "string") { return createSwitchOption(void 0, icon, text) } const div = createElement("div", { class: "switch_option bes_option" }) if (icon) { addElement2(div, "img", { src: icon, class: "bes_icon" }) } addElement2(div, "span", { textContent: text, class: "bes_title" }) div.append(createSwitch(options)) return div } var besVersion = 51 var openButton = '' var openInNewTabButton = '' var settingButton = '\n\n' function initI18n(messageMaps, language) { language = (language || navigator.language).toLowerCase() const language2 = language.slice(0, 2) let messagesDefault let messagesLocal for (const entry of Object.entries(messageMaps)) { const langs = new Set( entry[0] .toLowerCase() .split(",") .map((v) => v.trim()) ) const value = entry[1] if (langs.has(language)) { messagesLocal = value } if (langs.has(language2) && !messagesLocal) { messagesLocal = value } if (langs.has("en")) { messagesDefault = value } if (langs.has("en-us") && !messagesDefault) { messagesDefault = value } } if (!messagesLocal) { messagesLocal = {} } if (!messagesDefault || messagesDefault === messagesLocal) { messagesDefault = {} } return function (key, ...parameters) { let text = messagesLocal[key] || messagesDefault[key] || key if (parameters && parameters.length > 0 && text !== key) { for (let i2 = 0; i2 < parameters.length; i2++) { text = text.replaceAll( new RegExp("\\{".concat(i2 + 1, "\\}"), "g"), String(parameters[i2]) ) } } return text } } var messages = { "settings.title": "Settings", "settings.otherExtensions": "Other Extensions", "settings.displaySettingsButtonInSideMenu": "Display Settings Button in Side Menu", "settings.menu.settings": "\u2699\uFE0F Settings", "settings.extensions.utags.title": "\u{1F3F7}\uFE0F UTags - Add usertags to links", "settings.extensions.links-helper.title": "\u{1F517} Links Helper", "settings.extensions.v2ex.rep.title": "V2EX.REP - \u4E13\u6CE8\u63D0\u5347 V2EX \u4E3B\u9898\u56DE\u590D\u6D4F\u89C8\u4F53\u9A8C", "settings.extensions.v2ex.min.title": "v2ex.min - V2EX Minimalist (\u6781\u7B80\u98CE\u683C)", "settings.extensions.replace-ugly-avatars.title": "Replace Ugly Avatars", "settings.extensions.more-by-pipecraft.title": "Find more useful userscripts", } var en_default = messages var messages2 = { "settings.title": "\u8BBE\u7F6E", "settings.otherExtensions": "\u5176\u4ED6\u6269\u5C55", "settings.displaySettingsButtonInSideMenu": "\u5728\u4FA7\u8FB9\u680F\u83DC\u5355\u4E2D\u663E\u793A\u8BBE\u7F6E\u6309\u94AE", "settings.menu.settings": "\u2699\uFE0F \u8BBE\u7F6E", "settings.extensions.utags.title": "\u{1F3F7}\uFE0F \u5C0F\u9C7C\u6807\u7B7E (UTags) - \u4E3A\u94FE\u63A5\u6DFB\u52A0\u7528\u6237\u6807\u7B7E", "settings.extensions.links-helper.title": "\u{1F517} \u94FE\u63A5\u52A9\u624B", "settings.extensions.v2ex.rep.title": "V2EX.REP - \u4E13\u6CE8\u63D0\u5347 V2EX \u4E3B\u9898\u56DE\u590D\u6D4F\u89C8\u4F53\u9A8C", "settings.extensions.v2ex.min.title": "v2ex.min - V2EX \u6781\u7B80\u98CE\u683C", "settings.extensions.replace-ugly-avatars.title": "\u8D50\u4F60\u4E2A\u5934\u50CF\u5427", "settings.extensions.more-by-pipecraft.title": "\u66F4\u591A\u6709\u8DA3\u7684\u811A\u672C", } var zh_cn_default = messages2 var i = initI18n({ "en,en-US": en_default, "zh,zh-CN": zh_cn_default, }) var lang = navigator.language var locale if (lang === "zh-TW" || lang === "zh-HK") { locale = "zh-TW" } else if (lang.includes("zh")) { locale = "zh-CN" } else { locale = "en" } var relatedExtensions = [ { id: "utags", title: i("settings.extensions.utags.title"), url: "https://greasyfork.org/".concat( locale, "/scripts/460718-utags-add-usertags-to-links" ), }, { id: "links-helper", title: i("settings.extensions.links-helper.title"), description: "\u5728\u65B0\u6807\u7B7E\u9875\u4E2D\u6253\u5F00\u7B2C\u4E09\u65B9\u7F51\u7AD9\u94FE\u63A5\uFF0C\u56FE\u7247\u94FE\u63A5\u8F6C\u56FE\u7247\u6807\u7B7E\u7B49", url: "https://greasyfork.org/".concat( locale, "/scripts/464541-links-helper" ), }, { id: "v2ex.rep", title: i("settings.extensions.v2ex.rep.title"), url: "https://greasyfork.org/".concat( locale, "/scripts/466589-v2ex-rep-%E4%B8%93%E6%B3%A8%E6%8F%90%E5%8D%87-v2ex-%E4%B8%BB%E9%A2%98%E5%9B%9E%E5%A4%8D%E6%B5%8F%E8%A7%88%E4%BD%93%E9%AA%8C" ), }, { id: "v2ex.min", title: i("settings.extensions.v2ex.min.title"), url: "https://greasyfork.org/".concat( locale, "/scripts/463552-v2ex-min-v2ex-%E6%9E%81%E7%AE%80%E9%A3%8E%E6%A0%BC" ), }, { id: "replace-ugly-avatars", title: i("settings.extensions.replace-ugly-avatars.title"), url: "https://greasyfork.org/".concat( locale, "/scripts/472616-replace-ugly-avatars" ), }, { id: "more-by-pipecraft", title: i("settings.extensions.more-by-pipecraft.title"), url: "https://greasyfork.org/".concat(locale, "/users/1030884-pipecraft"), }, ] var getInstalledExtesionList = () => { return $(".extension_list_container .installed_extension_list") } var getRelatedExtesionList = () => { return $(".extension_list_container .related_extension_list") } var isInstalledExtension = (id) => { const list = getInstalledExtesionList() if (!list) { return false } const installed = $('[data-extension-id="'.concat(id, '"]'), list) return Boolean(installed) } var addCurrentExtension = (extension) => { const list = getInstalledExtesionList() if (!list) { return } if (isInstalledExtension(extension.id)) { return } const element = createInstalledExtension(extension) list.append(element) const list2 = getRelatedExtesionList() if (list2) { updateRelatedExtensions(list2) } } var activeExtension = (id) => { const list = getInstalledExtesionList() if (!list) { return false } for (const element of $$(".active", list)) { removeClass(element, "active") } const installed = $('[data-extension-id="'.concat(id, '"]'), list) if (installed) { addClass(installed, "active") } } var activeExtensionList = () => { const extensionListContainer = $(".extension_list_container") if (extensionListContainer) { addClass(extensionListContainer, "bes_active") } } var deactiveExtensionList = () => { const extensionListContainer = $(".extension_list_container") if (extensionListContainer) { removeClass(extensionListContainer, "bes_active") } } var createInstalledExtension = (installedExtension) => { const div = createElement("div", { class: "installed_extension", "data-extension-id": installedExtension.id, }) const a = addElement2(div, "a", { onclick: installedExtension.onclick, }) addElement2(a, "span", { textContent: installedExtension.title, }) const svg = addElement2(a, "svg") svg.outerHTML = createHTML(openButton) return div } var updateRelatedExtensions = (container) => { const relatedExtensionElements = $$("[data-extension-id]", container) if (relatedExtensionElements.length > 0) { for (const relatedExtensionElement of relatedExtensionElements) { if ( isInstalledExtension( relatedExtensionElement.dataset.extensionId || "noid" ) ) { relatedExtensionElement.remove() } } } else { container.innerHTML = createHTML("") } for (const relatedExtension of relatedExtensions) { if ( isInstalledExtension(relatedExtension.id) || $('[data-extension-id="'.concat(relatedExtension.id, '"]'), container) ) { continue } if ($$("[data-extension-id]", container).length >= 4) { return } const div4 = addElement2(container, "div", { class: "related_extension", "data-extension-id": relatedExtension.id, }) const a = addElement2(div4, "a", { href: relatedExtension.url, target: "_blank", }) addElement2(a, "span", { textContent: relatedExtension.title, }) const svg = addElement2(a, "svg") svg.outerHTML = createHTML(openInNewTabButton) } } function createExtensionList(installedExtensions) { const div = createElement("div", { class: "extension_list_container thin_scrollbar", }) addElement2(div, "h1", { textContent: i("settings.title") }) const div2 = addElement2(div, "div", { class: "installed_extension_list", }) for (const installedExtension of installedExtensions) { if (isInstalledExtension(installedExtension.id)) { continue } const element = createInstalledExtension(installedExtension) div2.append(element) } addElement2(div, "h2", { textContent: i("settings.otherExtensions") }) const div3 = addElement2(div, "div", { class: "related_extension_list", }) updateRelatedExtensions(div3) return div } var prefix = "browser_extension_settings_" var randomId = String(Math.round(Math.random() * 1e4)) var settingsContainerId = prefix + "container_" + randomId var settingsElementId = prefix + "main_" + randomId var getSettingsElement = () => $("#" + settingsElementId) var getSettingsStyle = () => style_default .replaceAll(/browser_extension_settings_container/gm, settingsContainerId) .replaceAll(/browser_extension_settings_main/gm, settingsElementId) var storageKey = "settings" var settingsOptions var settingsTable = {} var settings = {} async function getSettings() { var _a return (_a = await getValue(storageKey)) != null ? _a : {} } async function saveSettingsValue(key, value) { const settings2 = await getSettings() settings2[key] = settingsTable[key] && settingsTable[key].defaultValue === value ? void 0 : value await setValue(storageKey, settings2) } function getSettingsValue(key) { var _a return Object.hasOwn(settings, key) ? settings[key] : (_a = settingsTable[key]) == null ? void 0 : _a.defaultValue } var closeModal = () => { const settingsContainer = getSettingsContainer() if (settingsContainer) { settingsContainer.style.display = "none" } removeEventListener(document, "click", onDocumentClick, true) removeEventListener(document, "keydown", onDocumentKeyDown, true) } var onDocumentClick = (event) => { const target = event.target if ( target == null ? void 0 : target.closest(".".concat(prefix, "container")) ) { return } closeModal() } var onDocumentKeyDown = (event) => { if (event.defaultPrevented) { return } if (event.key === "Escape") { closeModal() event.preventDefault() } } async function updateOptions() { if (!getSettingsElement()) { return } for (const key in settingsTable) { if (Object.hasOwn(settingsTable, key)) { const item = settingsTable[key] const type = item.type || "switch" switch (type) { case "switch": { const checkbox = $( "#" .concat( settingsElementId, ' .option_groups .switch_option[data-key="' ) .concat(key, '"] input') ) if (checkbox) { checkbox.checked = getSettingsValue(key) } break } case "select": { const options = $$( "#" .concat( settingsElementId, ' .option_groups .select_option[data-key="' ) .concat(key, '"] .bes_select option') ) for (const option of options) { option.selected = option.value === String(getSettingsValue(key)) } break } case "textarea": { const textArea = $( "#" .concat( settingsElementId, ' .option_groups textarea[data-key="' ) .concat(key, '"]') ) if (textArea) { textArea.value = getSettingsValue(key) } break } default: { break } } } } if (typeof settingsOptions.onViewUpdate === "function") { const settingsMain = createSettingsElement() settingsOptions.onViewUpdate(settingsMain) } } function getSettingsContainer() { const container = $(".".concat(prefix, "container")) if (container) { const theVersion = parseInt10(container.dataset.besVersion, 0) if (theVersion < besVersion) { container.id = settingsContainerId container.dataset.besVersion = String(besVersion) } return container } return addElement2(doc.body, "div", { id: settingsContainerId, class: "".concat(prefix, "container"), "data-bes-version": besVersion, style: "display: none;", }) } function getSettingsWrapper() { const container = getSettingsContainer() return ( $(".".concat(prefix, "wrapper"), container) || addElement2(container, "div", { class: "".concat(prefix, "wrapper"), }) ) } function initExtensionList() { const wrapper = getSettingsWrapper() if (!$(".extension_list_container", wrapper)) { const list = createExtensionList([]) wrapper.append(list) } addCurrentExtension({ id: settingsOptions.id, title: settingsOptions.title, onclick: showSettings, }) } function createSettingsElement() { let settingsMain = getSettingsElement() if (!settingsMain) { const wrapper = getSettingsWrapper() for (const element of $$(".".concat(prefix, "main"))) { element.remove() } settingsMain = addElement2(wrapper, "div", { id: settingsElementId, class: "".concat(prefix, "main thin_scrollbar"), }) addElement2(settingsMain, "a", { textContent: "Settings", class: "navigation_go_previous", onclick() { activeExtensionList() }, }) if (settingsOptions.title) { addElement2(settingsMain, "h2", { textContent: settingsOptions.title }) } const optionGroups = [] const getOptionGroup = (index) => { if (index > optionGroups.length) { for (let i2 = optionGroups.length; i2 < index; i2++) { optionGroups.push( addElement2(settingsMain, "div", { class: "option_groups", }) ) } } return optionGroups[index - 1] } for (const key in settingsTable) { if (Object.hasOwn(settingsTable, key)) { const item = settingsTable[key] const type = item.type || "switch" const group = item.group || 1 const optionGroup = getOptionGroup(group) switch (type) { case "switch": { const switchOption = createSwitchOption(item.icon, item.title, { async onchange(event) { const checkbox = event.target if (checkbox) { let result = true if (typeof item.onConfirmChange === "function") { result = item.onConfirmChange(checkbox.checked) } if (result) { await saveSettingsValue(key, checkbox.checked) } else { checkbox.checked = !checkbox.checked } } }, }) switchOption.dataset.key = key addElement2(optionGroup, switchOption) break } case "textarea": { let timeoutId2 const div = addElement2(optionGroup, "div", { class: "bes_textarea", }) addElement2(div, "textarea", { "data-key": key, placeholder: item.placeholder || "", onkeyup(event) { const textArea = event.target if (timeoutId2) { clearTimeout(timeoutId2) timeoutId2 = void 0 } timeoutId2 = setTimeout(async () => { if (textArea) { await saveSettingsValue(key, textArea.value.trim()) } }, 100) }, }) break } case "action": { addElement2(optionGroup, "a", { class: "action", textContent: item.title, onclick: item.onclick, }) break } case "externalLink": { const div4 = addElement2(optionGroup, "div", { class: "bes_external_link", }) addElement2(div4, "a", { textContent: item.title, href: item.url, target: "_blank", }) break } case "select": { const div = addElement2(optionGroup, "div", { class: "select_option bes_option", "data-key": key, }) if (item.icon) { addElement2(div, "img", { src: item.icon, class: "bes_icon" }) } addElement2(div, "span", { textContent: item.title, class: "bes_title", }) const select = addElement2(div, "select", { class: "bes_select", async onchange() { await saveSettingsValue(key, select.value) }, }) for (const option of Object.entries(item.options)) { addElement2(select, "option", { textContent: option[0], value: option[1], }) } break } case "tip": { const tip = addElement2(optionGroup, "div", { class: "bes_tip", }) addElement2(tip, "a", { class: "bes_tip_anchor", textContent: item.title, }) const tipContent = addElement2(tip, "div", { class: "bes_tip_content", innerHTML: createHTML(item.tipContent), }) break } } } } if (settingsOptions.footer) { const footer = addElement2(settingsMain, "footer") footer.innerHTML = createHTML( typeof settingsOptions.footer === "string" ? settingsOptions.footer : '

Made with \u2764\uFE0F by\n \n Pipecraft\n

' ) } } return settingsMain } function addSideMenu() { if (!getSettingsValue("displaySettingsButtonInSideMenu")) { return } const menu = $("#browser_extension_side_menu") || addElement2(doc.body, "div", { id: "browser_extension_side_menu", "data-bes-version": besVersion, }) const button = $("button[data-bes-version]", menu) if (button) { const theVersion = parseInt10(button.dataset.besVersion, 0) if (theVersion >= besVersion) { return } button.remove() } addElement2(menu, "button", { type: "button", "data-bes-version": besVersion, title: i("settings.menu.settings"), onclick() { setTimeout(showSettings, 1) }, innerHTML: settingButton, }) } function addCommonSettings(settingsTable3) { let maxGroup = 0 for (const key in settingsTable3) { if (Object.hasOwn(settingsTable3, key)) { const item = settingsTable3[key] const group = item.group || 1 if (group > maxGroup) { maxGroup = group } } } settingsTable3.displaySettingsButtonInSideMenu = { title: i("settings.displaySettingsButtonInSideMenu"), defaultValue: !( typeof GM === "object" && typeof GM.registerMenuCommand === "function" ), group: maxGroup + 1, } } function handleShowSettingsUrl() { if (location.hash === "#bes-show-settings") { setTimeout(showSettings, 100) } } async function showSettings() { const settingsContainer = getSettingsContainer() const settingsMain = createSettingsElement() await updateOptions() settingsContainer.style.display = "block" addEventListener(document, "click", onDocumentClick, true) addEventListener(document, "keydown", onDocumentKeyDown, true) activeExtension(settingsOptions.id) deactiveExtensionList() } var initSettings = async (options) => { settingsOptions = options settingsTable = options.settingsTable || {} addCommonSettings(settingsTable) addValueChangeListener(storageKey, async () => { settings = await getSettings() await updateOptions() addSideMenu() if (typeof options.onValueChange === "function") { options.onValueChange() } }) settings = await getSettings() runWhenHeadExists(() => { addStyle(getSettingsStyle()) }) runWhenDomReady(() => { initExtensionList() addSideMenu() }) registerMenuCommand(i("settings.menu.settings"), showSettings, "o") handleShowSettingsUrl() } var content_default = 'a.icon_button{opacity:1 !important;visibility:visible;margin-right:14px}a.icon_button:last-child{margin-right:0}a.icon_button svg{vertical-align:text-top}body .v2p-controls{opacity:1}body .v2p-controls>a.icon_button{margin-right:0}body .v2p-controls div a{margin-right:15px}body .v2p-controls div a:last-child{margin-right:0}body .v2p-controls a[onclick^=replyOne]{opacity:1 !important}.sticky_rightbar #Rightbar{position:sticky;top:0;max-height:100vh;overflow-y:auto;overflow-x:hidden;--sb-track-color: #00000000;--sb-thumb-color: #33334480;--sb-size: 2px;scrollbar-color:rgba(0,0,0,0) rgba(0,0,0,0);scrollbar-width:thin}.sticky_rightbar #Rightbar:hover{scrollbar-color:var(--sb-thumb-color) var(--sb-track-color)}.sticky_rightbar #Rightbar::-webkit-scrollbar{width:var(--sb-size)}.sticky_rightbar #Rightbar::-webkit-scrollbar-track{background:rgba(0,0,0,0);border-radius:10px}.sticky_rightbar #Rightbar:hover::-webkit-scrollbar-track{background:var(--sb-track-color)}.sticky_rightbar #Rightbar::-webkit-scrollbar-thumb{background:rgba(0,0,0,0);border-radius:10px}.sticky_rightbar #Rightbar:hover::-webkit-scrollbar-thumb{background:var(--sb-thumb-color)}.sticky_rightbar #Rightbar .v2p-tool-box{position:unset}.related_replies_container .related_replies{position:absolute;z-index:10000;width:100%;-webkit-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);-moz-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);box-shadow:0px 10px 39px 10px rgba(62,66,66,.22) !important}.related_replies_container .related_replies_before::before{content:"";display:block;width:100%;height:10000px;position:absolute;margin-top:-10000px;background-color:#334;opacity:50%}.related_replies_container .related_replies_after::after{content:"";display:block;width:100%;height:10000px;position:absolute;background-color:#334;opacity:50%}.related_replies_container.no_replies .related_replies_before::before,.related_replies_container.no_replies .related_replies_after::after{display:none}.related_replies_container .tabs{position:sticky;top:0;display:flex;justify-content:center;z-index:10001}.related_replies_container .tabs a{cursor:pointer}a.no{background-color:rgba(0,0,0,0) !important;color:#1484cd !important;border:1px solid #1484cd;border-radius:3px !important;opacity:1 !important}.cited_floor_number{color:#1484cd !important;cursor:pointer}.reply_content .cell.cited_reply{scale:.85;background-color:#f5f5f5;border:1px solid var(--box-border-color);white-space:initial}.reply_content .cell.cited_reply::after{content:"";display:block;position:absolute;left:0;bottom:0;width:100%;height:100%;opacity:.45;background-color:var(--box-background-color)}.reply_content .cell.cited_reply:hover::after{display:none}.reply_content .cell.cited_reply .vr_wrapper{max-height:150px;overflow:auto;--sb-track-color: #00000000;--sb-thumb-color: #33334480;--sb-size: 2px;scrollbar-color:var(--sb-thumb-color) var(--sb-track-color);scrollbar-width:thin}.reply_content .cell.cited_reply .vr_wrapper::-webkit-scrollbar{width:var(--sb-size)}.reply_content .cell.cited_reply .vr_wrapper::-webkit-scrollbar-track{background:var(--sb-track-color);border-radius:10px}.reply_content .cell.cited_reply .vr_wrapper::-webkit-scrollbar-thumb{background:var(--sb-thumb-color);border-radius:10px}[data-vr-opaticy-of-cited-replies="0"] .reply_content .cell.cited_reply::after{display:none}[data-vr-opaticy-of-cited-replies="1"] .reply_content .cell.cited_reply::after{opacity:.2}[data-vr-opaticy-of-cited-replies="3"] .reply_content .cell.cited_reply::after{opacity:.6}.v2p-indent .cell.cited_reply,.v2p-indent .reply_content+.reply_content,.v2p-indent .reply_content+.reply_content+.v2p-expand-btn,.v2p-indent .v2p-collapsed:has(.reply_content+.reply_content)::before,.comment .comment .cell.cited_reply{display:none !important}#top_replies .cell .vr_wrapper{position:relative;max-height:150px;overflow:hidden}#top_replies .cell .vr_wrapper::after{content:"";display:block;position:absolute;bottom:0;width:100%;height:5px;opacity:.8;background-color:var(--box-background-color)}.sticky_paging{position:sticky;bottom:0;background-color:var(--box-background-color) !important;border-top:1px solid var(--box-border-color);z-index:90}.reply-box-sticky{z-index:91}.Night .reply_content .cell.cited_reply{background-color:#1d1f21}.vr_upload_image{cursor:pointer}.vr_upload_image.vr_button_disabled,.vr_upload_image.vr_button_disabled:hover{cursor:default;text-decoration:none;color:var(--color-fade)}.sticky_topic_buttons .topic_buttons,.sticky_topic_buttons .topic_buttons_mobile{position:sticky;bottom:0;background-color:var(--box-background-color) !important;border-top:1px solid var(--box-border-color)}.sticky_topic_buttons .header+.cell{border-bottom:none}' var addLinkToAvatars = (replyElement) => { var _a, _b const memberLink = $('a[href^="/member/"]', replyElement) if ( memberLink && ((_a = memberLink.firstChild) == null ? void 0 : _a.tagName) === "IMG" ) { return } const avatar = $("img.avatar", replyElement) if (memberLink && avatar) { if (((_b = avatar.parentElement) == null ? void 0 : _b.tagName) === "A") { return } const memberLink2 = createElement("a", { href: getAttribute(memberLink, "href"), }) avatar.after(memberLink2) memberLink2.append(avatar) } } var getReplyElements = () => { const firstReply = $('.box .cell[id^="r_"]') if (firstReply == null ? void 0 : firstReply.parentElement) { const v2exPolishModel = $(".v2p-model-mask") return $$('.cell[id^="r_"]', firstReply.parentElement).filter((reply) => { if (v2exPolishModel && reply.closest(".v2p-model-mask")) { return false } return true }) } return [] } var cachedReplyElements var getCachedReplyElements = () => { if (!cachedReplyElements) { if (doc.readyState === "loading") { return getReplyElements() } cachedReplyElements = getReplyElements() } return cachedReplyElements } var resetCachedReplyElements = () => { cachedReplyElements = void 0 } var getReplyId = (replyElement) => { if (!replyElement) { return "" } let id = replyElement.dataset.id if (id) { return id } id = replyElement.id.replace(/((top|related|cited)_)?r_/, "") replyElement.dataset.id = id return id } var getFloorNumberElement = (replyElement) => replyElement ? $(".no", replyElement) : void 0 var getFloorNumber = (replyElement) => { if (!replyElement) { return 0 } let floorNumber = parseInt10(replyElement.dataset.floorNumber) if (floorNumber) { return floorNumber } const numberElement = getFloorNumberElement(replyElement) if (numberElement) { floorNumber = parseInt10(numberElement.textContent, 0) replyElement.dataset.floorNumber = String(floorNumber) return floorNumber } return 0 } var cloneReplyElement = ( replyElement, wrappingTable = false, keepCitedReplies = false ) => { const cloned = replyElement.cloneNode(true) const floorNumber = $(".no", cloned) const toolbox = $(".fr", cloned) if (toolbox && floorNumber) { const floorNumber2 = createElement("a", { class: "no", textContent: floorNumber.textContent, }) addEventListener(floorNumber2, "click", (event) => { replyElement.scrollIntoView({ block: "start" }) event.preventDefault() event.stopPropagation() }) toolbox.innerHTML = "" toolbox.append(floorNumber2) } const cells = $$(".cell,.v2p-topic-reply-ref,.nested", cloned) for (const cell of cells) { if (keepCitedReplies && hasClass(cell, "cited_reply")) { continue } cell.remove() } if (wrappingTable) { const table = cloned.firstElementChild if (table && table.tagName === "TABLE") { const wrapper = createElement("div", { class: "vr_wrapper", }) table.after(wrapper) wrapper.append(table) } } return cloned } var sortReplyElementsByFloorNumberCompareFn = (a, b) => getFloorNumber(a) - getFloorNumber(b) var parseUrl = () => { const matched = /\/t\/(\d+)(?:.+\bp=(\d+))?/.exec(location.href) || [] const topicId = matched[1] const page = parseInt10(matched[2], 1) return { topicId, page } } var getRepliesCount = () => { var _a return parseInt10( (/(\d+)\s条回复/.exec( ((_a = $(".box .cell .gray")) == null ? void 0 : _a.textContent) || "" ) || [])[1], 0 ) } var getMemberIdFromMemberLink = (memberLink) => { if (!memberLink) { return } return (/member\/(\w+)/.exec(memberLink.href) || [])[1] } var getReplyAuthorMemberId = (replyElement) => { if (!replyElement) { return } const memberLink = $('a[href^="/member/"]', replyElement) return getMemberIdFromMemberLink(memberLink) } var getReplyElementByMemberIdAndFloorNumber = ( memberId, floorNumber, type = 0 ) => { if (!memberId || !floorNumber) { return } const replyElements = getCachedReplyElements() const length = replyElements.length const reverse = floorNumber > length / 2 let nearestReply let nearestReplyGap = 1e3 for (let i2 = 0; i2 < length; i2++) { const replyElement = replyElements[reverse ? length - i2 - 1 : i2] const memberId2 = getReplyAuthorMemberId(replyElement) if (memberId2 !== memberId) { continue } const floorNumber2 = getFloorNumber(replyElement) if (floorNumber2 === floorNumber) { return replyElement } if (type === 1 && floorNumber2 > floorNumber) { continue } if (type === 2 && floorNumber2 < floorNumber) { continue } if ( !nearestReply || Math.abs(floorNumber - floorNumber2) < nearestReplyGap ) { nearestReply = replyElement nearestReplyGap = Math.abs(floorNumber - floorNumber2) } } return nearestReply } var getPagingPreviousButtons = () => $$(".normal_page_right").map((right) => right.previousElementSibling) var getPagingNextButtons = () => $$(".normal_page_right") var getReplyInputElement = () => { return $("#reply_content") } function insertTextToReplyInput(text) { const replyTextArea = getReplyInputElement() if (replyTextArea) { const startPos = replyTextArea.selectionStart const endPos = replyTextArea.selectionEnd const valueToStart = replyTextArea.value.slice(0, startPos) const valueFromEnd = replyTextArea.value.slice( endPos, replyTextArea.value.length ) replyTextArea.value = "" .concat(valueToStart) .concat(text) .concat(valueFromEnd) replyTextArea.focus() const newPos = startPos + text.length replyTextArea.selectionStart = newPos replyTextArea.selectionEnd = newPos } } var replaceReplyInputText = (find, replace, dispatchInputEvent = false) => { const replyTextArea = getReplyInputElement() if (replyTextArea) { const value = replyTextArea.value if (typeof value === "string") { const index = value.indexOf(find) if (index === -1) { return } const endPos = replyTextArea.selectionEnd const newValue = value.replace(find, replace) replyTextArea.value = newValue replyTextArea.focus() const newPos = index > endPos ? endPos : endPos + newValue.length - value.length replyTextArea.selectionStart = newPos replyTextArea.selectionEnd = newPos if (dispatchInputEvent) { replyTextArea.dispatchEvent(new Event("input")) } } } } var getOnce = () => { const onceElement = $("#once") if (onceElement == null ? void 0 : onceElement.value) { return onceElement.value } const html = doc.body.innerHTML const once = (/once=(\d+)/.exec(html) || [])[1] return once } var addlinkToCitedFloorNumbers = (replyElement) => { const content = $(".reply_content", replyElement) const memberLinks = $$('a[href^="/member/"]', content) for (const memberLink of memberLinks) { const previousTextNode = memberLink.previousSibling const memberId = getMemberIdFromMemberLink(memberLink) if ( previousTextNode && previousTextNode.nodeType === 3 && previousTextNode.textContent && previousTextNode.textContent.endsWith("@") && memberId ) { let nextTextNode = memberLink.nextSibling while (nextTextNode) { if ( nextTextNode.tagName === "BR" || !nextTextNode.textContent || nextTextNode.textContent.trim().length === 0 ) { nextTextNode = nextTextNode.nextSibling } else { break } } if ( !nextTextNode || nextTextNode.nodeType !== 3 || !nextTextNode.textContent || !/^\s*#\d+/.test(nextTextNode.textContent) ) { continue } const match = /^(\s*)(#(\d+))(.*)/.exec(nextTextNode.textContent) if (!match) { continue } if (match[1]) { nextTextNode.before(doc.createTextNode(match[1])) } if (match[2]) { const element = createElement("a", { class: "cited_floor_number", textContent: match[2], "data-member-id": memberId, "data-floor-number": match[3], }) nextTextNode.before(element) } nextTextNode.textContent = match[4] } } runOnce("addlinkToCitedFloorNumbers:document-onclick", () => { addEventListener(doc, "click", (event) => { const target = event.target if (hasClass(target, "cited_floor_number")) { const memberId = target.dataset.memberId const floorNumber = parseInt10(target.dataset.floorNumber) const citedReplyElement = getReplyElementByMemberIdAndFloorNumber( memberId, floorNumber ) if (citedReplyElement) { citedReplyElement.scrollIntoView({ block: "start" }) event.preventDefault() event.stopPropagation() } } }) }) } var alwaysShowHideButton = (replyElement) => { const hideButton = $('a[onclick*="ignoreReply"]', replyElement) if (hideButton && !hasClass(hideButton, "icon_button")) { addAttribute(hideButton, "class", "icon_button") if (!$(".v2p-controls", replyElement)) { hideButton.innerHTML = '' } const nextSibling = hideButton.nextSibling if (nextSibling && nextSibling.nodeType === 3) { nextSibling.textContent = "" } } } var alwaysShowThankButton = (replyElement) => { const thankButton = $('a[onclick*="thankReply"]', replyElement) if (thankButton && !hasClass(thankButton, "icon_button")) { addAttribute(thankButton, "class", "icon_button") if (!$(".v2p-controls", replyElement)) { thankButton.innerHTML = '' } } } var fetchOnce = async () => { const url = "" .concat(location.protocol, "//") .concat(location.host, "/poll_once") const response = await fetch(url) try { if (response.status === 200) { return await response.text() } } catch (error) { console.error("[V2EX.REP] Unable to refresh once", error) } } var updateOnce = async () => { const once = await fetchOnce() if (once) { window.once = once if ($("#once")) { $("#once").value = once } const links = $$('a[href*="once="]') for (const link of links) { const href = getAttribute(link, "href") setAttribute( link, "href", href.replace(/once=\d+/, "once=".concat(once)) ) } } } var storageKey2 = "dailyCheckIn" var retryCount = 0 var fetchCheckInApi = async (once) => { const url = "" .concat(location.protocol, "//") .concat(location.host, "/mission/daily/redeem?once=") .concat(once) try { const response = await fetch(url) if (response.status === 200) { return await response.text() } } catch (error) { console.error(error) retryCount++ if (retryCount < 3) { await sleep(1e3) return fetchCheckInApi(once) } } } var dailyCheckIn = async () => { var _a if ($('a[href*="/signin"]')) { return } const once = getOnce() if (!once) { return } const lastCheckInDate = await getValue(storageKey2) if (lastCheckInDate) { const now = Date.now() if ( now - lastCheckInDate < 864e5 && new Date(now).getUTCDate() === new Date(lastCheckInDate).getUTCDate() ) { return } } const result = await fetchCheckInApi(once) if ( result.includes("\u6BCF\u65E5\u767B\u5F55\u5956\u52B1\u5DF2\u9886\u53D6") ) { console.info("[V2EX.REP] \u7B7E\u5230\u6210\u529F") await setValue(storageKey2, Date.now()) const checkInLink = $('a[href^="/mission/daily"]') if (checkInLink) { const box = checkInLink.closest(".box") if (box) { ;(_a = box.nextElementSibling) == null ? void 0 : _a.remove() box.remove() } } } else { console.error("[V2EX.REP] \u7B7E\u5230\u5931\u8D25") } await updateOnce() } var isTouchScreen1 = isTouchScreen() var timeoutId var scrollPositionStack = [] var showModalReplies = (replies, referElement, memberId, type) => { var _a const main2 = $("#Main") || $(".content") if (!main2) { return } if (doc.scrollingElement) { scrollPositionStack.push(doc.scrollingElement.scrollTop) } setStyle(main2, "position: relative;") const replyElement = $("#Main") ? referElement.closest("#Main .cell") : referElement.closest(".cell") const relatedBox = replyElement == null ? void 0 : replyElement.closest(".related_replies") if (replyElement && relatedBox) { const lastRelatedRepliesBox = $$(".related_replies_container").pop() if ( lastRelatedRepliesBox == null ? void 0 : lastRelatedRepliesBox.contains(replyElement) ) { } else { closeModal2(true) } } else { closeModal2() } const container = addElement2(main2, "div", { class: "related_replies_container", }) const box = addElement2(container, "div", { class: "box related_replies related_replies_before", }) const box2 = addElement2(container, "div", { class: "box related_replies related_replies_after", }) box.innerHTML = "" box2.innerHTML = "" const tabs = addElement2(box, "div", { class: "box tabs inner", }) addElement2(tabs, "a", { class: !type || type === "all" ? "tab_current" : "tab", href: actionHref, textContent: "\u5168\u90E8\u56DE\u590D", onclick() { showRelatedReplies(referElement, memberId) }, }) addElement2(tabs, "a", { class: type === "posted" ? "tab_current" : "tab", href: actionHref, textContent: "\u4EC5 ".concat( memberId, " \u53D1\u8868\u7684\u56DE\u590D" ), onclick() { showRelatedReplies(referElement, memberId, "posted") }, }) const replyId = replyElement ? getReplyId(replyElement) : void 0 const floorNumber = replyElement ? getFloorNumber(replyElement) : 0 let beforeCount = 0 let afterCount = 0 replies.sort(sortReplyElementsByFloorNumberCompareFn) for (const reply of replies) { const replyId2 = getReplyId(reply) const floorNumber2 = getFloorNumber(reply) if (replyId === replyId2) { continue } if (floorNumber > floorNumber2) { box.append(reply) beforeCount++ } else { box2.append(reply) afterCount++ } } if (beforeCount === 0 && afterCount === 0) { addElement2(box, "div", { class: "cell", innerHTML: '\u672C\u9875\u9762\u6CA1\u6709\u5176\u4ED6\u56DE\u590D', }) if (!type || type === "all") { tabs.remove() addClass(container, "no_replies") addEventListener( referElement, "mouseout", () => { container.remove() scrollPositionStack.pop() }, { once: true } ) } } if (beforeCount === 0 && afterCount > 0) { addElement2(box, "div", { class: "cell", innerHTML: '\u8FD9\u6761\u56DE\u590D\u540E\u9762\u8FD8\u6709 '.concat( afterCount, " \u6761\u56DE\u590D" ), }) } if (beforeCount > 0 && afterCount === 0) { addElement2(box2, "div", { class: "cell", innerHTML: '\u8FD9\u6761\u56DE\u590D\u524D\u9762\u8FD8\u6709 '.concat( beforeCount, " \u6761\u56DE\u590D" ), }) } const width = main2.offsetWidth + "px" if (replyElement) { const offsetTop = getOffsetPosition(replyElement, main2).top const height = box.offsetHeight || box.clientHeight const height2 = replyElement.offsetHeight || replyElement.clientHeight setStyle(box, { top: offsetTop - height + "px", width, }) setStyle(box2, { top: offsetTop + height2 + "px", width, }) } else if (afterCount > 0) { ;(_a = box2.firstChild) == null ? void 0 : _a.before(tabs) const headerElement = referElement == null ? void 0 : referElement.closest(".header") if (headerElement) { const offsetTop = getOffsetPosition(headerElement, main2).top const height2 = headerElement.offsetHeight || headerElement.clientHeight setStyle(box2, { top: offsetTop + height2 + "px", width, }) box.remove() } else { const firstReply = $('.box .cell[id^="r_"]') const offsetTop = firstReply ? Math.max(getOffsetPosition(firstReply, main2).top, win.scrollY) : win.scrollY setStyle(box, { top: offsetTop + "px", width, }) setStyle(box2, { top: offsetTop + "px", width, }) box2.scrollIntoView({ block: "start" }) } } else { box.remove() box2.remove() } } var filterRepliesPostedByMember = (memberIds) => { const replies = [] const replyElements = getCachedReplyElements() for (const replyElement of replyElements) { const memberLink = $('a[href^="/member/"]', replyElement) if (!memberLink) { continue } const memberId = (/member\/(\w+)/.exec(memberLink.href) || [])[1] if (memberIds.includes(memberId)) { const cloned = cloneReplyElement(replyElement, true, true) cloned.id = "related_" + replyElement.id replies.push(cloned) } } return replies } var filterRepliesByPosterOrMentioned = (memberId) => { const replies = [] const replyElements = getCachedReplyElements() for (const replyElement of replyElements) { const memberLink = $( 'a[href^="/member/'.concat(memberId, '"]'), replyElement ) if (!memberLink) { continue } let cloned = cloneReplyElement(replyElement, true) const memberLink2 = $('a[href^="/member/'.concat(memberId, '"]'), cloned) if (!memberLink2) { continue } cloned = cloneReplyElement(replyElement, true, true) cloned.id = "related_" + replyElement.id replies.push(cloned) } return replies } var showRelatedReplies = (memberLink, memberId, type) => { const replies = type === "posted" ? filterRepliesPostedByMember([memberId]) : filterRepliesByPosterOrMentioned(memberId) showModalReplies(replies, memberLink, memberId, type) } var onMouseOver = (event) => { if (timeoutId) { clearTimeout(timeoutId) timeoutId = void 0 } const memberLink = event.target timeoutId = setTimeout(() => { const memberId = (/member\/(\w+)/.exec(memberLink.href) || [])[1] if (memberId) { showRelatedReplies(memberLink, memberId) } }, 700) } var onMouseOut = () => { if (timeoutId) { clearTimeout(timeoutId) timeoutId = void 0 } } var closeModal2 = (closeLast = false) => { for (const element of $$(".related_replies_container").reverse()) { element.remove() const scrollPosition = scrollPositionStack.pop() if (scrollPosition !== void 0 && doc.scrollingElement) { doc.scrollingElement.scrollTop = scrollPosition } if (closeLast) { break } } } var onDocumentClick2 = (event) => { const target = event.target if (target.closest(".utags_ul")) { if ( hasClass(target, "utags_captain_tag") || hasClass(target, "utags_captain_tag2") ) { event.preventDefault() } return } if (isTouchScreen1) { const memberLink = target.closest('a[href^="/member/"]') if (memberLink && !$("img", memberLink)) { event.preventDefault() event.stopPropagation() return } } const floorNumberElement = target.closest(".related_replies a.no") if (floorNumberElement) { closeModal2() return } const lastRelatedRepliesBox = $$(".related_replies_container").pop() const relatedReply = target.closest(".related_replies .cell") if ( relatedReply && (lastRelatedRepliesBox == null ? void 0 : lastRelatedRepliesBox.contains(relatedReply)) ) { return } const relatedRepliesBox = target.closest(".related_replies_container") if (relatedRepliesBox) { closeModal2(true) return } closeModal2() } var onDocumentKeyDown2 = (event) => { if (event.defaultPrevented) { return } if (event.key === "Escape") { closeModal2(true) } } var filterRepliesByUser = (toogle) => { if (toogle) { const memberLinks = $$('a[href^="/member/"]') for (const memberLink of memberLinks) { if (!memberLink.boundEvent) { addEventListener(memberLink, "mouseover", onMouseOver, true) addEventListener(memberLink, "mouseout", onMouseOut) if (isTouchScreen1) { addEventListener(memberLink, "touchstart", onMouseOver, true) } memberLink.boundEvent = true } } if (!doc.boundEvent) { addEventListener(doc, "click", onDocumentClick2, true) addEventListener(doc, "keydown", onDocumentKeyDown2) doc.boundEvent = true } } else if (doc.boundEvent) { closeModal2() removeEventListener(doc, "click", onDocumentClick2, true) removeEventListener(doc, "keydown", onDocumentKeyDown2) doc.boundEvent = false const memberLinks = $$('a[href^="/member/"]') for (const memberLink of memberLinks) { if (memberLink.boundEvent) { removeEventListener(memberLink, "mouseover", onMouseOver, true) removeEventListener(memberLink, "mouseout", onMouseOut) if (isTouchScreen1) { removeEventListener(memberLink, "touchstart", onMouseOver, true) } memberLink.boundEvent = false } } } } var retryCount2 = 0 var getTopicReplies = async (topicId, replyCount) => { const cached = cache.get(["getTopicReplies", topicId, replyCount]) if (cached) { return cached } const url = "" .concat(location.protocol, "//") .concat(location.host, "/api/replies/show.json?topic_id=") .concat(topicId) .concat(replyCount ? "&replyCount=" + String(replyCount) : "") try { const response = await fetch(url) if (response.status === 200) { const result = await response.json() cache.add(["getTopicReplies", topicId, replyCount], result) return result } } catch (error) { console.error(error) retryCount2++ if (retryCount2 < 3) { await sleep(1e3) return getTopicReplies(topicId, replyCount) } } } var updateFloorNumber = (replyElement, newFloorNumber) => { const numberElement = getFloorNumberElement(replyElement) if (numberElement) { if (!numberElement.dataset.orgNumber) { const orgNumber = parseInt10(numberElement.textContent) if (orgNumber) { numberElement.dataset.orgNumber = String(orgNumber) } } numberElement.textContent = String(newFloorNumber) replyElement.dataset.floorNumber = String(newFloorNumber) } } var updateAllFloorNumberById = (id, newFloorNumber) => { for (const replyElement of $$( "#r_" .concat(id, ",\n #top_r_") .concat(id, ",\n #related_r_") .concat(id, ",\n #cited_r_") .concat(id) )) { updateFloorNumber(replyElement, newFloorNumber) } } var printHiddenReplies = (hiddenReplies) => { for (const reply of hiddenReplies) { console.group( "[V2EX.REP] \u5C4F\u853D\u6216\u9690\u85CF\u7684\u56DE\u590D: #" .concat(reply.floorNumber, ", \u7528\u6237 ID: ") .concat(reply.userId, ", \u56DE\u590D ID: ") .concat(reply.replyId, ", \u56DE\u590D\u5185\u5BB9: ") ) console.info(reply.replyContent) console.groupEnd() } } var updateReplyElements = (replies, replyElements, page = 1) => { let floorNumberOffset = 0 let hiddenCount = 0 let hiddenCount2 = 0 const dataOffSet = (page - 1) * 100 const length = Math.min(replies.length - (page - 1) * 100, 100) const hiddenReplies = [] for (let i2 = 0; i2 < length; i2++) { const realFloorNumber = i2 + dataOffSet + 1 const reply = replies[i2 + dataOffSet] const id = reply.id const element = $("#r_" + id) const member = reply.member || {} if (!element) { hiddenReplies.push({ floorNumber: realFloorNumber, userId: member.username, replyId: reply.id, replyContent: reply.content, }) hiddenCount++ continue } if (!isVisible(element)) { hiddenReplies.push({ floorNumber: realFloorNumber, userId: member.username, replyId: reply.id, replyContent: reply.content, }) hiddenCount2++ } element.found = true if (hiddenCount > 0) { const numberElement = getFloorNumberElement(element) if (numberElement) { const orgNumber = parseInt10( numberElement.dataset.orgNumber || numberElement.textContent ) if (orgNumber) { numberElement.dataset.orgNumber = String(orgNumber) floorNumberOffset = realFloorNumber - orgNumber } numberElement.textContent = String(realFloorNumber) } updateAllFloorNumberById(id, realFloorNumber) } } console.info( "[V2EX.REP] page: " .concat(page, ", floorNumberOffset: ") .concat(floorNumberOffset, ", hiddenCount: ") .concat(hiddenCount + hiddenCount2) ) if (floorNumberOffset > 0) { for (const element of replyElements) { if (element.found) { continue } const id = getReplyId(element) const numberElement = getFloorNumberElement(element) if (numberElement) { const orgNumber = parseInt10( numberElement.dataset.orgNumber || numberElement.textContent ) if (orgNumber) { numberElement.dataset.orgNumber = String(orgNumber) numberElement.textContent = String(orgNumber + floorNumberOffset) updateAllFloorNumberById(id, orgNumber + floorNumberOffset) } } } } printHiddenReplies(hiddenReplies) win.dispatchEvent(new Event("floorNumberUpdated")) } var isRunning = false var splitArrayPerPages = (replyElements) => { if ( !replyElements || replyElements.length === 0 || !replyElements[0].dataset.page ) { return } const replyElementsPerPages = [] let lastPage let replyElementsPerPage = [] for (const reply of replyElements) { if (reply.dataset.page !== lastPage) { lastPage = reply.dataset.page const page = parseInt10(reply.dataset.page) replyElementsPerPage = replyElementsPerPages[page - 1] || [] replyElementsPerPages[page - 1] = replyElementsPerPage } replyElementsPerPage.push(reply) } return replyElementsPerPages } var process2 = async ( topicId, page, displayNumber, replyElements, forceUpdate = false ) => { if (isRunning) { return } isRunning = true const replies = await getTopicReplies( topicId, forceUpdate ? displayNumber : void 0 ) if (replies) { const replyElementsPerPages = splitArrayPerPages(replyElements) if (replyElementsPerPages) { for (let i2 = 0; i2 < replyElementsPerPages.length; i2++) { const replyElementsPerPage = replyElementsPerPages[i2] if ( !replyElementsPerPage || (replyElementsPerPage.length > 0 && (displayNumber === replyElementsPerPage.length || displayNumber % 100 === replyElementsPerPage.length % 100 || replyElementsPerPage.length % 100 === 0)) ) { continue } updateReplyElements(replies, replyElementsPerPage, i2 + 1) } } else { updateReplyElements(replies, replyElements, page) } if (replies.length < displayNumber) { console.info("[V2EX.REP] API data outdated, re-fetch it") setTimeout(async () => { await process2(topicId, page, displayNumber, replyElements, true) }, 100) } } isRunning = false } var fixReplyFloorNumbers = async (replyElements) => { if (isRunning) { return } const result = parseUrl() const topicId = result.topicId const page = result.page if (!topicId) { return } const displayNumber = getRepliesCount() if ( replyElements.length > 0 && (displayNumber === replyElements.length || displayNumber % 100 === replyElements.length % 100 || replyElements.length % 100 === 0) ) { return } await process2(topicId, page, displayNumber, replyElements) } var restoreImgSrc = throttle(() => { for (const img of $$("img[data-src]")) { setAttribute(img, "src", getAttribute(img, "data-src")) delete img.dataset.src } }, 500) var lazyLoadAvatars = (replyElement) => { const avatar = $("img.avatar", replyElement) if (avatar) { if (getAttribute(avatar, "loading") === "lazy" || avatar.complete) { return } const src = getAttribute(avatar, "src") setAttribute(avatar, "loading", "lazy") setAttribute(avatar, "data-src", src) setAttribute( avatar, "src", "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" ) if (doc.readyState === "complete") { setTimeout(restoreImgSrc) } else { addEventListener(win, "load", restoreImgSrc) } } } var retryCount3 = 0 var getTopicPage = async (topicId, page = 1) => { const url = "" .concat(location.protocol, "//") .concat(location.host, "/t/") .concat(topicId, "?p=") .concat(page) try { const response = await fetch(url) if (response.status === 200) { return await response.text() } } catch (error) { console.error(error, "page ".concat(page)) retryCount3++ if (retryCount3 < 10) { await sleep(1e3) return getTopicPage(topicId, page) } } } var getReplyElements2 = (html) => { const htmlNode = createElement("html") htmlNode.innerHTML = html return $$('.cell[id^="r_"]', htmlNode) } var insertReplyElementsToPage = (replyElements, page, inertPoint) => { if (!replyElements || replyElements.length === 0 || !inertPoint) { return } for (const replyElement of replyElements) { replyElement.dataset.page = String(page) if (getSettingsValue("lazyLoadAvatars")) { lazyLoadAvatars(replyElement) } inertPoint.before(replyElement) } } var gotoPage = (page, event) => { if (!page) { return } history.pushState(null, null, "?p=".concat(page)) const main2 = $("#Main") || $(".content") const firstReply = $('.cell[data-page="'.concat(page, '"]'), main2) if (firstReply) { firstReply.scrollIntoView({ block: "start" }) event.preventDefault() event.stopImmediatePropagation() } for (const pagingElement of $$(".page_current,.page_normal")) { if (pagingElement.textContent === String(page)) { removeClass(pagingElement, "page_normal") addClass(pagingElement, "page_current") } else { removeClass(pagingElement, "page_current") addClass(pagingElement, "page_normal") } } for (const pageInput of $$(".page_input")) { pageInput.value = page } const repliesCount = getRepliesCount() const totalPage = Math.ceil(repliesCount / 100) for (const button of getPagingPreviousButtons()) { if (String(page) === "1") { addClass(button, "disable_now") } else { removeClass(button, "disable_now") } } for (const button of getPagingNextButtons()) { if (String(page) === String(totalPage)) { addClass(button, "disable_now") } else { removeClass(button, "disable_now") } } } var updatePagingElements = () => { runOnce("loadMultiPages:updatePagingElements", () => { for (const pagingElement of $$(".page_current,.page_normal")) { addEventListener(pagingElement, "click", (event) => { const page = pagingElement.textContent gotoPage(page, event) }) } for (const pageInput of $$(".page_input")) { pageInput.removeAttribute("onkeydown") addEventListener( pageInput, "keydown", (event) => { var _a if (event.keyCode === 13) { gotoPage((_a = event.target) == null ? void 0 : _a.value, event) return false } }, true ) } const buttons = [...getPagingPreviousButtons(), ...getPagingNextButtons()] for (const button of buttons) { button.removeAttribute("onclick") button.removeAttribute("onmouseover") button.removeAttribute("onmousedown") button.removeAttribute("onmouseleave") addEventListener( button, "mouseover", (event) => { if (!hasClass(button, "disable_now")) { addClass(button, "hover_now") } event.preventDefault() event.stopImmediatePropagation() }, true ) addEventListener( button, "mousedown", (event) => { if (!hasClass(button, "disable_now")) { addClass(button, "active_now") } event.preventDefault() event.stopImmediatePropagation() }, true ) addEventListener( button, "mouseleave", (event) => { removeClass(button, "hover_now") removeClass(button, "active_now") event.preventDefault() event.stopImmediatePropagation() }, true ) addEventListener( button, "click", (event) => { var _a if (!hasClass(button, "disable_now")) { const page = parseInt10( (_a = $(".page_input")) == null ? void 0 : _a.value ) if (page) { if (hasClass(button, "normal_page_right")) { gotoPage(page + 1, event) } else { gotoPage(page - 1, event) } } } setTimeout(() => { removeClass(button, "hover_now") removeClass(button, "active_now") }, 100) event.preventDefault() event.stopImmediatePropagation() }, true ) } }) } var loadMultiPages = async () => { const repliesCount = getRepliesCount() if (repliesCount > 100) { const result = parseUrl() const topicId = result.topicId const currentPage = result.page const totalPage = Math.ceil(repliesCount / 100) const orgReplyElements = getCachedReplyElements() const firstReply = orgReplyElements[0] const pageElement = orgReplyElements.at(-1).nextElementSibling addClass(pageElement, "sticky_paging") updatePagingElements() for (const replyElement of orgReplyElements) { replyElement.dataset.page = String(currentPage) } for (let i2 = 1; i2 <= totalPage; i2++) { if (i2 === currentPage) { continue } console.info("[V2EX.REP] Fetching page", i2) const html = await getTopicPage(topicId, i2) if (html) { const replyElements = getReplyElements2(html) insertReplyElementsToPage( replyElements, i2, i2 < currentPage ? firstReply : pageElement ) win.dispatchEvent(new Event("replyElementsLengthUpdated")) } await sleep(1e3) } } } var quickHideReply = (replyElement) => { const hideButton = $('a[onclick*="ignoreReply"]', replyElement) if (hideButton) { const onclick = getAttribute(hideButton, "onclick") if (!onclick.includes("confirm")) { return } setAttribute( hideButton, "onclick", onclick.replace(/.*(ignoreReply\(.+\)).*/, "$1") ) setAttribute(hideButton, "href", actionHref) hideButton.outerHTML = hideButton.outerHTML } } var state = 0 var scrollIntoView = (element) => { if (element) { element.scrollIntoView({ block: "start" }) } } var quickNavigation = () => { runOnce("quickNavigation", () => { const main2 = $("#Main") || $(".content") if (!main2) { return } const isMobile = hasClass(main2, "content") addEventListener(doc, "dblclick", (event) => { const target = event.target if ( target && ["TEXTAREA", "INPUT", "IMG", "A"].includes(target.tagName) ) { return } const selection = win.getSelection() if ( (target == null ? void 0 : target.closest(".box,.cell,.inner")) && selection && !selection.isCollapsed && selection.toString().trim().length > 0 ) { return } const boxes = $$(".box", main2) switch (state++) { case 0: { scrollIntoView(isMobile ? boxes[2] : boxes[1]) break } case 1: { scrollIntoView(isMobile ? boxes[3] : boxes[2]) break } default: { scrollIntoView(boxes[0]) state = 0 break } } }) }) } var quickSendThank = (replyElement) => { const thankButton = $('a[onclick*="thankReply"]', replyElement) if (thankButton) { const replyId = replyElement.id.replace("r_", "") const onclick = getAttribute(thankButton, "onclick") if (!onclick.includes("confirm")) { return } setAttribute( thankButton, "onclick", onclick.replace(/.*(thankReply\(.+\)).*/, "$1") ) setAttribute(thankButton, "href", actionHref) if (hasClass(thankButton.parentElement, "v2p-controls")) { const div = createElement("div", { id: "thank_area_" + replyId, }) thankButton.after(div) const hideButton = $('a[onclick*="ignoreReply"]', replyElement) if (hideButton) { div.append(hideButton) } div.append(thankButton) } thankButton.outerHTML = thankButton.outerHTML } } var replaceState = (newHref) => { history.replaceState(null, "", newHref) } var getVisitedUrl = (href, replyCount) => href.replace(/[?#].*|$/, "#reply".concat(replyCount)) var markAsVisited = (href, replyCount) => { for ( let count = Math.max(replyCount - 10, 1); count <= replyCount; count++ ) { replaceState(getVisitedUrl(href, count)) } } var removeLocationHash = () => { const href = location.href const hash = location.hash const replyCount = getRepliesCount() if (hash == null ? void 0 : hash.startsWith("#reply")) { if (replyCount) { markAsVisited(href, replyCount) } replaceState(href.replace(/#.*/, "")) } else if (replyCount) { markAsVisited(href, replyCount) replaceState(href) } } function setFavition(url, type) { const element = $('link[rel="shortcut icon"]') if (element) { setAttributes(element, { href: url, type: type || "image/png", }) } } function replaceToGithub() { setFavition( "https://github.githubassets.com/favicons/favicon.svg", "image/svg+xml" ) if (doc.title.includes("V2EX")) { doc.title = "Issues \xB7 " + (doc.title.replace(/( - V2EX|V2EX › |V2EX)/, "") || "github") } } function replaceToAvatar() { const main2 = $("#Main") || $(".content") if (!main2) { return } const avatar = $('.header img.avatar, td[width="73"] img.avatar', main2) if (avatar) { setFavition(avatar.src) if (!avatar.setFaviconHandler) { avatar.setFaviconHandler = true addEventListener(avatar, "load", () => { setFavition(avatar.src) }) } } else { setFavition("https://www.v2ex.com/static/favicon.ico") } } function replaceToDefault() { const main2 = $("#Main") || $(".content") if (!main2) { return } const avatar = $('td[width="73"] img.avatar', main2) if (avatar) { setFavition(avatar.src) } else { setFavition("https://www.v2ex.com/static/favicon.ico") } } function replaceFavicon(type) { if (type === "github") { replaceToGithub() } else if (type === "avatar") { replaceToAvatar() } else { replaceToDefault() } } var replyWithFloorNumber = (replyElement, forceUpdate = false) => { const replyButton = $('a[onclick^="replyOne"]', replyElement) if (replyButton) { setAttribute(replyButton, "href", actionHref) const onclick = getAttribute(replyButton, "onclick") || "" if (onclick.includes("#") && !forceUpdate) { return } const number = getFloorNumber(replyElement) if (number) { setAttribute( replyButton, "onclick", onclick.replace( /replyOne\('(\w+)(?: .*)?'\)/, "replyOne('$1 #".concat(number, "')") ) ) replyButton.outerHTML = replyButton.outerHTML } } } var showCitedReplies = ( replyElement, showPreviousCitedReplies, forceUpdate = false ) => { if ( !forceUpdate && (replyElement.dataset.showCitedReplies || $(".v2p-color-mode-toggle")) ) { return } const floorNumber = getFloorNumber(replyElement) if (!floorNumber) { return } replyElement.dataset.showCitedReplies = "done" for (const element of $$(".cited_reply", replyElement)) { element.remove() } const content = $(".reply_content", replyElement) const memberLinks = $$('a[href^="/member/"]', content) let hasCitedReplies = false for (const memberLink of memberLinks) { const textNode = memberLink.previousSibling let nextElement = memberLink.nextSibling let target = memberLink let citedFloorNumber if ( textNode && textNode.nodeType === 3 && textNode.textContent && textNode.textContent.endsWith("@") ) { const memberId = getMemberIdFromMemberLink(memberLink) if (!memberId) { continue } while ( nextElement && (nextElement.tagName === "BR" || !nextElement.textContent || nextElement.textContent.trim().length === 0 || hasClass(nextElement, "utags_ul")) ) { target = nextElement nextElement = nextElement.nextSibling } if (nextElement && hasClass(nextElement, "cited_floor_number")) { target = nextElement citedFloorNumber = parseInt10(nextElement.dataset.floorNumber) } let citedReplyElement if (citedFloorNumber) { citedReplyElement = getReplyElementByMemberIdAndFloorNumber( memberId, citedFloorNumber ) } if (!citedReplyElement) { citedReplyElement = getReplyElementByMemberIdAndFloorNumber( memberId, floorNumber - 1, 1 ) } if (citedReplyElement) { if ( citedReplyElement.nextElementSibling === replyElement && !hasCitedReplies && showPreviousCitedReplies !== "1" ) { continue } const cloned = cloneReplyElement(citedReplyElement, true) cloned.removeAttribute("id") addClass(cloned, "cited_reply") target.after(cloned) hasCitedReplies = true } } } } var done = false var reset = () => { const element = $("#top_replies") if (element) { const sep20 = element.previousElementSibling if (hasClass(sep20, "sep20")) { sep20.remove() } element.remove() } } var showTopReplies = (replyElements, toggle, forceUpdate = false) => { if (!toggle) { reset() removeClass($("#Wrapper"), "sticky_rightbar") done = false return } if (done && !forceUpdate) { return } done = true reset() addClass($("#Wrapper"), "sticky_rightbar") const topReplies = replyElements .filter((reply) => { var _a const heartElement = $('img[alt="\u2764\uFE0F"],.v2p-icon-heart', reply) if (heartElement) { const childReplies = $$(".reply_content,.cell", reply) for (const child of childReplies) { if (child.contains(heartElement)) { return false } } const thanked = parseInt10( (_a = heartElement.nextSibling) == null ? void 0 : _a.textContent, 0 ) if (thanked > 0) { reply.thanked = thanked return true } } return false }) .sort((a, b) => b.thanked === a.thanked ? sortReplyElementsByFloorNumberCompareFn(a, b) : b.thanked - a.thanked ) if (topReplies.length > 0) { const box = createElement("div", { class: "box", id: "top_replies", innerHTML: '
\u5F53\u524D\u9875\u70ED\u95E8\u56DE\u590D
', }) for (const element of topReplies) { const cloned = cloneReplyElement(element, true) cloned.id = "top_" + element.id const ago = $(".ago", cloned) if (ago) { ago.before(createElement("br")) } box.append(cloned) } const appendPosition = $("#Rightbar .box") const sep20 = createElement("div", { class: "sep20", }) if (appendPosition) { appendPosition.after(box) appendPosition.after(sep20) } } } var stickyTopicButtons = (toggle = false) => { const main2 = $("#Main") || $(".content") if (!main2) { return } if (hasClass(main2, "content")) { const buttons = $(".inner", main2) if (buttons) { addClass(buttons, "topic_buttons_mobile") } } const added = hasClass(main2, "sticky_topic_buttons") if (toggle && !added) { addClass(main2, "sticky_topic_buttons") } else if (!toggle && added) { removeClass(main2, "sticky_topic_buttons") } } var imgurClientIdPool = [ "3107b9ef8b316f3", "442b04f26eefc8a", "59cfebe717c09e4", "60605aad4a62882", "6c65ab1d3f5452a", "83e123737849aa9", "9311f6be1c10160", "c4a4a563f698595", "81be04b9e4a08ce", ] async function uploadImageToImgur(file) { const formData = new FormData() formData.append("image", file) const randomIndex = Math.floor(Math.random() * imgurClientIdPool.length) const clidenId = imgurClientIdPool[randomIndex] const response = await fetch("https://api.imgur.com/3/upload", { method: "POST", headers: { Authorization: "Client-ID ".concat(clidenId) }, body: formData, }) if (response.ok) { const responseData = await response.json() if (responseData.success) { return responseData.data.link } } throw new Error("\u4E0A\u4F20\u5931\u8D25") } var handleUploadImage = (file) => { const detail = { file } win.dispatchEvent(new CustomEvent("uploadImageStart", { detail })) uploadImageToImgur(file) .then((imgLink) => { detail.imgLink = imgLink win.dispatchEvent(new CustomEvent("uploadImageSuccess", { detail })) }) .catch(() => { win.dispatchEvent(new CustomEvent("uploadImageFailed", { detail })) }) } var handleClickUploadImage = () => { const imgInput = document.createElement("input") imgInput.style.display = "none" imgInput.type = "file" imgInput.accept = "image/*" addEventListener(imgInput, "change", () => { var _a const selectedFile = (_a = imgInput.files) == null ? void 0 : _a[0] if (selectedFile) { handleUploadImage(selectedFile) } }) imgInput.click() } var init = () => { const replyTextArea = getReplyInputElement() if (!replyTextArea) { return } const appendPosition = $("#reply-box > div > div") if (!appendPosition) { return } setAttribute( replyTextArea, "placeholder", "\u60A8\u53EF\u4EE5\u5728\u56DE\u590D\u6846\u5185\u76F4\u63A5\u7C98\u8D34\u56FE\u7247\u6216\u62D6\u62FD\u56FE\u7247\u6587\u4EF6\u81F3\u56DE\u590D\u6846\u5185\u4E0A\u4F20" ) const uploadTip = "+ \u63D2\u5165\u56FE\u7247" const placeholder = "[\u4E0A\u4F20\u56FE\u7247\u4E2D...]" addElement2(appendPosition, "span", { class: "snow", textContent: " \xB7 ", }) const uploadButton = createElement("a", { class: "vr_upload_image", textContent: uploadTip, }) appendPosition.append(uploadButton) addEventListener(uploadButton, "click", () => { if (!hasClass(uploadButton, "vr_button_disabled")) { handleClickUploadImage() } }) addEventListener( doc, "paste", (event) => { var _a if (!(event instanceof ClipboardEvent)) { return } const replyTextArea2 = getReplyInputElement() if ( !(replyTextArea2 == null ? void 0 : replyTextArea2.matches(":focus")) ) { return } const items = (_a = event.clipboardData) == null ? void 0 : _a.items if (!items) { return } const imageItem = Array.from(items).find((item) => item.type.includes("image") ) if (imageItem) { const file = imageItem.getAsFile() if (file) { handleUploadImage(file) } } }, true ) addEventListener( replyTextArea, "drop", (event) => { var _a if (!(event instanceof DragEvent)) { return } const files = (_a = event.dataTransfer) == null ? void 0 : _a.files if (files == null ? void 0 : files.length) { for (const file of files) { if (file.type.includes("image")) { event.preventDefault() event.stopImmediatePropagation() handleUploadImage(file) } } } }, true ) addEventListener(win, { uploadImageStart(event) { if (!event.detail) { return } const detail = event.detail const fileName = detail.file.name || "noname" detail.placeholder = placeholder.replace( /]/, " (".concat(fileName, ")]") ) const replyTextArea2 = getReplyInputElement() if (replyTextArea2) { insertTextToReplyInput( replyTextArea2.value.trim().length > 0 && replyTextArea2.selectionStart > 0 ? "\n".concat(detail.placeholder, "\n") : "".concat(detail.placeholder, "\n") ) } }, uploadImageSuccess(event) { if (!event.detail) { return } const detail = event.detail removeClass(uploadButton, "vr_button_disabled") uploadButton.textContent = uploadTip replaceReplyInputText( detail.placeholder || placeholder, detail.imgLink || "", true ) }, uploadImageFailed(event) { if (!event.detail) { return } const detail = event.detail removeClass(uploadButton, "vr_button_disabled") uploadButton.textContent = uploadTip replaceReplyInputText(detail.placeholder || placeholder, "") alert( "[V2EX.REP] \u274C \u4E0A\u4F20\u56FE\u7247\u5931\u8D25\uFF0C\u8BF7\u6253\u5F00\u63A7\u5236\u53F0\u67E5\u770B\u539F\u56E0" ) }, }) } var uploadImage = () => { runOnce("uploadImage:init", init) } var config = { matches: ["https://*.v2ex.com/*"], run_at: "document_start", } var settingsTable2 = { fixReplyFloorNumbers: { title: "\u4FEE\u590D\u697C\u5C42\u53F7", defaultValue: true, }, replyWithFloorNumber: { title: "\u56DE\u590D\u65F6\u5E26\u4E0A\u697C\u5C42\u53F7", defaultValue: true, }, showTopReplies: { title: "\u663E\u793A\u70ED\u95E8\u56DE\u590D", defaultValue: true, }, showCitedReplies: { title: "\u663E\u793A\u88AB\u5F15\u7528\u7684\u56DE\u590D", defaultValue: true, }, opaticyOfCitedReplies: { title: "\u88AB\u5F15\u7528\u7684\u56DE\u590D\u4E0A\u9762\u906E\u7F69\u7684\u4E0D\u900F\u660E\u5EA6", type: "select", defaultValue: "2", options: { 无遮罩: "0", 低: "1", 中: "2", 高: "3", }, }, showPreviousCitedReplies: { title: "\u88AB\u5F15\u7528\u7684\u56DE\u590D\u662F\u524D\u4E00\u4E2A\u697C\u5C42\u65F6", type: "select", defaultValue: "0", options: { 不显示: "0", 始终显示: "1", }, }, filterRepliesByUser: { title: "\u67E5\u770B\u7528\u6237\u5728\u5F53\u524D\u4E3B\u9898\u4E0B\u7684\u6240\u6709\u56DE\u590D\u4E0E\u88AB\u63D0\u53CA\u7684\u56DE\u590D", description: "\u9F20\u6807\u79FB\u81F3\u7528\u6237\u540D\uFF0C\u4F1A\u663E\u793A\u8BE5\u7528\u6237\u5728\u5F53\u524D\u4E3B\u9898\u4E0B\u7684\u6240\u6709\u56DE\u590D\u4E0E\u88AB\u63D0\u53CA\u7684\u56DE\u590D", defaultValue: true, }, loadMultiPages: { title: "\u9884\u52A0\u8F7D\u6240\u6709\u5206\u9875", defaultValue: true, }, uploadImage: { title: "\u56DE\u590D\u65F6\u4E0A\u4F20\u56FE\u7247", defaultValue: true, }, dailyCheckIn: { title: "\u6BCF\u65E5\u81EA\u52A8\u7B7E\u5230", defaultValue: true, }, lazyLoadAvatars: { title: "\u61D2\u52A0\u8F7D\u7528\u6237\u5934\u50CF\u56FE\u7247", defaultValue: false, }, quickSendThank: { title: "\u5FEB\u901F\u53D1\u9001\u611F\u8C22", defaultValue: false, }, alwaysShowThankButton: { title: "\u4E00\u76F4\u663E\u793A\u611F\u8C22\u6309\u94AE", defaultValue: false, }, quickHideReply: { title: "\u5FEB\u901F\u9690\u85CF\u56DE\u590D", defaultValue: false, }, alwaysShowHideButton: { title: "\u4E00\u76F4\u663E\u793A\u9690\u85CF\u56DE\u590D\u6309\u94AE", defaultValue: false, }, removeLocationHash: { title: "\u53BB\u6389 URL \u4E2D\u7684 #replyXX", defaultValue: true, }, stickyTopicButtons: { title: "\u4E3B\u9898\u5185\u5BB9\u5E95\u90E8\u56FA\u5B9A\u663E\u793A\u6309\u94AE\u680F", defaultValue: true, }, quickNavigation: { title: "\u53CC\u51FB\u7A7A\u767D\u5904\u5FEB\u901F\u5BFC\u822A", defaultValue: false, }, replaceFavicon: { title: "\u66F4\u6362 favicon \u56FE\u6807", type: "select", defaultValue: "default", options: { 默认: "default", GitHub: "github", 用户头像: "avatar", }, }, } var fixedReplyFloorNumbers = false async function process3() { const opaticyOfCitedReplies = getSettingsValue("opaticyOfCitedReplies") if (doc.documentElement) { doc.documentElement.dataset.vrOpaticyOfCitedReplies = opaticyOfCitedReplies } const domReady = doc.readyState === "interactive" || doc.readyState === "complete" if (doc.readyState === "complete" && getSettingsValue("dailyCheckIn")) { runOnce("dailyCheckIn", () => { setTimeout(dailyCheckIn, 1e3) }) } replaceFavicon(getSettingsValue("replaceFavicon")) if (/\/t\/\d+/.test(location.href)) { const replyElements = getReplyElements() for (const replyElement of replyElements) { if (!$(".reply_content", replyElement)) { continue } if (getSettingsValue("lazyLoadAvatars")) { lazyLoadAvatars(replyElement) } addLinkToAvatars(replyElement) if (getSettingsValue("replyWithFloorNumber")) { replyWithFloorNumber(replyElement) } if (getSettingsValue("alwaysShowThankButton")) { alwaysShowThankButton(replyElement) } if (getSettingsValue("alwaysShowHideButton")) { alwaysShowHideButton(replyElement) } if (getSettingsValue("quickSendThank")) { quickSendThank(replyElement) } if (getSettingsValue("quickHideReply")) { quickHideReply(replyElement) } addlinkToCitedFloorNumbers(replyElement) if (getSettingsValue("showCitedReplies")) { showCitedReplies( replyElement, getSettingsValue("showPreviousCitedReplies") ) } } if (domReady) { showTopReplies(replyElements, getSettingsValue("showTopReplies")) } stickyTopicButtons(getSettingsValue("stickyTopicButtons")) filterRepliesByUser(getSettingsValue("filterRepliesByUser")) if ( domReady && getSettingsValue("fixReplyFloorNumbers") && !fixedReplyFloorNumbers ) { await fixReplyFloorNumbers(replyElements) } if (domReady && getSettingsValue("uploadImage")) { uploadImage() } if (domReady && getSettingsValue("removeLocationHash")) { runOnce("main:removeLocationHash", removeLocationHash) } if (domReady && getSettingsValue("quickNavigation")) { quickNavigation() } if (doc.readyState === "complete" && getSettingsValue("loadMultiPages")) { runOnce("main:loadMultiPages", () => { setTimeout(loadMultiPages, 1e3) }) } } } async function main() { await initSettings({ id: "v2ex.rep", title: "V2EX.REP", footer: '\n

\u66F4\u6539\u8BBE\u7F6E\u540E\uFF0C\u9700\u8981\u91CD\u65B0\u52A0\u8F7D\u9875\u9762

\n

\n \n \u95EE\u9898\u53CD\u9988\n

\n

Made with \u2764\uFE0F by\n \n Pipecraft\n

', settingsTable: settingsTable2, async onValueChange() { await process3() }, }) addStyle(content_default) const resetCachedReplyElementsThenProcess = async () => { resetCachedReplyElements() await process3() } addEventListener(win, { floorNumberUpdated() { fixedReplyFloorNumbers = true if ( getSettingsValue("replyWithFloorNumber") || getSettingsValue("showCitedReplies") ) { const replyElements = getReplyElements() for (const replyElement of replyElements) { if (getSettingsValue("replyWithFloorNumber")) { replyWithFloorNumber(replyElement, true) } if (getSettingsValue("showCitedReplies")) { showCitedReplies( replyElement, getSettingsValue("showPreviousCitedReplies"), true ) } } } }, async replyElementsLengthUpdated() { await resetCachedReplyElementsThenProcess() const replyElements = getCachedReplyElements() for (const replyElement of replyElements) { if (getSettingsValue("showCitedReplies")) { showCitedReplies( replyElement, getSettingsValue("showPreviousCitedReplies"), true ) } } showTopReplies(replyElements, getSettingsValue("showTopReplies"), true) if (getSettingsValue("fixReplyFloorNumbers")) { await fixReplyFloorNumbers(replyElements) } }, }) addEventListener( doc, "readystatechange", resetCachedReplyElementsThenProcess ) await process3() const scanNodes = throttle(async () => { await process3() }, 500) addEventListener(doc, "visibilitychange", async () => { if (!doc.hidden) { await process3() } }) const observer = new MutationObserver((mutationsList) => { scanNodes() }) observer.observe($("#Main") || doc, { childList: true, subtree: true, }) } runWhenBodyExists(async () => { if (doc.documentElement.dataset.v2exRep === void 0) { doc.documentElement.dataset.v2exRep = "" await main() } }) })()