Greasy Fork is available in English.

V2EX.REP - 专注提升 V2EX 主题回复浏览体验

专注提升 V2EX 主题回复浏览体验的浏览器扩展/用户脚本。主要功能有 ✅ 修复有被 block 的用户时错位的楼层号;✅ 回复时自动带上楼层号;✅ 显示热门回复;✅ 显示被引用的回复;✅ 查看用户在当前主题下的所有回复与被提及的回复;✅ 自动预加载所有分页,支持解析显示跨页面引用;✅ 回复时上传图片;✅ 无感自动签到;✅ 懒加载用户头像图片;✅ 一直显示感谢按钮 🙏;✅ 一直显示隐藏回复按钮 🙈;✅ 快速发送感谢/快速隐藏回复(no confirm)等。

  1. // ==UserScript==
  2. // @name V2EX.REP - 专注提升 V2EX 主题回复浏览体验
  3. // @name:zh-CN V2EX.REP - 专注提升 V2EX 主题回复浏览体验
  4. // @namespace https://github.com/v2hot/v2ex.rep
  5. // @homepageURL https://github.com/v2hot/v2ex.rep#readme
  6. // @supportURL https://github.com/v2hot/v2ex.rep/issues
  7. // @version 1.5.4
  8. // @description 专注提升 V2EX 主题回复浏览体验的浏览器扩展/用户脚本。主要功能有 ✅ 修复有被 block 的用户时错位的楼层号;✅ 回复时自动带上楼层号;✅ 显示热门回复;✅ 显示被引用的回复;✅ 查看用户在当前主题下的所有回复与被提及的回复;✅ 自动预加载所有分页,支持解析显示跨页面引用;✅ 回复时上传图片;✅ 无感自动签到;✅ 懒加载用户头像图片;✅ 一直显示感谢按钮 🙏;✅ 一直显示隐藏回复按钮 🙈;✅ 快速发送感谢/快速隐藏回复(no confirm)等。
  9. // @description:zh-CN 专注提升 V2EX 主题回复浏览体验的浏览器扩展/用户脚本。主要功能有 ✅ 修复有被 block 的用户时错位的楼层号;✅ 回复时自动带上楼层号;✅ 显示热门回复;✅ 显示被引用的回复;✅ 查看用户在当前主题下的所有回复与被提及的回复;✅ 自动预加载所有分页,支持解析显示跨页面引用;✅ 回复时上传图片;✅ 无感自动签到;✅ 懒加载用户头像图片;✅ 一直显示感谢按钮 🙏;✅ 一直显示隐藏回复按钮 🙈;✅ 快速发送感谢/快速隐藏回复(no confirm)等。
  10. // @icon https://www.v2ex.com/favicon.ico
  11. // @author Pipecraft
  12. // @license MIT
  13. // @match https://*.v2ex.com/*
  14. // @run-at document-start
  15. // @grant GM.getValue
  16. // @grant GM.setValue
  17. // @grant GM_addValueChangeListener
  18. // @grant GM_removeValueChangeListener
  19. // @grant GM_addElement
  20. // @grant GM.registerMenuCommand
  21. // ==/UserScript==
  22. //
  23. ;(() => {
  24. "use strict"
  25. var listeners = {}
  26. var getValue = async (key) => {
  27. const value = await GM.getValue(key)
  28. return value && value !== "undefined" ? JSON.parse(value) : void 0
  29. }
  30. var setValue = async (key, value) => {
  31. if (value !== void 0) {
  32. const newValue = JSON.stringify(value)
  33. if (listeners[key]) {
  34. const oldValue = await GM.getValue(key)
  35. await GM.setValue(key, newValue)
  36. if (newValue !== oldValue) {
  37. for (const func of listeners[key]) {
  38. func(key, oldValue, newValue)
  39. }
  40. }
  41. } else {
  42. await GM.setValue(key, newValue)
  43. }
  44. }
  45. }
  46. var _addValueChangeListener = (key, func) => {
  47. listeners[key] = listeners[key] || []
  48. listeners[key].push(func)
  49. return () => {
  50. if (listeners[key] && listeners[key].length > 0) {
  51. for (let i2 = listeners[key].length - 1; i2 >= 0; i2--) {
  52. if (listeners[key][i2] === func) {
  53. listeners[key].splice(i2, 1)
  54. }
  55. }
  56. }
  57. }
  58. }
  59. var addValueChangeListener = (key, func) => {
  60. if (typeof GM_addValueChangeListener !== "function") {
  61. console.warn("Do not support GM_addValueChangeListener!")
  62. return _addValueChangeListener(key, func)
  63. }
  64. const listenerId = GM_addValueChangeListener(key, func)
  65. return () => {
  66. GM_removeValueChangeListener(listenerId)
  67. }
  68. }
  69. var doc = document
  70. var win = window
  71. if (typeof String.prototype.replaceAll !== "function") {
  72. String.prototype.replaceAll = String.prototype.replace
  73. }
  74. var $ = (selectors, element) => (element || doc).querySelector(selectors)
  75. var $$ = (selectors, element) => [
  76. ...(element || doc).querySelectorAll(selectors),
  77. ]
  78. var getRootElement = (type) =>
  79. type === 1
  80. ? doc.head || doc.body || doc.documentElement
  81. : type === 2
  82. ? doc.body || doc.documentElement
  83. : doc.documentElement
  84. var createElement = (tagName, attributes) =>
  85. setAttributes(doc.createElement(tagName), attributes)
  86. var addElement = (parentNode, tagName, attributes) => {
  87. if (typeof parentNode === "string") {
  88. return addElement(null, parentNode, tagName)
  89. }
  90. if (!tagName) {
  91. return
  92. }
  93. if (!parentNode) {
  94. parentNode = /^(script|link|style|meta)$/.test(tagName)
  95. ? getRootElement(1)
  96. : getRootElement(2)
  97. }
  98. if (typeof tagName === "string") {
  99. const element = createElement(tagName, attributes)
  100. parentNode.append(element)
  101. return element
  102. }
  103. setAttributes(tagName, attributes)
  104. parentNode.append(tagName)
  105. return tagName
  106. }
  107. var addEventListener = (element, type, listener, options) => {
  108. if (!element) {
  109. return
  110. }
  111. if (typeof type === "object") {
  112. for (const type1 in type) {
  113. if (Object.hasOwn(type, type1)) {
  114. element.addEventListener(type1, type[type1])
  115. }
  116. }
  117. } else if (typeof type === "string" && typeof listener === "function") {
  118. element.addEventListener(type, listener, options)
  119. }
  120. }
  121. var removeEventListener = (element, type, listener, options) => {
  122. if (!element) {
  123. return
  124. }
  125. if (typeof type === "object") {
  126. for (const type1 in type) {
  127. if (Object.hasOwn(type, type1)) {
  128. element.removeEventListener(type1, type[type1])
  129. }
  130. }
  131. } else if (typeof type === "string" && typeof listener === "function") {
  132. element.removeEventListener(type, listener, options)
  133. }
  134. }
  135. var getAttribute = (element, name) =>
  136. element ? element.getAttribute(name) : null
  137. var setAttribute = (element, name, value) =>
  138. element ? element.setAttribute(name, value) : void 0
  139. var setAttributes = (element, attributes) => {
  140. if (element && attributes) {
  141. for (const name in attributes) {
  142. if (Object.hasOwn(attributes, name)) {
  143. const value = attributes[name]
  144. if (value === void 0) {
  145. continue
  146. }
  147. if (/^(value|textContent|innerText)$/.test(name)) {
  148. element[name] = value
  149. } else if (/^(innerHTML)$/.test(name)) {
  150. element[name] = createHTML(value)
  151. } else if (name === "style") {
  152. setStyle(element, value, true)
  153. } else if (/on\w+/.test(name)) {
  154. const type = name.slice(2)
  155. addEventListener(element, type, value)
  156. } else {
  157. setAttribute(element, name, value)
  158. }
  159. }
  160. }
  161. }
  162. return element
  163. }
  164. var addAttribute = (element, name, value) => {
  165. const orgValue = getAttribute(element, name)
  166. if (!orgValue) {
  167. setAttribute(element, name, value)
  168. } else if (!orgValue.includes(value)) {
  169. setAttribute(element, name, orgValue + " " + value)
  170. }
  171. }
  172. var addClass = (element, className) => {
  173. if (!element || !element.classList) {
  174. return
  175. }
  176. element.classList.add(className)
  177. }
  178. var removeClass = (element, className) => {
  179. if (!element || !element.classList) {
  180. return
  181. }
  182. element.classList.remove(className)
  183. }
  184. var hasClass = (element, className) => {
  185. if (!element || !element.classList) {
  186. return false
  187. }
  188. return element.classList.contains(className)
  189. }
  190. var setStyle = (element, values, overwrite) => {
  191. if (!element) {
  192. return
  193. }
  194. const style = element.style
  195. if (typeof values === "string") {
  196. style.cssText = overwrite ? values : style.cssText + ";" + values
  197. return
  198. }
  199. if (overwrite) {
  200. style.cssText = ""
  201. }
  202. for (const key in values) {
  203. if (Object.hasOwn(values, key)) {
  204. style[key] = values[key].replace("!important", "")
  205. }
  206. }
  207. }
  208. var throttle = (func, interval) => {
  209. let timeoutId2 = null
  210. let next = false
  211. const handler = (...args) => {
  212. if (timeoutId2) {
  213. next = true
  214. } else {
  215. func.apply(void 0, args)
  216. timeoutId2 = setTimeout(() => {
  217. timeoutId2 = null
  218. if (next) {
  219. next = false
  220. handler()
  221. }
  222. }, interval)
  223. }
  224. }
  225. return handler
  226. }
  227. if (typeof Object.hasOwn !== "function") {
  228. Object.hasOwn = (instance, prop) =>
  229. Object.prototype.hasOwnProperty.call(instance, prop)
  230. }
  231. var actionHref = "javascript:;"
  232. var getOffsetPosition = (element, referElement) => {
  233. const position = { top: 0, left: 0 }
  234. referElement = referElement || doc.body
  235. while (element && element !== referElement) {
  236. position.top += element.offsetTop
  237. position.left += element.offsetLeft
  238. element = element.offsetParent
  239. }
  240. return position
  241. }
  242. var runOnceCache = {}
  243. var runOnce = async (key, func) => {
  244. if (Object.hasOwn(runOnceCache, key)) {
  245. return runOnceCache[key]
  246. }
  247. const result = await func()
  248. if (key) {
  249. runOnceCache[key] = result
  250. }
  251. return result
  252. }
  253. var cacheStore = {}
  254. var makeKey = (key) => (Array.isArray(key) ? key.join(":") : key)
  255. var cache = {
  256. get: (key) => cacheStore[makeKey(key)],
  257. add(key, value) {
  258. cacheStore[makeKey(key)] = value
  259. },
  260. }
  261. var sleep = async (time) => {
  262. return new Promise((resolve) => {
  263. setTimeout(() => {
  264. resolve(1)
  265. }, time)
  266. })
  267. }
  268. var parseInt10 = (number, defaultValue) => {
  269. if (typeof number === "number" && !Number.isNaN(number)) {
  270. return number
  271. }
  272. if (typeof defaultValue !== "number") {
  273. defaultValue = Number.NaN
  274. }
  275. if (!number) {
  276. return defaultValue
  277. }
  278. const result = Number.parseInt(number, 10)
  279. return Number.isNaN(result) ? defaultValue : result
  280. }
  281. var rootFuncArray = []
  282. var headFuncArray = []
  283. var bodyFuncArray = []
  284. var headBodyObserver
  285. var startObserveHeadBodyExists = () => {
  286. if (headBodyObserver) {
  287. return
  288. }
  289. headBodyObserver = new MutationObserver(() => {
  290. if (doc.head && doc.body) {
  291. headBodyObserver.disconnect()
  292. }
  293. if (doc.documentElement && rootFuncArray.length > 0) {
  294. for (const func of rootFuncArray) {
  295. func()
  296. }
  297. rootFuncArray.length = 0
  298. }
  299. if (doc.head && headFuncArray.length > 0) {
  300. for (const func of headFuncArray) {
  301. func()
  302. }
  303. headFuncArray.length = 0
  304. }
  305. if (doc.body && bodyFuncArray.length > 0) {
  306. for (const func of bodyFuncArray) {
  307. func()
  308. }
  309. bodyFuncArray.length = 0
  310. }
  311. })
  312. headBodyObserver.observe(doc, {
  313. childList: true,
  314. subtree: true,
  315. })
  316. }
  317. var runWhenHeadExists = (func) => {
  318. if (!doc.head) {
  319. headFuncArray.push(func)
  320. startObserveHeadBodyExists()
  321. return
  322. }
  323. func()
  324. }
  325. var runWhenBodyExists = (func) => {
  326. if (!doc.body) {
  327. bodyFuncArray.push(func)
  328. startObserveHeadBodyExists()
  329. return
  330. }
  331. func()
  332. }
  333. var runWhenDomReady = (func) => {
  334. if (doc.readyState === "interactive" || doc.readyState === "complete") {
  335. return func()
  336. }
  337. const handler = () => {
  338. if (doc.readyState === "interactive" || doc.readyState === "complete") {
  339. func()
  340. removeEventListener(doc, "readystatechange", handler)
  341. }
  342. }
  343. addEventListener(doc, "readystatechange", handler)
  344. }
  345. var isVisible = (element) => {
  346. if (typeof element.checkVisibility === "function") {
  347. return element.checkVisibility()
  348. }
  349. return element.offsetParent !== null
  350. }
  351. var isTouchScreen = () => "ontouchstart" in win
  352. var escapeHTMLPolicy =
  353. typeof trustedTypes !== "undefined" &&
  354. typeof trustedTypes.createPolicy === "function"
  355. ? trustedTypes.createPolicy("beuEscapePolicy", {
  356. createHTML: (string) => string,
  357. })
  358. : void 0
  359. var createHTML = (html) => {
  360. return escapeHTMLPolicy ? escapeHTMLPolicy.createHTML(html) : html
  361. }
  362. var addElement2 =
  363. typeof GM_addElement === "function"
  364. ? (parentNode, tagName, attributes) => {
  365. if (typeof parentNode === "string") {
  366. return addElement2(null, parentNode, tagName)
  367. }
  368. if (!tagName) {
  369. return
  370. }
  371. if (!parentNode) {
  372. parentNode = /^(script|link|style|meta)$/.test(tagName)
  373. ? getRootElement(1)
  374. : getRootElement(2)
  375. }
  376. if (typeof tagName === "string") {
  377. let attributes2
  378. if (attributes) {
  379. const entries1 = []
  380. const entries2 = []
  381. for (const entry of Object.entries(attributes)) {
  382. if (/^(on\w+|innerHTML)$/.test(entry[0])) {
  383. entries2.push(entry)
  384. } else {
  385. entries1.push(entry)
  386. }
  387. }
  388. attributes = Object.fromEntries(entries1)
  389. attributes2 = Object.fromEntries(entries2)
  390. }
  391. const element = GM_addElement(null, tagName, attributes)
  392. setAttributes(element, attributes2)
  393. parentNode.append(element)
  394. return element
  395. }
  396. setAttributes(tagName, attributes)
  397. parentNode.append(tagName)
  398. return tagName
  399. }
  400. : addElement
  401. var addStyle = (styleText) =>
  402. addElement2(null, "style", { textContent: styleText })
  403. var registerMenuCommand = (name, callback, accessKey) => {
  404. if (window !== top) {
  405. return
  406. }
  407. if (typeof GM.registerMenuCommand !== "function") {
  408. console.warn("Do not support GM.registerMenuCommand!")
  409. return
  410. }
  411. GM.registerMenuCommand(name, callback, accessKey)
  412. }
  413. var style_default =
  414. '#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}}'
  415. function createSwitch(options = {}) {
  416. const container = createElement("label", { class: "container" })
  417. const checkbox = createElement(
  418. "input",
  419. options.checked ? { type: "checkbox", checked: "" } : { type: "checkbox" }
  420. )
  421. addElement2(container, checkbox)
  422. const switchElm = createElement("span", { class: "switch" })
  423. addElement2(switchElm, "span", { class: "slider" })
  424. addElement2(container, switchElm)
  425. if (options.onchange) {
  426. addEventListener(checkbox, "change", options.onchange)
  427. }
  428. return container
  429. }
  430. function createSwitchOption(icon, text, options) {
  431. if (typeof text !== "string") {
  432. return createSwitchOption(void 0, icon, text)
  433. }
  434. const div = createElement("div", { class: "switch_option bes_option" })
  435. if (icon) {
  436. addElement2(div, "img", { src: icon, class: "bes_icon" })
  437. }
  438. addElement2(div, "span", { textContent: text, class: "bes_title" })
  439. div.append(createSwitch(options))
  440. return div
  441. }
  442. var besVersion = 51
  443. var openButton =
  444. '<svg viewBox="0 0 60.2601318359375 84.8134765625" version="1.1" xmlns="http://www.w3.org/2000/svg" class=" glyph-box" style="height: 9.62969px; width: 6.84191px;"><g transform="matrix(1 0 0 1 -6.194965820312518 77.63671875)"><path d="M66.4551-35.2539C66.4551-36.4746 65.9668-37.5977 65.0391-38.4766L26.3672-76.3672C25.4883-77.1973 24.4141-77.6367 23.1445-77.6367C20.6543-77.6367 18.7012-75.7324 18.7012-73.1934C18.7012-71.9727 19.1895-70.8496 19.9707-70.0195L55.5176-35.2539L19.9707-0.488281C19.1895 0.341797 18.7012 1.41602 18.7012 2.68555C18.7012 5.22461 20.6543 7.12891 23.1445 7.12891C24.4141 7.12891 25.4883 6.68945 26.3672 5.81055L65.0391-32.0312C65.9668-32.959 66.4551-34.0332 66.4551-35.2539Z"></path></g></svg>'
  445. var openInNewTabButton =
  446. '<svg viewBox="0 0 72.127685546875 72.2177734375" version="1.1" xmlns="http://www.w3.org/2000/svg" class=" glyph-box" style="height: 8.19958px; width: 8.18935px;"><g transform="matrix(1 0 0 1 -12.451127929687573 71.3388671875)"><path d="M84.5703-17.334L84.5215-66.4551C84.5215-69.2383 82.7148-71.1914 79.7852-71.1914L30.6641-71.1914C27.9297-71.1914 26.0742-69.0918 26.0742-66.748C26.0742-64.4043 28.1738-62.4023 30.4688-62.4023L47.4609-62.4023L71.2891-63.1836L62.207-55.2246L13.8184-6.73828C12.9395-5.85938 12.4512-4.73633 12.4512-3.66211C12.4512-1.31836 14.5508 0.878906 16.9922 0.878906C18.1152 0.878906 19.1895 0.488281 20.0684-0.439453L68.5547-48.877L76.6113-58.0078L75.7324-35.2051L75.7324-17.1387C75.7324-14.8438 77.7344-12.6953 80.127-12.6953C82.4707-12.6953 84.5703-14.6973 84.5703-17.334Z"></path></g></svg>'
  447. var settingButton =
  448. '<svg viewBox="0 0 16 16" version="1.1">\n<path d="M8 0a8.2 8.2 0 0 1 .701.031C9.444.095 9.99.645 10.16 1.29l.288 1.107c.018.066.079.158.212.224.231.114.454.243.668.386.123.082.233.09.299.071l1.103-.303c.644-.176 1.392.021 1.82.63.27.385.506.792.704 1.218.315.675.111 1.422-.364 1.891l-.814.806c-.049.048-.098.147-.088.294.016.257.016.515 0 .772-.01.147.038.246.088.294l.814.806c.475.469.679 1.216.364 1.891a7.977 7.977 0 0 1-.704 1.217c-.428.61-1.176.807-1.82.63l-1.102-.302c-.067-.019-.177-.011-.3.071a5.909 5.909 0 0 1-.668.386c-.133.066-.194.158-.211.224l-.29 1.106c-.168.646-.715 1.196-1.458 1.26a8.006 8.006 0 0 1-1.402 0c-.743-.064-1.289-.614-1.458-1.26l-.289-1.106c-.018-.066-.079-.158-.212-.224a5.738 5.738 0 0 1-.668-.386c-.123-.082-.233-.09-.299-.071l-1.103.303c-.644.176-1.392-.021-1.82-.63a8.12 8.12 0 0 1-.704-1.218c-.315-.675-.111-1.422.363-1.891l.815-.806c.05-.048.098-.147.088-.294a6.214 6.214 0 0 1 0-.772c.01-.147-.038-.246-.088-.294l-.815-.806C.635 6.045.431 5.298.746 4.623a7.92 7.92 0 0 1 .704-1.217c.428-.61 1.176-.807 1.82-.63l1.102.302c.067.019.177.011.3-.071.214-.143.437-.272.668-.386.133-.066.194-.158.211-.224l.29-1.106C6.009.645 6.556.095 7.299.03 7.53.01 7.764 0 8 0Zm-.571 1.525c-.036.003-.108.036-.137.146l-.289 1.105c-.147.561-.549.967-.998 1.189-.173.086-.34.183-.5.29-.417.278-.97.423-1.529.27l-1.103-.303c-.109-.03-.175.016-.195.045-.22.312-.412.644-.573.99-.014.031-.021.11.059.19l.815.806c.411.406.562.957.53 1.456a4.709 4.709 0 0 0 0 .582c.032.499-.119 1.05-.53 1.456l-.815.806c-.081.08-.073.159-.059.19.162.346.353.677.573.989.02.03.085.076.195.046l1.102-.303c.56-.153 1.113-.008 1.53.27.161.107.328.204.501.29.447.222.85.629.997 1.189l.289 1.105c.029.109.101.143.137.146a6.6 6.6 0 0 0 1.142 0c.036-.003.108-.036.137-.146l.289-1.105c.147-.561.549-.967.998-1.189.173-.086.34-.183.5-.29.417-.278.97-.423 1.529-.27l1.103.303c.109.029.175-.016.195-.045.22-.313.411-.644.573-.99.014-.031.021-.11-.059-.19l-.815-.806c-.411-.406-.562-.957-.53-1.456a4.709 4.709 0 0 0 0-.582c-.032-.499.119-1.05.53-1.456l.815-.806c.081-.08.073-.159.059-.19a6.464 6.464 0 0 0-.573-.989c-.02-.03-.085-.076-.195-.046l-1.102.303c-.56.153-1.113.008-1.53-.27a4.44 4.44 0 0 0-.501-.29c-.447-.222-.85-.629-.997-1.189l-.289-1.105c-.029-.11-.101-.143-.137-.146a6.6 6.6 0 0 0-1.142 0ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8Z"></path>\n</svg>'
  449. function initI18n(messageMaps, language) {
  450. language = (language || navigator.language).toLowerCase()
  451. const language2 = language.slice(0, 2)
  452. let messagesDefault
  453. let messagesLocal
  454. for (const entry of Object.entries(messageMaps)) {
  455. const langs = new Set(
  456. entry[0]
  457. .toLowerCase()
  458. .split(",")
  459. .map((v) => v.trim())
  460. )
  461. const value = entry[1]
  462. if (langs.has(language)) {
  463. messagesLocal = value
  464. }
  465. if (langs.has(language2) && !messagesLocal) {
  466. messagesLocal = value
  467. }
  468. if (langs.has("en")) {
  469. messagesDefault = value
  470. }
  471. if (langs.has("en-us") && !messagesDefault) {
  472. messagesDefault = value
  473. }
  474. }
  475. if (!messagesLocal) {
  476. messagesLocal = {}
  477. }
  478. if (!messagesDefault || messagesDefault === messagesLocal) {
  479. messagesDefault = {}
  480. }
  481. return function (key, ...parameters) {
  482. let text = messagesLocal[key] || messagesDefault[key] || key
  483. if (parameters && parameters.length > 0 && text !== key) {
  484. for (let i2 = 0; i2 < parameters.length; i2++) {
  485. text = text.replaceAll(
  486. new RegExp("\\{".concat(i2 + 1, "\\}"), "g"),
  487. String(parameters[i2])
  488. )
  489. }
  490. }
  491. return text
  492. }
  493. }
  494. var messages = {
  495. "settings.title": "Settings",
  496. "settings.otherExtensions": "Other Extensions",
  497. "settings.displaySettingsButtonInSideMenu":
  498. "Display Settings Button in Side Menu",
  499. "settings.menu.settings": "\u2699\uFE0F Settings",
  500. "settings.extensions.utags.title":
  501. "\u{1F3F7}\uFE0F UTags - Add usertags to links",
  502. "settings.extensions.links-helper.title": "\u{1F517} Links Helper",
  503. "settings.extensions.v2ex.rep.title":
  504. "V2EX.REP - \u4E13\u6CE8\u63D0\u5347 V2EX \u4E3B\u9898\u56DE\u590D\u6D4F\u89C8\u4F53\u9A8C",
  505. "settings.extensions.v2ex.min.title":
  506. "v2ex.min - V2EX Minimalist (\u6781\u7B80\u98CE\u683C)",
  507. "settings.extensions.replace-ugly-avatars.title": "Replace Ugly Avatars",
  508. "settings.extensions.more-by-pipecraft.title":
  509. "Find more useful userscripts",
  510. }
  511. var en_default = messages
  512. var messages2 = {
  513. "settings.title": "\u8BBE\u7F6E",
  514. "settings.otherExtensions": "\u5176\u4ED6\u6269\u5C55",
  515. "settings.displaySettingsButtonInSideMenu":
  516. "\u5728\u4FA7\u8FB9\u680F\u83DC\u5355\u4E2D\u663E\u793A\u8BBE\u7F6E\u6309\u94AE",
  517. "settings.menu.settings": "\u2699\uFE0F \u8BBE\u7F6E",
  518. "settings.extensions.utags.title":
  519. "\u{1F3F7}\uFE0F \u5C0F\u9C7C\u6807\u7B7E (UTags) - \u4E3A\u94FE\u63A5\u6DFB\u52A0\u7528\u6237\u6807\u7B7E",
  520. "settings.extensions.links-helper.title":
  521. "\u{1F517} \u94FE\u63A5\u52A9\u624B",
  522. "settings.extensions.v2ex.rep.title":
  523. "V2EX.REP - \u4E13\u6CE8\u63D0\u5347 V2EX \u4E3B\u9898\u56DE\u590D\u6D4F\u89C8\u4F53\u9A8C",
  524. "settings.extensions.v2ex.min.title":
  525. "v2ex.min - V2EX \u6781\u7B80\u98CE\u683C",
  526. "settings.extensions.replace-ugly-avatars.title":
  527. "\u8D50\u4F60\u4E2A\u5934\u50CF\u5427",
  528. "settings.extensions.more-by-pipecraft.title":
  529. "\u66F4\u591A\u6709\u8DA3\u7684\u811A\u672C",
  530. }
  531. var zh_cn_default = messages2
  532. var i = initI18n({
  533. "en,en-US": en_default,
  534. "zh,zh-CN": zh_cn_default,
  535. })
  536. var lang = navigator.language
  537. var locale
  538. if (lang === "zh-TW" || lang === "zh-HK") {
  539. locale = "zh-TW"
  540. } else if (lang.includes("zh")) {
  541. locale = "zh-CN"
  542. } else {
  543. locale = "en"
  544. }
  545. var relatedExtensions = [
  546. {
  547. id: "utags",
  548. title: i("settings.extensions.utags.title"),
  549. url: "https://greasyfork.org/".concat(
  550. locale,
  551. "/scripts/460718-utags-add-usertags-to-links"
  552. ),
  553. },
  554. {
  555. id: "links-helper",
  556. title: i("settings.extensions.links-helper.title"),
  557. description:
  558. "\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",
  559. url: "https://greasyfork.org/".concat(
  560. locale,
  561. "/scripts/464541-links-helper"
  562. ),
  563. },
  564. {
  565. id: "v2ex.rep",
  566. title: i("settings.extensions.v2ex.rep.title"),
  567. url: "https://greasyfork.org/".concat(
  568. locale,
  569. "/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"
  570. ),
  571. },
  572. {
  573. id: "v2ex.min",
  574. title: i("settings.extensions.v2ex.min.title"),
  575. url: "https://greasyfork.org/".concat(
  576. locale,
  577. "/scripts/463552-v2ex-min-v2ex-%E6%9E%81%E7%AE%80%E9%A3%8E%E6%A0%BC"
  578. ),
  579. },
  580. {
  581. id: "replace-ugly-avatars",
  582. title: i("settings.extensions.replace-ugly-avatars.title"),
  583. url: "https://greasyfork.org/".concat(
  584. locale,
  585. "/scripts/472616-replace-ugly-avatars"
  586. ),
  587. },
  588. {
  589. id: "more-by-pipecraft",
  590. title: i("settings.extensions.more-by-pipecraft.title"),
  591. url: "https://greasyfork.org/".concat(locale, "/users/1030884-pipecraft"),
  592. },
  593. ]
  594. var getInstalledExtesionList = () => {
  595. return $(".extension_list_container .installed_extension_list")
  596. }
  597. var getRelatedExtesionList = () => {
  598. return $(".extension_list_container .related_extension_list")
  599. }
  600. var isInstalledExtension = (id) => {
  601. const list = getInstalledExtesionList()
  602. if (!list) {
  603. return false
  604. }
  605. const installed = $('[data-extension-id="'.concat(id, '"]'), list)
  606. return Boolean(installed)
  607. }
  608. var addCurrentExtension = (extension) => {
  609. const list = getInstalledExtesionList()
  610. if (!list) {
  611. return
  612. }
  613. if (isInstalledExtension(extension.id)) {
  614. return
  615. }
  616. const element = createInstalledExtension(extension)
  617. list.append(element)
  618. const list2 = getRelatedExtesionList()
  619. if (list2) {
  620. updateRelatedExtensions(list2)
  621. }
  622. }
  623. var activeExtension = (id) => {
  624. const list = getInstalledExtesionList()
  625. if (!list) {
  626. return false
  627. }
  628. for (const element of $$(".active", list)) {
  629. removeClass(element, "active")
  630. }
  631. const installed = $('[data-extension-id="'.concat(id, '"]'), list)
  632. if (installed) {
  633. addClass(installed, "active")
  634. }
  635. }
  636. var activeExtensionList = () => {
  637. const extensionListContainer = $(".extension_list_container")
  638. if (extensionListContainer) {
  639. addClass(extensionListContainer, "bes_active")
  640. }
  641. }
  642. var deactiveExtensionList = () => {
  643. const extensionListContainer = $(".extension_list_container")
  644. if (extensionListContainer) {
  645. removeClass(extensionListContainer, "bes_active")
  646. }
  647. }
  648. var createInstalledExtension = (installedExtension) => {
  649. const div = createElement("div", {
  650. class: "installed_extension",
  651. "data-extension-id": installedExtension.id,
  652. })
  653. const a = addElement2(div, "a", {
  654. onclick: installedExtension.onclick,
  655. })
  656. addElement2(a, "span", {
  657. textContent: installedExtension.title,
  658. })
  659. const svg = addElement2(a, "svg")
  660. svg.outerHTML = createHTML(openButton)
  661. return div
  662. }
  663. var updateRelatedExtensions = (container) => {
  664. const relatedExtensionElements = $$("[data-extension-id]", container)
  665. if (relatedExtensionElements.length > 0) {
  666. for (const relatedExtensionElement of relatedExtensionElements) {
  667. if (
  668. isInstalledExtension(
  669. relatedExtensionElement.dataset.extensionId || "noid"
  670. )
  671. ) {
  672. relatedExtensionElement.remove()
  673. }
  674. }
  675. } else {
  676. container.innerHTML = createHTML("")
  677. }
  678. for (const relatedExtension of relatedExtensions) {
  679. if (
  680. isInstalledExtension(relatedExtension.id) ||
  681. $('[data-extension-id="'.concat(relatedExtension.id, '"]'), container)
  682. ) {
  683. continue
  684. }
  685. if ($$("[data-extension-id]", container).length >= 4) {
  686. return
  687. }
  688. const div4 = addElement2(container, "div", {
  689. class: "related_extension",
  690. "data-extension-id": relatedExtension.id,
  691. })
  692. const a = addElement2(div4, "a", {
  693. href: relatedExtension.url,
  694. target: "_blank",
  695. })
  696. addElement2(a, "span", {
  697. textContent: relatedExtension.title,
  698. })
  699. const svg = addElement2(a, "svg")
  700. svg.outerHTML = createHTML(openInNewTabButton)
  701. }
  702. }
  703. function createExtensionList(installedExtensions) {
  704. const div = createElement("div", {
  705. class: "extension_list_container thin_scrollbar",
  706. })
  707. addElement2(div, "h1", { textContent: i("settings.title") })
  708. const div2 = addElement2(div, "div", {
  709. class: "installed_extension_list",
  710. })
  711. for (const installedExtension of installedExtensions) {
  712. if (isInstalledExtension(installedExtension.id)) {
  713. continue
  714. }
  715. const element = createInstalledExtension(installedExtension)
  716. div2.append(element)
  717. }
  718. addElement2(div, "h2", { textContent: i("settings.otherExtensions") })
  719. const div3 = addElement2(div, "div", {
  720. class: "related_extension_list",
  721. })
  722. updateRelatedExtensions(div3)
  723. return div
  724. }
  725. var prefix = "browser_extension_settings_"
  726. var randomId = String(Math.round(Math.random() * 1e4))
  727. var settingsContainerId = prefix + "container_" + randomId
  728. var settingsElementId = prefix + "main_" + randomId
  729. var getSettingsElement = () => $("#" + settingsElementId)
  730. var getSettingsStyle = () =>
  731. style_default
  732. .replaceAll(/browser_extension_settings_container/gm, settingsContainerId)
  733. .replaceAll(/browser_extension_settings_main/gm, settingsElementId)
  734. var storageKey = "settings"
  735. var settingsOptions
  736. var settingsTable = {}
  737. var settings = {}
  738. async function getSettings() {
  739. var _a
  740. return (_a = await getValue(storageKey)) != null ? _a : {}
  741. }
  742. async function saveSettingsValue(key, value) {
  743. const settings2 = await getSettings()
  744. settings2[key] =
  745. settingsTable[key] && settingsTable[key].defaultValue === value
  746. ? void 0
  747. : value
  748. await setValue(storageKey, settings2)
  749. }
  750. function getSettingsValue(key) {
  751. var _a
  752. return Object.hasOwn(settings, key)
  753. ? settings[key]
  754. : (_a = settingsTable[key]) == null
  755. ? void 0
  756. : _a.defaultValue
  757. }
  758. var closeModal = () => {
  759. const settingsContainer = getSettingsContainer()
  760. if (settingsContainer) {
  761. settingsContainer.style.display = "none"
  762. }
  763. removeEventListener(document, "click", onDocumentClick, true)
  764. removeEventListener(document, "keydown", onDocumentKeyDown, true)
  765. }
  766. var onDocumentClick = (event) => {
  767. const target = event.target
  768. if (
  769. target == null ? void 0 : target.closest(".".concat(prefix, "container"))
  770. ) {
  771. return
  772. }
  773. closeModal()
  774. }
  775. var onDocumentKeyDown = (event) => {
  776. if (event.defaultPrevented) {
  777. return
  778. }
  779. if (event.key === "Escape") {
  780. closeModal()
  781. event.preventDefault()
  782. }
  783. }
  784. async function updateOptions() {
  785. if (!getSettingsElement()) {
  786. return
  787. }
  788. for (const key in settingsTable) {
  789. if (Object.hasOwn(settingsTable, key)) {
  790. const item = settingsTable[key]
  791. const type = item.type || "switch"
  792. switch (type) {
  793. case "switch": {
  794. const checkbox = $(
  795. "#"
  796. .concat(
  797. settingsElementId,
  798. ' .option_groups .switch_option[data-key="'
  799. )
  800. .concat(key, '"] input')
  801. )
  802. if (checkbox) {
  803. checkbox.checked = getSettingsValue(key)
  804. }
  805. break
  806. }
  807. case "select": {
  808. const options = $$(
  809. "#"
  810. .concat(
  811. settingsElementId,
  812. ' .option_groups .select_option[data-key="'
  813. )
  814. .concat(key, '"] .bes_select option')
  815. )
  816. for (const option of options) {
  817. option.selected = option.value === String(getSettingsValue(key))
  818. }
  819. break
  820. }
  821. case "textarea": {
  822. const textArea = $(
  823. "#"
  824. .concat(
  825. settingsElementId,
  826. ' .option_groups textarea[data-key="'
  827. )
  828. .concat(key, '"]')
  829. )
  830. if (textArea) {
  831. textArea.value = getSettingsValue(key)
  832. }
  833. break
  834. }
  835. default: {
  836. break
  837. }
  838. }
  839. }
  840. }
  841. if (typeof settingsOptions.onViewUpdate === "function") {
  842. const settingsMain = createSettingsElement()
  843. settingsOptions.onViewUpdate(settingsMain)
  844. }
  845. }
  846. function getSettingsContainer() {
  847. const container = $(".".concat(prefix, "container"))
  848. if (container) {
  849. const theVersion = parseInt10(container.dataset.besVersion, 0)
  850. if (theVersion < besVersion) {
  851. container.id = settingsContainerId
  852. container.dataset.besVersion = String(besVersion)
  853. }
  854. return container
  855. }
  856. return addElement2(doc.body, "div", {
  857. id: settingsContainerId,
  858. class: "".concat(prefix, "container"),
  859. "data-bes-version": besVersion,
  860. style: "display: none;",
  861. })
  862. }
  863. function getSettingsWrapper() {
  864. const container = getSettingsContainer()
  865. return (
  866. $(".".concat(prefix, "wrapper"), container) ||
  867. addElement2(container, "div", {
  868. class: "".concat(prefix, "wrapper"),
  869. })
  870. )
  871. }
  872. function initExtensionList() {
  873. const wrapper = getSettingsWrapper()
  874. if (!$(".extension_list_container", wrapper)) {
  875. const list = createExtensionList([])
  876. wrapper.append(list)
  877. }
  878. addCurrentExtension({
  879. id: settingsOptions.id,
  880. title: settingsOptions.title,
  881. onclick: showSettings,
  882. })
  883. }
  884. function createSettingsElement() {
  885. let settingsMain = getSettingsElement()
  886. if (!settingsMain) {
  887. const wrapper = getSettingsWrapper()
  888. for (const element of $$(".".concat(prefix, "main"))) {
  889. element.remove()
  890. }
  891. settingsMain = addElement2(wrapper, "div", {
  892. id: settingsElementId,
  893. class: "".concat(prefix, "main thin_scrollbar"),
  894. })
  895. addElement2(settingsMain, "a", {
  896. textContent: "Settings",
  897. class: "navigation_go_previous",
  898. onclick() {
  899. activeExtensionList()
  900. },
  901. })
  902. if (settingsOptions.title) {
  903. addElement2(settingsMain, "h2", { textContent: settingsOptions.title })
  904. }
  905. const optionGroups = []
  906. const getOptionGroup = (index) => {
  907. if (index > optionGroups.length) {
  908. for (let i2 = optionGroups.length; i2 < index; i2++) {
  909. optionGroups.push(
  910. addElement2(settingsMain, "div", {
  911. class: "option_groups",
  912. })
  913. )
  914. }
  915. }
  916. return optionGroups[index - 1]
  917. }
  918. for (const key in settingsTable) {
  919. if (Object.hasOwn(settingsTable, key)) {
  920. const item = settingsTable[key]
  921. const type = item.type || "switch"
  922. const group = item.group || 1
  923. const optionGroup = getOptionGroup(group)
  924. switch (type) {
  925. case "switch": {
  926. const switchOption = createSwitchOption(item.icon, item.title, {
  927. async onchange(event) {
  928. const checkbox = event.target
  929. if (checkbox) {
  930. let result = true
  931. if (typeof item.onConfirmChange === "function") {
  932. result = item.onConfirmChange(checkbox.checked)
  933. }
  934. if (result) {
  935. await saveSettingsValue(key, checkbox.checked)
  936. } else {
  937. checkbox.checked = !checkbox.checked
  938. }
  939. }
  940. },
  941. })
  942. switchOption.dataset.key = key
  943. addElement2(optionGroup, switchOption)
  944. break
  945. }
  946. case "textarea": {
  947. let timeoutId2
  948. const div = addElement2(optionGroup, "div", {
  949. class: "bes_textarea",
  950. })
  951. addElement2(div, "textarea", {
  952. "data-key": key,
  953. placeholder: item.placeholder || "",
  954. onkeyup(event) {
  955. const textArea = event.target
  956. if (timeoutId2) {
  957. clearTimeout(timeoutId2)
  958. timeoutId2 = void 0
  959. }
  960. timeoutId2 = setTimeout(async () => {
  961. if (textArea) {
  962. await saveSettingsValue(key, textArea.value.trim())
  963. }
  964. }, 100)
  965. },
  966. })
  967. break
  968. }
  969. case "action": {
  970. addElement2(optionGroup, "a", {
  971. class: "action",
  972. textContent: item.title,
  973. onclick: item.onclick,
  974. })
  975. break
  976. }
  977. case "externalLink": {
  978. const div4 = addElement2(optionGroup, "div", {
  979. class: "bes_external_link",
  980. })
  981. addElement2(div4, "a", {
  982. textContent: item.title,
  983. href: item.url,
  984. target: "_blank",
  985. })
  986. break
  987. }
  988. case "select": {
  989. const div = addElement2(optionGroup, "div", {
  990. class: "select_option bes_option",
  991. "data-key": key,
  992. })
  993. if (item.icon) {
  994. addElement2(div, "img", { src: item.icon, class: "bes_icon" })
  995. }
  996. addElement2(div, "span", {
  997. textContent: item.title,
  998. class: "bes_title",
  999. })
  1000. const select = addElement2(div, "select", {
  1001. class: "bes_select",
  1002. async onchange() {
  1003. await saveSettingsValue(key, select.value)
  1004. },
  1005. })
  1006. for (const option of Object.entries(item.options)) {
  1007. addElement2(select, "option", {
  1008. textContent: option[0],
  1009. value: option[1],
  1010. })
  1011. }
  1012. break
  1013. }
  1014. case "tip": {
  1015. const tip = addElement2(optionGroup, "div", {
  1016. class: "bes_tip",
  1017. })
  1018. addElement2(tip, "a", {
  1019. class: "bes_tip_anchor",
  1020. textContent: item.title,
  1021. })
  1022. const tipContent = addElement2(tip, "div", {
  1023. class: "bes_tip_content",
  1024. innerHTML: createHTML(item.tipContent),
  1025. })
  1026. break
  1027. }
  1028. }
  1029. }
  1030. }
  1031. if (settingsOptions.footer) {
  1032. const footer = addElement2(settingsMain, "footer")
  1033. footer.innerHTML = createHTML(
  1034. typeof settingsOptions.footer === "string"
  1035. ? settingsOptions.footer
  1036. : '<p>Made with \u2764\uFE0F by\n <a href="https://www.pipecraft.net/" target="_blank">\n Pipecraft\n </a></p>'
  1037. )
  1038. }
  1039. }
  1040. return settingsMain
  1041. }
  1042. function addSideMenu() {
  1043. if (!getSettingsValue("displaySettingsButtonInSideMenu")) {
  1044. return
  1045. }
  1046. const menu =
  1047. $("#browser_extension_side_menu") ||
  1048. addElement2(doc.body, "div", {
  1049. id: "browser_extension_side_menu",
  1050. "data-bes-version": besVersion,
  1051. })
  1052. const button = $("button[data-bes-version]", menu)
  1053. if (button) {
  1054. const theVersion = parseInt10(button.dataset.besVersion, 0)
  1055. if (theVersion >= besVersion) {
  1056. return
  1057. }
  1058. button.remove()
  1059. }
  1060. addElement2(menu, "button", {
  1061. type: "button",
  1062. "data-bes-version": besVersion,
  1063. title: i("settings.menu.settings"),
  1064. onclick() {
  1065. setTimeout(showSettings, 1)
  1066. },
  1067. innerHTML: settingButton,
  1068. })
  1069. }
  1070. function addCommonSettings(settingsTable3) {
  1071. let maxGroup = 0
  1072. for (const key in settingsTable3) {
  1073. if (Object.hasOwn(settingsTable3, key)) {
  1074. const item = settingsTable3[key]
  1075. const group = item.group || 1
  1076. if (group > maxGroup) {
  1077. maxGroup = group
  1078. }
  1079. }
  1080. }
  1081. settingsTable3.displaySettingsButtonInSideMenu = {
  1082. title: i("settings.displaySettingsButtonInSideMenu"),
  1083. defaultValue: !(
  1084. typeof GM === "object" && typeof GM.registerMenuCommand === "function"
  1085. ),
  1086. group: maxGroup + 1,
  1087. }
  1088. }
  1089. function handleShowSettingsUrl() {
  1090. if (location.hash === "#bes-show-settings") {
  1091. setTimeout(showSettings, 100)
  1092. }
  1093. }
  1094. async function showSettings() {
  1095. const settingsContainer = getSettingsContainer()
  1096. const settingsMain = createSettingsElement()
  1097. await updateOptions()
  1098. settingsContainer.style.display = "block"
  1099. addEventListener(document, "click", onDocumentClick, true)
  1100. addEventListener(document, "keydown", onDocumentKeyDown, true)
  1101. activeExtension(settingsOptions.id)
  1102. deactiveExtensionList()
  1103. }
  1104. var initSettings = async (options) => {
  1105. settingsOptions = options
  1106. settingsTable = options.settingsTable || {}
  1107. addCommonSettings(settingsTable)
  1108. addValueChangeListener(storageKey, async () => {
  1109. settings = await getSettings()
  1110. await updateOptions()
  1111. addSideMenu()
  1112. if (typeof options.onValueChange === "function") {
  1113. options.onValueChange()
  1114. }
  1115. })
  1116. settings = await getSettings()
  1117. runWhenHeadExists(() => {
  1118. addStyle(getSettingsStyle())
  1119. })
  1120. runWhenDomReady(() => {
  1121. initExtensionList()
  1122. addSideMenu()
  1123. })
  1124. registerMenuCommand(i("settings.menu.settings"), showSettings, "o")
  1125. handleShowSettingsUrl()
  1126. }
  1127. var content_default =
  1128. '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}'
  1129. var addLinkToAvatars = (replyElement) => {
  1130. var _a, _b
  1131. const memberLink = $('a[href^="/member/"]', replyElement)
  1132. if (
  1133. memberLink &&
  1134. ((_a = memberLink.firstChild) == null ? void 0 : _a.tagName) === "IMG"
  1135. ) {
  1136. return
  1137. }
  1138. const avatar = $("img.avatar", replyElement)
  1139. if (memberLink && avatar) {
  1140. if (((_b = avatar.parentElement) == null ? void 0 : _b.tagName) === "A") {
  1141. return
  1142. }
  1143. const memberLink2 = createElement("a", {
  1144. href: getAttribute(memberLink, "href"),
  1145. })
  1146. avatar.after(memberLink2)
  1147. memberLink2.append(avatar)
  1148. }
  1149. }
  1150. var getReplyElements = () => {
  1151. const firstReply = $('.box .cell[id^="r_"]')
  1152. if (firstReply == null ? void 0 : firstReply.parentElement) {
  1153. const v2exPolishModel = $(".v2p-model-mask")
  1154. return $$('.cell[id^="r_"]', firstReply.parentElement).filter((reply) => {
  1155. if (v2exPolishModel && reply.closest(".v2p-model-mask")) {
  1156. return false
  1157. }
  1158. return true
  1159. })
  1160. }
  1161. return []
  1162. }
  1163. var cachedReplyElements
  1164. var getCachedReplyElements = () => {
  1165. if (!cachedReplyElements) {
  1166. if (doc.readyState === "loading") {
  1167. return getReplyElements()
  1168. }
  1169. cachedReplyElements = getReplyElements()
  1170. }
  1171. return cachedReplyElements
  1172. }
  1173. var resetCachedReplyElements = () => {
  1174. cachedReplyElements = void 0
  1175. }
  1176. var getReplyId = (replyElement) => {
  1177. if (!replyElement) {
  1178. return ""
  1179. }
  1180. let id = replyElement.dataset.id
  1181. if (id) {
  1182. return id
  1183. }
  1184. id = replyElement.id.replace(/((top|related|cited)_)?r_/, "")
  1185. replyElement.dataset.id = id
  1186. return id
  1187. }
  1188. var getFloorNumberElement = (replyElement) =>
  1189. replyElement ? $(".no", replyElement) : void 0
  1190. var getFloorNumber = (replyElement) => {
  1191. if (!replyElement) {
  1192. return 0
  1193. }
  1194. let floorNumber = parseInt10(replyElement.dataset.floorNumber)
  1195. if (floorNumber) {
  1196. return floorNumber
  1197. }
  1198. const numberElement = getFloorNumberElement(replyElement)
  1199. if (numberElement) {
  1200. floorNumber = parseInt10(numberElement.textContent, 0)
  1201. replyElement.dataset.floorNumber = String(floorNumber)
  1202. return floorNumber
  1203. }
  1204. return 0
  1205. }
  1206. var cloneReplyElement = (
  1207. replyElement,
  1208. wrappingTable = false,
  1209. keepCitedReplies = false
  1210. ) => {
  1211. const cloned = replyElement.cloneNode(true)
  1212. const floorNumber = $(".no", cloned)
  1213. const toolbox = $(".fr", cloned)
  1214. if (toolbox && floorNumber) {
  1215. const floorNumber2 = createElement("a", {
  1216. class: "no",
  1217. textContent: floorNumber.textContent,
  1218. })
  1219. addEventListener(floorNumber2, "click", (event) => {
  1220. replyElement.scrollIntoView({ block: "start" })
  1221. event.preventDefault()
  1222. event.stopPropagation()
  1223. })
  1224. toolbox.innerHTML = ""
  1225. toolbox.append(floorNumber2)
  1226. }
  1227. const cells = $$(".cell,.v2p-topic-reply-ref,.nested", cloned)
  1228. for (const cell of cells) {
  1229. if (keepCitedReplies && hasClass(cell, "cited_reply")) {
  1230. continue
  1231. }
  1232. cell.remove()
  1233. }
  1234. if (wrappingTable) {
  1235. const table = cloned.firstElementChild
  1236. if (table && table.tagName === "TABLE") {
  1237. const wrapper = createElement("div", {
  1238. class: "vr_wrapper",
  1239. })
  1240. table.after(wrapper)
  1241. wrapper.append(table)
  1242. }
  1243. }
  1244. return cloned
  1245. }
  1246. var sortReplyElementsByFloorNumberCompareFn = (a, b) =>
  1247. getFloorNumber(a) - getFloorNumber(b)
  1248. var parseUrl = () => {
  1249. const matched = /\/t\/(\d+)(?:.+\bp=(\d+))?/.exec(location.href) || []
  1250. const topicId = matched[1]
  1251. const page = parseInt10(matched[2], 1)
  1252. return { topicId, page }
  1253. }
  1254. var getRepliesCount = () => {
  1255. var _a
  1256. return parseInt10(
  1257. (/(\d+)\s条回复/.exec(
  1258. ((_a = $(".box .cell .gray")) == null ? void 0 : _a.textContent) || ""
  1259. ) || [])[1],
  1260. 0
  1261. )
  1262. }
  1263. var getMemberIdFromMemberLink = (memberLink) => {
  1264. if (!memberLink) {
  1265. return
  1266. }
  1267. return (/member\/(\w+)/.exec(memberLink.href) || [])[1]
  1268. }
  1269. var getReplyAuthorMemberId = (replyElement) => {
  1270. if (!replyElement) {
  1271. return
  1272. }
  1273. const memberLink = $('a[href^="/member/"]', replyElement)
  1274. return getMemberIdFromMemberLink(memberLink)
  1275. }
  1276. var getReplyElementByMemberIdAndFloorNumber = (
  1277. memberId,
  1278. floorNumber,
  1279. type = 0
  1280. ) => {
  1281. if (!memberId || !floorNumber) {
  1282. return
  1283. }
  1284. const replyElements = getCachedReplyElements()
  1285. const length = replyElements.length
  1286. const reverse = floorNumber > length / 2
  1287. let nearestReply
  1288. let nearestReplyGap = 1e3
  1289. for (let i2 = 0; i2 < length; i2++) {
  1290. const replyElement = replyElements[reverse ? length - i2 - 1 : i2]
  1291. const memberId2 = getReplyAuthorMemberId(replyElement)
  1292. if (memberId2 !== memberId) {
  1293. continue
  1294. }
  1295. const floorNumber2 = getFloorNumber(replyElement)
  1296. if (floorNumber2 === floorNumber) {
  1297. return replyElement
  1298. }
  1299. if (type === 1 && floorNumber2 > floorNumber) {
  1300. continue
  1301. }
  1302. if (type === 2 && floorNumber2 < floorNumber) {
  1303. continue
  1304. }
  1305. if (
  1306. !nearestReply ||
  1307. Math.abs(floorNumber - floorNumber2) < nearestReplyGap
  1308. ) {
  1309. nearestReply = replyElement
  1310. nearestReplyGap = Math.abs(floorNumber - floorNumber2)
  1311. }
  1312. }
  1313. return nearestReply
  1314. }
  1315. var getPagingPreviousButtons = () =>
  1316. $$(".normal_page_right").map((right) => right.previousElementSibling)
  1317. var getPagingNextButtons = () => $$(".normal_page_right")
  1318. var getReplyInputElement = () => {
  1319. return $("#reply_content")
  1320. }
  1321. function insertTextToReplyInput(text) {
  1322. const replyTextArea = getReplyInputElement()
  1323. if (replyTextArea) {
  1324. const startPos = replyTextArea.selectionStart
  1325. const endPos = replyTextArea.selectionEnd
  1326. const valueToStart = replyTextArea.value.slice(0, startPos)
  1327. const valueFromEnd = replyTextArea.value.slice(
  1328. endPos,
  1329. replyTextArea.value.length
  1330. )
  1331. replyTextArea.value = ""
  1332. .concat(valueToStart)
  1333. .concat(text)
  1334. .concat(valueFromEnd)
  1335. replyTextArea.focus()
  1336. const newPos = startPos + text.length
  1337. replyTextArea.selectionStart = newPos
  1338. replyTextArea.selectionEnd = newPos
  1339. }
  1340. }
  1341. var replaceReplyInputText = (find, replace, dispatchInputEvent = false) => {
  1342. const replyTextArea = getReplyInputElement()
  1343. if (replyTextArea) {
  1344. const value = replyTextArea.value
  1345. if (typeof value === "string") {
  1346. const index = value.indexOf(find)
  1347. if (index === -1) {
  1348. return
  1349. }
  1350. const endPos = replyTextArea.selectionEnd
  1351. const newValue = value.replace(find, replace)
  1352. replyTextArea.value = newValue
  1353. replyTextArea.focus()
  1354. const newPos =
  1355. index > endPos ? endPos : endPos + newValue.length - value.length
  1356. replyTextArea.selectionStart = newPos
  1357. replyTextArea.selectionEnd = newPos
  1358. if (dispatchInputEvent) {
  1359. replyTextArea.dispatchEvent(new Event("input"))
  1360. }
  1361. }
  1362. }
  1363. }
  1364. var getOnce = () => {
  1365. const onceElement = $("#once")
  1366. if (onceElement == null ? void 0 : onceElement.value) {
  1367. return onceElement.value
  1368. }
  1369. const html = doc.body.innerHTML
  1370. const once = (/once=(\d+)/.exec(html) || [])[1]
  1371. return once
  1372. }
  1373. var addlinkToCitedFloorNumbers = (replyElement) => {
  1374. const content = $(".reply_content", replyElement)
  1375. const memberLinks = $$('a[href^="/member/"]', content)
  1376. for (const memberLink of memberLinks) {
  1377. const previousTextNode = memberLink.previousSibling
  1378. const memberId = getMemberIdFromMemberLink(memberLink)
  1379. if (
  1380. previousTextNode &&
  1381. previousTextNode.nodeType === 3 &&
  1382. previousTextNode.textContent &&
  1383. previousTextNode.textContent.endsWith("@") &&
  1384. memberId
  1385. ) {
  1386. let nextTextNode = memberLink.nextSibling
  1387. while (nextTextNode) {
  1388. if (
  1389. nextTextNode.tagName === "BR" ||
  1390. !nextTextNode.textContent ||
  1391. nextTextNode.textContent.trim().length === 0
  1392. ) {
  1393. nextTextNode = nextTextNode.nextSibling
  1394. } else {
  1395. break
  1396. }
  1397. }
  1398. if (
  1399. !nextTextNode ||
  1400. nextTextNode.nodeType !== 3 ||
  1401. !nextTextNode.textContent ||
  1402. !/^\s*#\d+/.test(nextTextNode.textContent)
  1403. ) {
  1404. continue
  1405. }
  1406. const match = /^(\s*)(#(\d+))(.*)/.exec(nextTextNode.textContent)
  1407. if (!match) {
  1408. continue
  1409. }
  1410. if (match[1]) {
  1411. nextTextNode.before(doc.createTextNode(match[1]))
  1412. }
  1413. if (match[2]) {
  1414. const element = createElement("a", {
  1415. class: "cited_floor_number",
  1416. textContent: match[2],
  1417. "data-member-id": memberId,
  1418. "data-floor-number": match[3],
  1419. })
  1420. nextTextNode.before(element)
  1421. }
  1422. nextTextNode.textContent = match[4]
  1423. }
  1424. }
  1425. runOnce("addlinkToCitedFloorNumbers:document-onclick", () => {
  1426. addEventListener(doc, "click", (event) => {
  1427. const target = event.target
  1428. if (hasClass(target, "cited_floor_number")) {
  1429. const memberId = target.dataset.memberId
  1430. const floorNumber = parseInt10(target.dataset.floorNumber)
  1431. const citedReplyElement = getReplyElementByMemberIdAndFloorNumber(
  1432. memberId,
  1433. floorNumber
  1434. )
  1435. if (citedReplyElement) {
  1436. citedReplyElement.scrollIntoView({ block: "start" })
  1437. event.preventDefault()
  1438. event.stopPropagation()
  1439. }
  1440. }
  1441. })
  1442. })
  1443. }
  1444. var alwaysShowHideButton = (replyElement) => {
  1445. const hideButton = $('a[onclick*="ignoreReply"]', replyElement)
  1446. if (hideButton && !hasClass(hideButton, "icon_button")) {
  1447. addAttribute(hideButton, "class", "icon_button")
  1448. if (!$(".v2p-controls", replyElement)) {
  1449. hideButton.innerHTML =
  1450. '<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.3536 2.35355C13.5488 2.15829 13.5488 1.84171 13.3536 1.64645C13.1583 1.45118 12.8417 1.45118 12.6464 1.64645L10.6828 3.61012C9.70652 3.21671 8.63759 3 7.5 3C4.30786 3 1.65639 4.70638 0.0760002 7.23501C-0.0253338 7.39715 -0.0253334 7.60288 0.0760014 7.76501C0.902945 9.08812 2.02314 10.1861 3.36061 10.9323L1.64645 12.6464C1.45118 12.8417 1.45118 13.1583 1.64645 13.3536C1.84171 13.5488 2.15829 13.5488 2.35355 13.3536L4.31723 11.3899C5.29348 11.7833 6.36241 12 7.5 12C10.6921 12 13.3436 10.2936 14.924 7.76501C15.0253 7.60288 15.0253 7.39715 14.924 7.23501C14.0971 5.9119 12.9769 4.81391 11.6394 4.06771L13.3536 2.35355ZM9.90428 4.38861C9.15332 4.1361 8.34759 4 7.5 4C4.80285 4 2.52952 5.37816 1.09622 7.50001C1.87284 8.6497 2.89609 9.58106 4.09974 10.1931L9.90428 4.38861ZM5.09572 10.6114L10.9003 4.80685C12.1039 5.41894 13.1272 6.35031 13.9038 7.50001C12.4705 9.62183 10.1971 11 7.5 11C6.65241 11 5.84668 10.8639 5.09572 10.6114Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>'
  1451. }
  1452. const nextSibling = hideButton.nextSibling
  1453. if (nextSibling && nextSibling.nodeType === 3) {
  1454. nextSibling.textContent = ""
  1455. }
  1456. }
  1457. }
  1458. var alwaysShowThankButton = (replyElement) => {
  1459. const thankButton = $('a[onclick*="thankReply"]', replyElement)
  1460. if (thankButton && !hasClass(thankButton, "icon_button")) {
  1461. addAttribute(thankButton, "class", "icon_button")
  1462. if (!$(".v2p-controls", replyElement)) {
  1463. thankButton.innerHTML =
  1464. '<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4.89346 2.35248C3.49195 2.35248 2.35248 3.49359 2.35248 4.90532C2.35248 6.38164 3.20954 7.9168 4.37255 9.33522C5.39396 10.581 6.59464 11.6702 7.50002 12.4778C8.4054 11.6702 9.60608 10.581 10.6275 9.33522C11.7905 7.9168 12.6476 6.38164 12.6476 4.90532C12.6476 3.49359 11.5081 2.35248 10.1066 2.35248C9.27059 2.35248 8.81894 2.64323 8.5397 2.95843C8.27877 3.25295 8.14623 3.58566 8.02501 3.88993C8.00391 3.9429 7.98315 3.99501 7.96211 4.04591C7.88482 4.23294 7.7024 4.35494 7.50002 4.35494C7.29765 4.35494 7.11523 4.23295 7.03793 4.04592C7.01689 3.99501 6.99612 3.94289 6.97502 3.8899C6.8538 3.58564 6.72126 3.25294 6.46034 2.95843C6.18109 2.64323 5.72945 2.35248 4.89346 2.35248ZM1.35248 4.90532C1.35248 2.94498 2.936 1.35248 4.89346 1.35248C6.0084 1.35248 6.73504 1.76049 7.20884 2.2953C7.32062 2.42147 7.41686 2.55382 7.50002 2.68545C7.58318 2.55382 7.67941 2.42147 7.79119 2.2953C8.265 1.76049 8.99164 1.35248 10.1066 1.35248C12.064 1.35248 13.6476 2.94498 13.6476 4.90532C13.6476 6.74041 12.6013 8.50508 11.4008 9.96927C10.2636 11.3562 8.92194 12.5508 8.00601 13.3664C7.94645 13.4194 7.88869 13.4709 7.83291 13.5206C7.64324 13.6899 7.3568 13.6899 7.16713 13.5206C7.11135 13.4709 7.05359 13.4194 6.99403 13.3664C6.0781 12.5508 4.73641 11.3562 3.59926 9.96927C2.39872 8.50508 1.35248 6.74041 1.35248 4.90532Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>'
  1465. }
  1466. }
  1467. }
  1468. var fetchOnce = async () => {
  1469. const url = ""
  1470. .concat(location.protocol, "//")
  1471. .concat(location.host, "/poll_once")
  1472. const response = await fetch(url)
  1473. try {
  1474. if (response.status === 200) {
  1475. return await response.text()
  1476. }
  1477. } catch (error) {
  1478. console.error("[V2EX.REP] Unable to refresh once", error)
  1479. }
  1480. }
  1481. var updateOnce = async () => {
  1482. const once = await fetchOnce()
  1483. if (once) {
  1484. window.once = once
  1485. if ($("#once")) {
  1486. $("#once").value = once
  1487. }
  1488. const links = $$('a[href*="once="]')
  1489. for (const link of links) {
  1490. const href = getAttribute(link, "href")
  1491. setAttribute(
  1492. link,
  1493. "href",
  1494. href.replace(/once=\d+/, "once=".concat(once))
  1495. )
  1496. }
  1497. }
  1498. }
  1499. var storageKey2 = "dailyCheckIn"
  1500. var retryCount = 0
  1501. var fetchCheckInApi = async (once) => {
  1502. const url = ""
  1503. .concat(location.protocol, "//")
  1504. .concat(location.host, "/mission/daily/redeem?once=")
  1505. .concat(once)
  1506. try {
  1507. const response = await fetch(url)
  1508. if (response.status === 200) {
  1509. return await response.text()
  1510. }
  1511. } catch (error) {
  1512. console.error(error)
  1513. retryCount++
  1514. if (retryCount < 3) {
  1515. await sleep(1e3)
  1516. return fetchCheckInApi(once)
  1517. }
  1518. }
  1519. }
  1520. var dailyCheckIn = async () => {
  1521. var _a
  1522. if ($('a[href*="/signin"]')) {
  1523. return
  1524. }
  1525. const once = getOnce()
  1526. if (!once) {
  1527. return
  1528. }
  1529. const lastCheckInDate = await getValue(storageKey2)
  1530. if (lastCheckInDate) {
  1531. const now = Date.now()
  1532. if (
  1533. now - lastCheckInDate < 864e5 &&
  1534. new Date(now).getUTCDate() === new Date(lastCheckInDate).getUTCDate()
  1535. ) {
  1536. return
  1537. }
  1538. }
  1539. const result = await fetchCheckInApi(once)
  1540. if (
  1541. result.includes("\u6BCF\u65E5\u767B\u5F55\u5956\u52B1\u5DF2\u9886\u53D6")
  1542. ) {
  1543. console.info("[V2EX.REP] \u7B7E\u5230\u6210\u529F")
  1544. await setValue(storageKey2, Date.now())
  1545. const checkInLink = $('a[href^="/mission/daily"]')
  1546. if (checkInLink) {
  1547. const box = checkInLink.closest(".box")
  1548. if (box) {
  1549. ;(_a = box.nextElementSibling) == null ? void 0 : _a.remove()
  1550. box.remove()
  1551. }
  1552. }
  1553. } else {
  1554. console.error("[V2EX.REP] \u7B7E\u5230\u5931\u8D25")
  1555. }
  1556. await updateOnce()
  1557. }
  1558. var isTouchScreen1 = isTouchScreen()
  1559. var timeoutId
  1560. var scrollPositionStack = []
  1561. var showModalReplies = (replies, referElement, memberId, type) => {
  1562. var _a
  1563. const main2 = $("#Main") || $(".content")
  1564. if (!main2) {
  1565. return
  1566. }
  1567. if (doc.scrollingElement) {
  1568. scrollPositionStack.push(doc.scrollingElement.scrollTop)
  1569. }
  1570. setStyle(main2, "position: relative;")
  1571. const replyElement = $("#Main")
  1572. ? referElement.closest("#Main .cell")
  1573. : referElement.closest(".cell")
  1574. const relatedBox =
  1575. replyElement == null ? void 0 : replyElement.closest(".related_replies")
  1576. if (replyElement && relatedBox) {
  1577. const lastRelatedRepliesBox = $$(".related_replies_container").pop()
  1578. if (
  1579. lastRelatedRepliesBox == null
  1580. ? void 0
  1581. : lastRelatedRepliesBox.contains(replyElement)
  1582. ) {
  1583. } else {
  1584. closeModal2(true)
  1585. }
  1586. } else {
  1587. closeModal2()
  1588. }
  1589. const container = addElement2(main2, "div", {
  1590. class: "related_replies_container",
  1591. })
  1592. const box = addElement2(container, "div", {
  1593. class: "box related_replies related_replies_before",
  1594. })
  1595. const box2 = addElement2(container, "div", {
  1596. class: "box related_replies related_replies_after",
  1597. })
  1598. box.innerHTML = ""
  1599. box2.innerHTML = ""
  1600. const tabs = addElement2(box, "div", {
  1601. class: "box tabs inner",
  1602. })
  1603. addElement2(tabs, "a", {
  1604. class: !type || type === "all" ? "tab_current" : "tab",
  1605. href: actionHref,
  1606. textContent: "\u5168\u90E8\u56DE\u590D",
  1607. onclick() {
  1608. showRelatedReplies(referElement, memberId)
  1609. },
  1610. })
  1611. addElement2(tabs, "a", {
  1612. class: type === "posted" ? "tab_current" : "tab",
  1613. href: actionHref,
  1614. textContent: "\u4EC5 ".concat(
  1615. memberId,
  1616. " \u53D1\u8868\u7684\u56DE\u590D"
  1617. ),
  1618. onclick() {
  1619. showRelatedReplies(referElement, memberId, "posted")
  1620. },
  1621. })
  1622. const replyId = replyElement ? getReplyId(replyElement) : void 0
  1623. const floorNumber = replyElement ? getFloorNumber(replyElement) : 0
  1624. let beforeCount = 0
  1625. let afterCount = 0
  1626. replies.sort(sortReplyElementsByFloorNumberCompareFn)
  1627. for (const reply of replies) {
  1628. const replyId2 = getReplyId(reply)
  1629. const floorNumber2 = getFloorNumber(reply)
  1630. if (replyId === replyId2) {
  1631. continue
  1632. }
  1633. if (floorNumber > floorNumber2) {
  1634. box.append(reply)
  1635. beforeCount++
  1636. } else {
  1637. box2.append(reply)
  1638. afterCount++
  1639. }
  1640. }
  1641. if (beforeCount === 0 && afterCount === 0) {
  1642. addElement2(box, "div", {
  1643. class: "cell",
  1644. innerHTML:
  1645. '<span class="fade">\u672C\u9875\u9762\u6CA1\u6709\u5176\u4ED6\u56DE\u590D</span>',
  1646. })
  1647. if (!type || type === "all") {
  1648. tabs.remove()
  1649. addClass(container, "no_replies")
  1650. addEventListener(
  1651. referElement,
  1652. "mouseout",
  1653. () => {
  1654. container.remove()
  1655. scrollPositionStack.pop()
  1656. },
  1657. { once: true }
  1658. )
  1659. }
  1660. }
  1661. if (beforeCount === 0 && afterCount > 0) {
  1662. addElement2(box, "div", {
  1663. class: "cell",
  1664. innerHTML:
  1665. '<span class="fade">\u8FD9\u6761\u56DE\u590D\u540E\u9762\u8FD8\u6709 '.concat(
  1666. afterCount,
  1667. " \u6761\u56DE\u590D</span>"
  1668. ),
  1669. })
  1670. }
  1671. if (beforeCount > 0 && afterCount === 0) {
  1672. addElement2(box2, "div", {
  1673. class: "cell",
  1674. innerHTML:
  1675. '<span class="fade">\u8FD9\u6761\u56DE\u590D\u524D\u9762\u8FD8\u6709 '.concat(
  1676. beforeCount,
  1677. " \u6761\u56DE\u590D</span>"
  1678. ),
  1679. })
  1680. }
  1681. const width = main2.offsetWidth + "px"
  1682. if (replyElement) {
  1683. const offsetTop = getOffsetPosition(replyElement, main2).top
  1684. const height = box.offsetHeight || box.clientHeight
  1685. const height2 = replyElement.offsetHeight || replyElement.clientHeight
  1686. setStyle(box, {
  1687. top: offsetTop - height + "px",
  1688. width,
  1689. })
  1690. setStyle(box2, {
  1691. top: offsetTop + height2 + "px",
  1692. width,
  1693. })
  1694. } else if (afterCount > 0) {
  1695. ;(_a = box2.firstChild) == null ? void 0 : _a.before(tabs)
  1696. const headerElement =
  1697. referElement == null ? void 0 : referElement.closest(".header")
  1698. if (headerElement) {
  1699. const offsetTop = getOffsetPosition(headerElement, main2).top
  1700. const height2 = headerElement.offsetHeight || headerElement.clientHeight
  1701. setStyle(box2, {
  1702. top: offsetTop + height2 + "px",
  1703. width,
  1704. })
  1705. box.remove()
  1706. } else {
  1707. const firstReply = $('.box .cell[id^="r_"]')
  1708. const offsetTop = firstReply
  1709. ? Math.max(getOffsetPosition(firstReply, main2).top, win.scrollY)
  1710. : win.scrollY
  1711. setStyle(box, {
  1712. top: offsetTop + "px",
  1713. width,
  1714. })
  1715. setStyle(box2, {
  1716. top: offsetTop + "px",
  1717. width,
  1718. })
  1719. box2.scrollIntoView({ block: "start" })
  1720. }
  1721. } else {
  1722. box.remove()
  1723. box2.remove()
  1724. }
  1725. }
  1726. var filterRepliesPostedByMember = (memberIds) => {
  1727. const replies = []
  1728. const replyElements = getCachedReplyElements()
  1729. for (const replyElement of replyElements) {
  1730. const memberLink = $('a[href^="/member/"]', replyElement)
  1731. if (!memberLink) {
  1732. continue
  1733. }
  1734. const memberId = (/member\/(\w+)/.exec(memberLink.href) || [])[1]
  1735. if (memberIds.includes(memberId)) {
  1736. const cloned = cloneReplyElement(replyElement, true, true)
  1737. cloned.id = "related_" + replyElement.id
  1738. replies.push(cloned)
  1739. }
  1740. }
  1741. return replies
  1742. }
  1743. var filterRepliesByPosterOrMentioned = (memberId) => {
  1744. const replies = []
  1745. const replyElements = getCachedReplyElements()
  1746. for (const replyElement of replyElements) {
  1747. const memberLink = $(
  1748. 'a[href^="/member/'.concat(memberId, '"]'),
  1749. replyElement
  1750. )
  1751. if (!memberLink) {
  1752. continue
  1753. }
  1754. let cloned = cloneReplyElement(replyElement, true)
  1755. const memberLink2 = $('a[href^="/member/'.concat(memberId, '"]'), cloned)
  1756. if (!memberLink2) {
  1757. continue
  1758. }
  1759. cloned = cloneReplyElement(replyElement, true, true)
  1760. cloned.id = "related_" + replyElement.id
  1761. replies.push(cloned)
  1762. }
  1763. return replies
  1764. }
  1765. var showRelatedReplies = (memberLink, memberId, type) => {
  1766. const replies =
  1767. type === "posted"
  1768. ? filterRepliesPostedByMember([memberId])
  1769. : filterRepliesByPosterOrMentioned(memberId)
  1770. showModalReplies(replies, memberLink, memberId, type)
  1771. }
  1772. var onMouseOver = (event) => {
  1773. if (timeoutId) {
  1774. clearTimeout(timeoutId)
  1775. timeoutId = void 0
  1776. }
  1777. const memberLink = event.target
  1778. timeoutId = setTimeout(() => {
  1779. const memberId = (/member\/(\w+)/.exec(memberLink.href) || [])[1]
  1780. if (memberId) {
  1781. showRelatedReplies(memberLink, memberId)
  1782. }
  1783. }, 700)
  1784. }
  1785. var onMouseOut = () => {
  1786. if (timeoutId) {
  1787. clearTimeout(timeoutId)
  1788. timeoutId = void 0
  1789. }
  1790. }
  1791. var closeModal2 = (closeLast = false) => {
  1792. for (const element of $$(".related_replies_container").reverse()) {
  1793. element.remove()
  1794. const scrollPosition = scrollPositionStack.pop()
  1795. if (scrollPosition !== void 0 && doc.scrollingElement) {
  1796. doc.scrollingElement.scrollTop = scrollPosition
  1797. }
  1798. if (closeLast) {
  1799. break
  1800. }
  1801. }
  1802. }
  1803. var onDocumentClick2 = (event) => {
  1804. const target = event.target
  1805. if (target.closest(".utags_ul")) {
  1806. if (
  1807. hasClass(target, "utags_captain_tag") ||
  1808. hasClass(target, "utags_captain_tag2")
  1809. ) {
  1810. event.preventDefault()
  1811. }
  1812. return
  1813. }
  1814. if (isTouchScreen1) {
  1815. const memberLink = target.closest('a[href^="/member/"]')
  1816. if (memberLink && !$("img", memberLink)) {
  1817. event.preventDefault()
  1818. event.stopPropagation()
  1819. return
  1820. }
  1821. }
  1822. const floorNumberElement = target.closest(".related_replies a.no")
  1823. if (floorNumberElement) {
  1824. closeModal2()
  1825. return
  1826. }
  1827. const lastRelatedRepliesBox = $$(".related_replies_container").pop()
  1828. const relatedReply = target.closest(".related_replies .cell")
  1829. if (
  1830. relatedReply &&
  1831. (lastRelatedRepliesBox == null
  1832. ? void 0
  1833. : lastRelatedRepliesBox.contains(relatedReply))
  1834. ) {
  1835. return
  1836. }
  1837. const relatedRepliesBox = target.closest(".related_replies_container")
  1838. if (relatedRepliesBox) {
  1839. closeModal2(true)
  1840. return
  1841. }
  1842. closeModal2()
  1843. }
  1844. var onDocumentKeyDown2 = (event) => {
  1845. if (event.defaultPrevented) {
  1846. return
  1847. }
  1848. if (event.key === "Escape") {
  1849. closeModal2(true)
  1850. }
  1851. }
  1852. var filterRepliesByUser = (toogle) => {
  1853. if (toogle) {
  1854. const memberLinks = $$('a[href^="/member/"]')
  1855. for (const memberLink of memberLinks) {
  1856. if (!memberLink.boundEvent) {
  1857. addEventListener(memberLink, "mouseover", onMouseOver, true)
  1858. addEventListener(memberLink, "mouseout", onMouseOut)
  1859. if (isTouchScreen1) {
  1860. addEventListener(memberLink, "touchstart", onMouseOver, true)
  1861. }
  1862. memberLink.boundEvent = true
  1863. }
  1864. }
  1865. if (!doc.boundEvent) {
  1866. addEventListener(doc, "click", onDocumentClick2, true)
  1867. addEventListener(doc, "keydown", onDocumentKeyDown2)
  1868. doc.boundEvent = true
  1869. }
  1870. } else if (doc.boundEvent) {
  1871. closeModal2()
  1872. removeEventListener(doc, "click", onDocumentClick2, true)
  1873. removeEventListener(doc, "keydown", onDocumentKeyDown2)
  1874. doc.boundEvent = false
  1875. const memberLinks = $$('a[href^="/member/"]')
  1876. for (const memberLink of memberLinks) {
  1877. if (memberLink.boundEvent) {
  1878. removeEventListener(memberLink, "mouseover", onMouseOver, true)
  1879. removeEventListener(memberLink, "mouseout", onMouseOut)
  1880. if (isTouchScreen1) {
  1881. removeEventListener(memberLink, "touchstart", onMouseOver, true)
  1882. }
  1883. memberLink.boundEvent = false
  1884. }
  1885. }
  1886. }
  1887. }
  1888. var retryCount2 = 0
  1889. var getTopicReplies = async (topicId, replyCount) => {
  1890. const cached = cache.get(["getTopicReplies", topicId, replyCount])
  1891. if (cached) {
  1892. return cached
  1893. }
  1894. const url = ""
  1895. .concat(location.protocol, "//")
  1896. .concat(location.host, "/api/replies/show.json?topic_id=")
  1897. .concat(topicId)
  1898. .concat(replyCount ? "&replyCount=" + String(replyCount) : "")
  1899. try {
  1900. const response = await fetch(url)
  1901. if (response.status === 200) {
  1902. const result = await response.json()
  1903. cache.add(["getTopicReplies", topicId, replyCount], result)
  1904. return result
  1905. }
  1906. } catch (error) {
  1907. console.error(error)
  1908. retryCount2++
  1909. if (retryCount2 < 3) {
  1910. await sleep(1e3)
  1911. return getTopicReplies(topicId, replyCount)
  1912. }
  1913. }
  1914. }
  1915. var updateFloorNumber = (replyElement, newFloorNumber) => {
  1916. const numberElement = getFloorNumberElement(replyElement)
  1917. if (numberElement) {
  1918. if (!numberElement.dataset.orgNumber) {
  1919. const orgNumber = parseInt10(numberElement.textContent)
  1920. if (orgNumber) {
  1921. numberElement.dataset.orgNumber = String(orgNumber)
  1922. }
  1923. }
  1924. numberElement.textContent = String(newFloorNumber)
  1925. replyElement.dataset.floorNumber = String(newFloorNumber)
  1926. }
  1927. }
  1928. var updateAllFloorNumberById = (id, newFloorNumber) => {
  1929. for (const replyElement of $$(
  1930. "#r_"
  1931. .concat(id, ",\n #top_r_")
  1932. .concat(id, ",\n #related_r_")
  1933. .concat(id, ",\n #cited_r_")
  1934. .concat(id)
  1935. )) {
  1936. updateFloorNumber(replyElement, newFloorNumber)
  1937. }
  1938. }
  1939. var printHiddenReplies = (hiddenReplies) => {
  1940. for (const reply of hiddenReplies) {
  1941. console.group(
  1942. "[V2EX.REP] \u5C4F\u853D\u6216\u9690\u85CF\u7684\u56DE\u590D: #"
  1943. .concat(reply.floorNumber, ", \u7528\u6237 ID: ")
  1944. .concat(reply.userId, ", \u56DE\u590D ID: ")
  1945. .concat(reply.replyId, ", \u56DE\u590D\u5185\u5BB9: ")
  1946. )
  1947. console.info(reply.replyContent)
  1948. console.groupEnd()
  1949. }
  1950. }
  1951. var updateReplyElements = (replies, replyElements, page = 1) => {
  1952. let floorNumberOffset = 0
  1953. let hiddenCount = 0
  1954. let hiddenCount2 = 0
  1955. const dataOffSet = (page - 1) * 100
  1956. const length = Math.min(replies.length - (page - 1) * 100, 100)
  1957. const hiddenReplies = []
  1958. for (let i2 = 0; i2 < length; i2++) {
  1959. const realFloorNumber = i2 + dataOffSet + 1
  1960. const reply = replies[i2 + dataOffSet]
  1961. const id = reply.id
  1962. const element = $("#r_" + id)
  1963. const member = reply.member || {}
  1964. if (!element) {
  1965. hiddenReplies.push({
  1966. floorNumber: realFloorNumber,
  1967. userId: member.username,
  1968. replyId: reply.id,
  1969. replyContent: reply.content,
  1970. })
  1971. hiddenCount++
  1972. continue
  1973. }
  1974. if (!isVisible(element)) {
  1975. hiddenReplies.push({
  1976. floorNumber: realFloorNumber,
  1977. userId: member.username,
  1978. replyId: reply.id,
  1979. replyContent: reply.content,
  1980. })
  1981. hiddenCount2++
  1982. }
  1983. element.found = true
  1984. if (hiddenCount > 0) {
  1985. const numberElement = getFloorNumberElement(element)
  1986. if (numberElement) {
  1987. const orgNumber = parseInt10(
  1988. numberElement.dataset.orgNumber || numberElement.textContent
  1989. )
  1990. if (orgNumber) {
  1991. numberElement.dataset.orgNumber = String(orgNumber)
  1992. floorNumberOffset = realFloorNumber - orgNumber
  1993. }
  1994. numberElement.textContent = String(realFloorNumber)
  1995. }
  1996. updateAllFloorNumberById(id, realFloorNumber)
  1997. }
  1998. }
  1999. console.info(
  2000. "[V2EX.REP] page: "
  2001. .concat(page, ", floorNumberOffset: ")
  2002. .concat(floorNumberOffset, ", hiddenCount: ")
  2003. .concat(hiddenCount + hiddenCount2)
  2004. )
  2005. if (floorNumberOffset > 0) {
  2006. for (const element of replyElements) {
  2007. if (element.found) {
  2008. continue
  2009. }
  2010. const id = getReplyId(element)
  2011. const numberElement = getFloorNumberElement(element)
  2012. if (numberElement) {
  2013. const orgNumber = parseInt10(
  2014. numberElement.dataset.orgNumber || numberElement.textContent
  2015. )
  2016. if (orgNumber) {
  2017. numberElement.dataset.orgNumber = String(orgNumber)
  2018. numberElement.textContent = String(orgNumber + floorNumberOffset)
  2019. updateAllFloorNumberById(id, orgNumber + floorNumberOffset)
  2020. }
  2021. }
  2022. }
  2023. }
  2024. printHiddenReplies(hiddenReplies)
  2025. win.dispatchEvent(new Event("floorNumberUpdated"))
  2026. }
  2027. var isRunning = false
  2028. var splitArrayPerPages = (replyElements) => {
  2029. if (
  2030. !replyElements ||
  2031. replyElements.length === 0 ||
  2032. !replyElements[0].dataset.page
  2033. ) {
  2034. return
  2035. }
  2036. const replyElementsPerPages = []
  2037. let lastPage
  2038. let replyElementsPerPage = []
  2039. for (const reply of replyElements) {
  2040. if (reply.dataset.page !== lastPage) {
  2041. lastPage = reply.dataset.page
  2042. const page = parseInt10(reply.dataset.page)
  2043. replyElementsPerPage = replyElementsPerPages[page - 1] || []
  2044. replyElementsPerPages[page - 1] = replyElementsPerPage
  2045. }
  2046. replyElementsPerPage.push(reply)
  2047. }
  2048. return replyElementsPerPages
  2049. }
  2050. var process2 = async (
  2051. topicId,
  2052. page,
  2053. displayNumber,
  2054. replyElements,
  2055. forceUpdate = false
  2056. ) => {
  2057. if (isRunning) {
  2058. return
  2059. }
  2060. isRunning = true
  2061. const replies = await getTopicReplies(
  2062. topicId,
  2063. forceUpdate ? displayNumber : void 0
  2064. )
  2065. if (replies) {
  2066. const replyElementsPerPages = splitArrayPerPages(replyElements)
  2067. if (replyElementsPerPages) {
  2068. for (let i2 = 0; i2 < replyElementsPerPages.length; i2++) {
  2069. const replyElementsPerPage = replyElementsPerPages[i2]
  2070. if (
  2071. !replyElementsPerPage ||
  2072. (replyElementsPerPage.length > 0 &&
  2073. (displayNumber === replyElementsPerPage.length ||
  2074. displayNumber % 100 === replyElementsPerPage.length % 100 ||
  2075. replyElementsPerPage.length % 100 === 0))
  2076. ) {
  2077. continue
  2078. }
  2079. updateReplyElements(replies, replyElementsPerPage, i2 + 1)
  2080. }
  2081. } else {
  2082. updateReplyElements(replies, replyElements, page)
  2083. }
  2084. if (replies.length < displayNumber) {
  2085. console.info("[V2EX.REP] API data outdated, re-fetch it")
  2086. setTimeout(async () => {
  2087. await process2(topicId, page, displayNumber, replyElements, true)
  2088. }, 100)
  2089. }
  2090. }
  2091. isRunning = false
  2092. }
  2093. var fixReplyFloorNumbers = async (replyElements) => {
  2094. if (isRunning) {
  2095. return
  2096. }
  2097. const result = parseUrl()
  2098. const topicId = result.topicId
  2099. const page = result.page
  2100. if (!topicId) {
  2101. return
  2102. }
  2103. const displayNumber = getRepliesCount()
  2104. if (
  2105. replyElements.length > 0 &&
  2106. (displayNumber === replyElements.length ||
  2107. displayNumber % 100 === replyElements.length % 100 ||
  2108. replyElements.length % 100 === 0)
  2109. ) {
  2110. return
  2111. }
  2112. await process2(topicId, page, displayNumber, replyElements)
  2113. }
  2114. var restoreImgSrc = throttle(() => {
  2115. for (const img of $$("img[data-src]")) {
  2116. setAttribute(img, "src", getAttribute(img, "data-src"))
  2117. delete img.dataset.src
  2118. }
  2119. }, 500)
  2120. var lazyLoadAvatars = (replyElement) => {
  2121. const avatar = $("img.avatar", replyElement)
  2122. if (avatar) {
  2123. if (getAttribute(avatar, "loading") === "lazy" || avatar.complete) {
  2124. return
  2125. }
  2126. const src = getAttribute(avatar, "src")
  2127. setAttribute(avatar, "loading", "lazy")
  2128. setAttribute(avatar, "data-src", src)
  2129. setAttribute(
  2130. avatar,
  2131. "src",
  2132. "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
  2133. )
  2134. if (doc.readyState === "complete") {
  2135. setTimeout(restoreImgSrc)
  2136. } else {
  2137. addEventListener(win, "load", restoreImgSrc)
  2138. }
  2139. }
  2140. }
  2141. var retryCount3 = 0
  2142. var getTopicPage = async (topicId, page = 1) => {
  2143. const url = ""
  2144. .concat(location.protocol, "//")
  2145. .concat(location.host, "/t/")
  2146. .concat(topicId, "?p=")
  2147. .concat(page)
  2148. try {
  2149. const response = await fetch(url)
  2150. if (response.status === 200) {
  2151. return await response.text()
  2152. }
  2153. } catch (error) {
  2154. console.error(error, "page ".concat(page))
  2155. retryCount3++
  2156. if (retryCount3 < 10) {
  2157. await sleep(1e3)
  2158. return getTopicPage(topicId, page)
  2159. }
  2160. }
  2161. }
  2162. var getReplyElements2 = (html) => {
  2163. const htmlNode = createElement("html")
  2164. htmlNode.innerHTML = html
  2165. return $$('.cell[id^="r_"]', htmlNode)
  2166. }
  2167. var insertReplyElementsToPage = (replyElements, page, inertPoint) => {
  2168. if (!replyElements || replyElements.length === 0 || !inertPoint) {
  2169. return
  2170. }
  2171. for (const replyElement of replyElements) {
  2172. replyElement.dataset.page = String(page)
  2173. if (getSettingsValue("lazyLoadAvatars")) {
  2174. lazyLoadAvatars(replyElement)
  2175. }
  2176. inertPoint.before(replyElement)
  2177. }
  2178. }
  2179. var gotoPage = (page, event) => {
  2180. if (!page) {
  2181. return
  2182. }
  2183. history.pushState(null, null, "?p=".concat(page))
  2184. const main2 = $("#Main") || $(".content")
  2185. const firstReply = $('.cell[data-page="'.concat(page, '"]'), main2)
  2186. if (firstReply) {
  2187. firstReply.scrollIntoView({ block: "start" })
  2188. event.preventDefault()
  2189. event.stopImmediatePropagation()
  2190. }
  2191. for (const pagingElement of $$(".page_current,.page_normal")) {
  2192. if (pagingElement.textContent === String(page)) {
  2193. removeClass(pagingElement, "page_normal")
  2194. addClass(pagingElement, "page_current")
  2195. } else {
  2196. removeClass(pagingElement, "page_current")
  2197. addClass(pagingElement, "page_normal")
  2198. }
  2199. }
  2200. for (const pageInput of $$(".page_input")) {
  2201. pageInput.value = page
  2202. }
  2203. const repliesCount = getRepliesCount()
  2204. const totalPage = Math.ceil(repliesCount / 100)
  2205. for (const button of getPagingPreviousButtons()) {
  2206. if (String(page) === "1") {
  2207. addClass(button, "disable_now")
  2208. } else {
  2209. removeClass(button, "disable_now")
  2210. }
  2211. }
  2212. for (const button of getPagingNextButtons()) {
  2213. if (String(page) === String(totalPage)) {
  2214. addClass(button, "disable_now")
  2215. } else {
  2216. removeClass(button, "disable_now")
  2217. }
  2218. }
  2219. }
  2220. var updatePagingElements = () => {
  2221. runOnce("loadMultiPages:updatePagingElements", () => {
  2222. for (const pagingElement of $$(".page_current,.page_normal")) {
  2223. addEventListener(pagingElement, "click", (event) => {
  2224. const page = pagingElement.textContent
  2225. gotoPage(page, event)
  2226. })
  2227. }
  2228. for (const pageInput of $$(".page_input")) {
  2229. pageInput.removeAttribute("onkeydown")
  2230. addEventListener(
  2231. pageInput,
  2232. "keydown",
  2233. (event) => {
  2234. var _a
  2235. if (event.keyCode === 13) {
  2236. gotoPage((_a = event.target) == null ? void 0 : _a.value, event)
  2237. return false
  2238. }
  2239. },
  2240. true
  2241. )
  2242. }
  2243. const buttons = [...getPagingPreviousButtons(), ...getPagingNextButtons()]
  2244. for (const button of buttons) {
  2245. button.removeAttribute("onclick")
  2246. button.removeAttribute("onmouseover")
  2247. button.removeAttribute("onmousedown")
  2248. button.removeAttribute("onmouseleave")
  2249. addEventListener(
  2250. button,
  2251. "mouseover",
  2252. (event) => {
  2253. if (!hasClass(button, "disable_now")) {
  2254. addClass(button, "hover_now")
  2255. }
  2256. event.preventDefault()
  2257. event.stopImmediatePropagation()
  2258. },
  2259. true
  2260. )
  2261. addEventListener(
  2262. button,
  2263. "mousedown",
  2264. (event) => {
  2265. if (!hasClass(button, "disable_now")) {
  2266. addClass(button, "active_now")
  2267. }
  2268. event.preventDefault()
  2269. event.stopImmediatePropagation()
  2270. },
  2271. true
  2272. )
  2273. addEventListener(
  2274. button,
  2275. "mouseleave",
  2276. (event) => {
  2277. removeClass(button, "hover_now")
  2278. removeClass(button, "active_now")
  2279. event.preventDefault()
  2280. event.stopImmediatePropagation()
  2281. },
  2282. true
  2283. )
  2284. addEventListener(
  2285. button,
  2286. "click",
  2287. (event) => {
  2288. var _a
  2289. if (!hasClass(button, "disable_now")) {
  2290. const page = parseInt10(
  2291. (_a = $(".page_input")) == null ? void 0 : _a.value
  2292. )
  2293. if (page) {
  2294. if (hasClass(button, "normal_page_right")) {
  2295. gotoPage(page + 1, event)
  2296. } else {
  2297. gotoPage(page - 1, event)
  2298. }
  2299. }
  2300. }
  2301. setTimeout(() => {
  2302. removeClass(button, "hover_now")
  2303. removeClass(button, "active_now")
  2304. }, 100)
  2305. event.preventDefault()
  2306. event.stopImmediatePropagation()
  2307. },
  2308. true
  2309. )
  2310. }
  2311. })
  2312. }
  2313. var loadMultiPages = async () => {
  2314. const repliesCount = getRepliesCount()
  2315. if (repliesCount > 100) {
  2316. const result = parseUrl()
  2317. const topicId = result.topicId
  2318. const currentPage = result.page
  2319. const totalPage = Math.ceil(repliesCount / 100)
  2320. const orgReplyElements = getCachedReplyElements()
  2321. const firstReply = orgReplyElements[0]
  2322. const pageElement = orgReplyElements.at(-1).nextElementSibling
  2323. addClass(pageElement, "sticky_paging")
  2324. updatePagingElements()
  2325. for (const replyElement of orgReplyElements) {
  2326. replyElement.dataset.page = String(currentPage)
  2327. }
  2328. for (let i2 = 1; i2 <= totalPage; i2++) {
  2329. if (i2 === currentPage) {
  2330. continue
  2331. }
  2332. console.info("[V2EX.REP] Fetching page", i2)
  2333. const html = await getTopicPage(topicId, i2)
  2334. if (html) {
  2335. const replyElements = getReplyElements2(html)
  2336. insertReplyElementsToPage(
  2337. replyElements,
  2338. i2,
  2339. i2 < currentPage ? firstReply : pageElement
  2340. )
  2341. win.dispatchEvent(new Event("replyElementsLengthUpdated"))
  2342. }
  2343. await sleep(1e3)
  2344. }
  2345. }
  2346. }
  2347. var quickHideReply = (replyElement) => {
  2348. const hideButton = $('a[onclick*="ignoreReply"]', replyElement)
  2349. if (hideButton) {
  2350. const onclick = getAttribute(hideButton, "onclick")
  2351. if (!onclick.includes("confirm")) {
  2352. return
  2353. }
  2354. setAttribute(
  2355. hideButton,
  2356. "onclick",
  2357. onclick.replace(/.*(ignoreReply\(.+\)).*/, "$1")
  2358. )
  2359. setAttribute(hideButton, "href", actionHref)
  2360. hideButton.outerHTML = hideButton.outerHTML
  2361. }
  2362. }
  2363. var state = 0
  2364. var scrollIntoView = (element) => {
  2365. if (element) {
  2366. element.scrollIntoView({ block: "start" })
  2367. }
  2368. }
  2369. var quickNavigation = () => {
  2370. runOnce("quickNavigation", () => {
  2371. const main2 = $("#Main") || $(".content")
  2372. if (!main2) {
  2373. return
  2374. }
  2375. const isMobile = hasClass(main2, "content")
  2376. addEventListener(doc, "dblclick", (event) => {
  2377. const target = event.target
  2378. if (
  2379. target &&
  2380. ["TEXTAREA", "INPUT", "IMG", "A"].includes(target.tagName)
  2381. ) {
  2382. return
  2383. }
  2384. const selection = win.getSelection()
  2385. if (
  2386. (target == null ? void 0 : target.closest(".box,.cell,.inner")) &&
  2387. selection &&
  2388. !selection.isCollapsed &&
  2389. selection.toString().trim().length > 0
  2390. ) {
  2391. return
  2392. }
  2393. const boxes = $$(".box", main2)
  2394. switch (state++) {
  2395. case 0: {
  2396. scrollIntoView(isMobile ? boxes[2] : boxes[1])
  2397. break
  2398. }
  2399. case 1: {
  2400. scrollIntoView(isMobile ? boxes[3] : boxes[2])
  2401. break
  2402. }
  2403. default: {
  2404. scrollIntoView(boxes[0])
  2405. state = 0
  2406. break
  2407. }
  2408. }
  2409. })
  2410. })
  2411. }
  2412. var quickSendThank = (replyElement) => {
  2413. const thankButton = $('a[onclick*="thankReply"]', replyElement)
  2414. if (thankButton) {
  2415. const replyId = replyElement.id.replace("r_", "")
  2416. const onclick = getAttribute(thankButton, "onclick")
  2417. if (!onclick.includes("confirm")) {
  2418. return
  2419. }
  2420. setAttribute(
  2421. thankButton,
  2422. "onclick",
  2423. onclick.replace(/.*(thankReply\(.+\)).*/, "$1")
  2424. )
  2425. setAttribute(thankButton, "href", actionHref)
  2426. if (hasClass(thankButton.parentElement, "v2p-controls")) {
  2427. const div = createElement("div", {
  2428. id: "thank_area_" + replyId,
  2429. })
  2430. thankButton.after(div)
  2431. const hideButton = $('a[onclick*="ignoreReply"]', replyElement)
  2432. if (hideButton) {
  2433. div.append(hideButton)
  2434. }
  2435. div.append(thankButton)
  2436. }
  2437. thankButton.outerHTML = thankButton.outerHTML
  2438. }
  2439. }
  2440. var replaceState = (newHref) => {
  2441. history.replaceState(null, "", newHref)
  2442. }
  2443. var getVisitedUrl = (href, replyCount) =>
  2444. href.replace(/[?#].*|$/, "#reply".concat(replyCount))
  2445. var markAsVisited = (href, replyCount) => {
  2446. for (
  2447. let count = Math.max(replyCount - 10, 1);
  2448. count <= replyCount;
  2449. count++
  2450. ) {
  2451. replaceState(getVisitedUrl(href, count))
  2452. }
  2453. }
  2454. var removeLocationHash = () => {
  2455. const href = location.href
  2456. const hash = location.hash
  2457. const replyCount = getRepliesCount()
  2458. if (hash == null ? void 0 : hash.startsWith("#reply")) {
  2459. if (replyCount) {
  2460. markAsVisited(href, replyCount)
  2461. }
  2462. replaceState(href.replace(/#.*/, ""))
  2463. } else if (replyCount) {
  2464. markAsVisited(href, replyCount)
  2465. replaceState(href)
  2466. }
  2467. }
  2468. function setFavition(url, type) {
  2469. const element = $('link[rel="shortcut icon"]')
  2470. if (element) {
  2471. setAttributes(element, {
  2472. href: url,
  2473. type: type || "image/png",
  2474. })
  2475. }
  2476. }
  2477. function replaceToGithub() {
  2478. setFavition(
  2479. "https://github.githubassets.com/favicons/favicon.svg",
  2480. "image/svg+xml"
  2481. )
  2482. if (doc.title.includes("V2EX")) {
  2483. doc.title =
  2484. "Issues \xB7 " +
  2485. (doc.title.replace(/( - V2EX|V2EX › |V2EX)/, "") || "github")
  2486. }
  2487. }
  2488. function replaceToAvatar() {
  2489. const main2 = $("#Main") || $(".content")
  2490. if (!main2) {
  2491. return
  2492. }
  2493. const avatar = $('.header img.avatar, td[width="73"] img.avatar', main2)
  2494. if (avatar) {
  2495. setFavition(avatar.src)
  2496. if (!avatar.setFaviconHandler) {
  2497. avatar.setFaviconHandler = true
  2498. addEventListener(avatar, "load", () => {
  2499. setFavition(avatar.src)
  2500. })
  2501. }
  2502. } else {
  2503. setFavition("https://www.v2ex.com/static/favicon.ico")
  2504. }
  2505. }
  2506. function replaceToDefault() {
  2507. const main2 = $("#Main") || $(".content")
  2508. if (!main2) {
  2509. return
  2510. }
  2511. const avatar = $('td[width="73"] img.avatar', main2)
  2512. if (avatar) {
  2513. setFavition(avatar.src)
  2514. } else {
  2515. setFavition("https://www.v2ex.com/static/favicon.ico")
  2516. }
  2517. }
  2518. function replaceFavicon(type) {
  2519. if (type === "github") {
  2520. replaceToGithub()
  2521. } else if (type === "avatar") {
  2522. replaceToAvatar()
  2523. } else {
  2524. replaceToDefault()
  2525. }
  2526. }
  2527. var replyWithFloorNumber = (replyElement, forceUpdate = false) => {
  2528. const replyButton = $('a[onclick^="replyOne"]', replyElement)
  2529. if (replyButton) {
  2530. setAttribute(replyButton, "href", actionHref)
  2531. const onclick = getAttribute(replyButton, "onclick") || ""
  2532. if (onclick.includes("#") && !forceUpdate) {
  2533. return
  2534. }
  2535. const number = getFloorNumber(replyElement)
  2536. if (number) {
  2537. setAttribute(
  2538. replyButton,
  2539. "onclick",
  2540. onclick.replace(
  2541. /replyOne\('(\w+)(?: .*)?'\)/,
  2542. "replyOne('$1 #".concat(number, "')")
  2543. )
  2544. )
  2545. replyButton.outerHTML = replyButton.outerHTML
  2546. }
  2547. }
  2548. }
  2549. var showCitedReplies = (
  2550. replyElement,
  2551. showPreviousCitedReplies,
  2552. forceUpdate = false
  2553. ) => {
  2554. if (
  2555. !forceUpdate &&
  2556. (replyElement.dataset.showCitedReplies || $(".v2p-color-mode-toggle"))
  2557. ) {
  2558. return
  2559. }
  2560. const floorNumber = getFloorNumber(replyElement)
  2561. if (!floorNumber) {
  2562. return
  2563. }
  2564. replyElement.dataset.showCitedReplies = "done"
  2565. for (const element of $$(".cited_reply", replyElement)) {
  2566. element.remove()
  2567. }
  2568. const content = $(".reply_content", replyElement)
  2569. const memberLinks = $$('a[href^="/member/"]', content)
  2570. let hasCitedReplies = false
  2571. for (const memberLink of memberLinks) {
  2572. const textNode = memberLink.previousSibling
  2573. let nextElement = memberLink.nextSibling
  2574. let target = memberLink
  2575. let citedFloorNumber
  2576. if (
  2577. textNode &&
  2578. textNode.nodeType === 3 &&
  2579. textNode.textContent &&
  2580. textNode.textContent.endsWith("@")
  2581. ) {
  2582. const memberId = getMemberIdFromMemberLink(memberLink)
  2583. if (!memberId) {
  2584. continue
  2585. }
  2586. while (
  2587. nextElement &&
  2588. (nextElement.tagName === "BR" ||
  2589. !nextElement.textContent ||
  2590. nextElement.textContent.trim().length === 0 ||
  2591. hasClass(nextElement, "utags_ul"))
  2592. ) {
  2593. target = nextElement
  2594. nextElement = nextElement.nextSibling
  2595. }
  2596. if (nextElement && hasClass(nextElement, "cited_floor_number")) {
  2597. target = nextElement
  2598. citedFloorNumber = parseInt10(nextElement.dataset.floorNumber)
  2599. }
  2600. let citedReplyElement
  2601. if (citedFloorNumber) {
  2602. citedReplyElement = getReplyElementByMemberIdAndFloorNumber(
  2603. memberId,
  2604. citedFloorNumber
  2605. )
  2606. }
  2607. if (!citedReplyElement) {
  2608. citedReplyElement = getReplyElementByMemberIdAndFloorNumber(
  2609. memberId,
  2610. floorNumber - 1,
  2611. 1
  2612. )
  2613. }
  2614. if (citedReplyElement) {
  2615. if (
  2616. citedReplyElement.nextElementSibling === replyElement &&
  2617. !hasCitedReplies &&
  2618. showPreviousCitedReplies !== "1"
  2619. ) {
  2620. continue
  2621. }
  2622. const cloned = cloneReplyElement(citedReplyElement, true)
  2623. cloned.removeAttribute("id")
  2624. addClass(cloned, "cited_reply")
  2625. target.after(cloned)
  2626. hasCitedReplies = true
  2627. }
  2628. }
  2629. }
  2630. }
  2631. var done = false
  2632. var reset = () => {
  2633. const element = $("#top_replies")
  2634. if (element) {
  2635. const sep20 = element.previousElementSibling
  2636. if (hasClass(sep20, "sep20")) {
  2637. sep20.remove()
  2638. }
  2639. element.remove()
  2640. }
  2641. }
  2642. var showTopReplies = (replyElements, toggle, forceUpdate = false) => {
  2643. if (!toggle) {
  2644. reset()
  2645. removeClass($("#Wrapper"), "sticky_rightbar")
  2646. done = false
  2647. return
  2648. }
  2649. if (done && !forceUpdate) {
  2650. return
  2651. }
  2652. done = true
  2653. reset()
  2654. addClass($("#Wrapper"), "sticky_rightbar")
  2655. const topReplies = replyElements
  2656. .filter((reply) => {
  2657. var _a
  2658. const heartElement = $('img[alt="\u2764\uFE0F"],.v2p-icon-heart', reply)
  2659. if (heartElement) {
  2660. const childReplies = $$(".reply_content,.cell", reply)
  2661. for (const child of childReplies) {
  2662. if (child.contains(heartElement)) {
  2663. return false
  2664. }
  2665. }
  2666. const thanked = parseInt10(
  2667. (_a = heartElement.nextSibling) == null ? void 0 : _a.textContent,
  2668. 0
  2669. )
  2670. if (thanked > 0) {
  2671. reply.thanked = thanked
  2672. return true
  2673. }
  2674. }
  2675. return false
  2676. })
  2677. .sort((a, b) =>
  2678. b.thanked === a.thanked
  2679. ? sortReplyElementsByFloorNumberCompareFn(a, b)
  2680. : b.thanked - a.thanked
  2681. )
  2682. if (topReplies.length > 0) {
  2683. const box = createElement("div", {
  2684. class: "box",
  2685. id: "top_replies",
  2686. innerHTML:
  2687. '<div class="cell"><div class="fr"></div><span class="fade">\u5F53\u524D\u9875\u70ED\u95E8\u56DE\u590D</span></div>',
  2688. })
  2689. for (const element of topReplies) {
  2690. const cloned = cloneReplyElement(element, true)
  2691. cloned.id = "top_" + element.id
  2692. const ago = $(".ago", cloned)
  2693. if (ago) {
  2694. ago.before(createElement("br"))
  2695. }
  2696. box.append(cloned)
  2697. }
  2698. const appendPosition = $("#Rightbar .box")
  2699. const sep20 = createElement("div", {
  2700. class: "sep20",
  2701. })
  2702. if (appendPosition) {
  2703. appendPosition.after(box)
  2704. appendPosition.after(sep20)
  2705. }
  2706. }
  2707. }
  2708. var stickyTopicButtons = (toggle = false) => {
  2709. const main2 = $("#Main") || $(".content")
  2710. if (!main2) {
  2711. return
  2712. }
  2713. if (hasClass(main2, "content")) {
  2714. const buttons = $(".inner", main2)
  2715. if (buttons) {
  2716. addClass(buttons, "topic_buttons_mobile")
  2717. }
  2718. }
  2719. const added = hasClass(main2, "sticky_topic_buttons")
  2720. if (toggle && !added) {
  2721. addClass(main2, "sticky_topic_buttons")
  2722. } else if (!toggle && added) {
  2723. removeClass(main2, "sticky_topic_buttons")
  2724. }
  2725. }
  2726. var imgurClientIdPool = [
  2727. "3107b9ef8b316f3",
  2728. "442b04f26eefc8a",
  2729. "59cfebe717c09e4",
  2730. "60605aad4a62882",
  2731. "6c65ab1d3f5452a",
  2732. "83e123737849aa9",
  2733. "9311f6be1c10160",
  2734. "c4a4a563f698595",
  2735. "81be04b9e4a08ce",
  2736. ]
  2737. async function uploadImageToImgur(file) {
  2738. const formData = new FormData()
  2739. formData.append("image", file)
  2740. const randomIndex = Math.floor(Math.random() * imgurClientIdPool.length)
  2741. const clidenId = imgurClientIdPool[randomIndex]
  2742. const response = await fetch("https://api.imgur.com/3/upload", {
  2743. method: "POST",
  2744. headers: { Authorization: "Client-ID ".concat(clidenId) },
  2745. body: formData,
  2746. })
  2747. if (response.ok) {
  2748. const responseData = await response.json()
  2749. if (responseData.success) {
  2750. return responseData.data.link
  2751. }
  2752. }
  2753. throw new Error("\u4E0A\u4F20\u5931\u8D25")
  2754. }
  2755. var handleUploadImage = (file) => {
  2756. const detail = { file }
  2757. win.dispatchEvent(new CustomEvent("uploadImageStart", { detail }))
  2758. uploadImageToImgur(file)
  2759. .then((imgLink) => {
  2760. detail.imgLink = imgLink
  2761. win.dispatchEvent(new CustomEvent("uploadImageSuccess", { detail }))
  2762. })
  2763. .catch(() => {
  2764. win.dispatchEvent(new CustomEvent("uploadImageFailed", { detail }))
  2765. })
  2766. }
  2767. var handleClickUploadImage = () => {
  2768. const imgInput = document.createElement("input")
  2769. imgInput.style.display = "none"
  2770. imgInput.type = "file"
  2771. imgInput.accept = "image/*"
  2772. addEventListener(imgInput, "change", () => {
  2773. var _a
  2774. const selectedFile = (_a = imgInput.files) == null ? void 0 : _a[0]
  2775. if (selectedFile) {
  2776. handleUploadImage(selectedFile)
  2777. }
  2778. })
  2779. imgInput.click()
  2780. }
  2781. var init = () => {
  2782. const replyTextArea = getReplyInputElement()
  2783. if (!replyTextArea) {
  2784. return
  2785. }
  2786. const appendPosition = $("#reply-box > div > div")
  2787. if (!appendPosition) {
  2788. return
  2789. }
  2790. setAttribute(
  2791. replyTextArea,
  2792. "placeholder",
  2793. "\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"
  2794. )
  2795. const uploadTip = "+ \u63D2\u5165\u56FE\u7247"
  2796. const placeholder = "[\u4E0A\u4F20\u56FE\u7247\u4E2D...]"
  2797. addElement2(appendPosition, "span", {
  2798. class: "snow",
  2799. textContent: " \xB7 ",
  2800. })
  2801. const uploadButton = createElement("a", {
  2802. class: "vr_upload_image",
  2803. textContent: uploadTip,
  2804. })
  2805. appendPosition.append(uploadButton)
  2806. addEventListener(uploadButton, "click", () => {
  2807. if (!hasClass(uploadButton, "vr_button_disabled")) {
  2808. handleClickUploadImage()
  2809. }
  2810. })
  2811. addEventListener(
  2812. doc,
  2813. "paste",
  2814. (event) => {
  2815. var _a
  2816. if (!(event instanceof ClipboardEvent)) {
  2817. return
  2818. }
  2819. const replyTextArea2 = getReplyInputElement()
  2820. if (
  2821. !(replyTextArea2 == null ? void 0 : replyTextArea2.matches(":focus"))
  2822. ) {
  2823. return
  2824. }
  2825. const items = (_a = event.clipboardData) == null ? void 0 : _a.items
  2826. if (!items) {
  2827. return
  2828. }
  2829. const imageItem = Array.from(items).find((item) =>
  2830. item.type.includes("image")
  2831. )
  2832. if (imageItem) {
  2833. const file = imageItem.getAsFile()
  2834. if (file) {
  2835. handleUploadImage(file)
  2836. }
  2837. }
  2838. },
  2839. true
  2840. )
  2841. addEventListener(
  2842. replyTextArea,
  2843. "drop",
  2844. (event) => {
  2845. var _a
  2846. if (!(event instanceof DragEvent)) {
  2847. return
  2848. }
  2849. const files = (_a = event.dataTransfer) == null ? void 0 : _a.files
  2850. if (files == null ? void 0 : files.length) {
  2851. for (const file of files) {
  2852. if (file.type.includes("image")) {
  2853. event.preventDefault()
  2854. event.stopImmediatePropagation()
  2855. handleUploadImage(file)
  2856. }
  2857. }
  2858. }
  2859. },
  2860. true
  2861. )
  2862. addEventListener(win, {
  2863. uploadImageStart(event) {
  2864. if (!event.detail) {
  2865. return
  2866. }
  2867. const detail = event.detail
  2868. const fileName = detail.file.name || "noname"
  2869. detail.placeholder = placeholder.replace(
  2870. /]/,
  2871. " (".concat(fileName, ")]")
  2872. )
  2873. const replyTextArea2 = getReplyInputElement()
  2874. if (replyTextArea2) {
  2875. insertTextToReplyInput(
  2876. replyTextArea2.value.trim().length > 0 &&
  2877. replyTextArea2.selectionStart > 0
  2878. ? "\n".concat(detail.placeholder, "\n")
  2879. : "".concat(detail.placeholder, "\n")
  2880. )
  2881. }
  2882. },
  2883. uploadImageSuccess(event) {
  2884. if (!event.detail) {
  2885. return
  2886. }
  2887. const detail = event.detail
  2888. removeClass(uploadButton, "vr_button_disabled")
  2889. uploadButton.textContent = uploadTip
  2890. replaceReplyInputText(
  2891. detail.placeholder || placeholder,
  2892. detail.imgLink || "",
  2893. true
  2894. )
  2895. },
  2896. uploadImageFailed(event) {
  2897. if (!event.detail) {
  2898. return
  2899. }
  2900. const detail = event.detail
  2901. removeClass(uploadButton, "vr_button_disabled")
  2902. uploadButton.textContent = uploadTip
  2903. replaceReplyInputText(detail.placeholder || placeholder, "")
  2904. alert(
  2905. "[V2EX.REP] \u274C \u4E0A\u4F20\u56FE\u7247\u5931\u8D25\uFF0C\u8BF7\u6253\u5F00\u63A7\u5236\u53F0\u67E5\u770B\u539F\u56E0"
  2906. )
  2907. },
  2908. })
  2909. }
  2910. var uploadImage = () => {
  2911. runOnce("uploadImage:init", init)
  2912. }
  2913. var config = {
  2914. matches: ["https://*.v2ex.com/*"],
  2915. run_at: "document_start",
  2916. }
  2917. var settingsTable2 = {
  2918. fixReplyFloorNumbers: {
  2919. title: "\u4FEE\u590D\u697C\u5C42\u53F7",
  2920. defaultValue: true,
  2921. },
  2922. replyWithFloorNumber: {
  2923. title: "\u56DE\u590D\u65F6\u5E26\u4E0A\u697C\u5C42\u53F7",
  2924. defaultValue: true,
  2925. },
  2926. showTopReplies: {
  2927. title: "\u663E\u793A\u70ED\u95E8\u56DE\u590D",
  2928. defaultValue: true,
  2929. },
  2930. showCitedReplies: {
  2931. title: "\u663E\u793A\u88AB\u5F15\u7528\u7684\u56DE\u590D",
  2932. defaultValue: true,
  2933. },
  2934. opaticyOfCitedReplies: {
  2935. title:
  2936. "\u88AB\u5F15\u7528\u7684\u56DE\u590D\u4E0A\u9762\u906E\u7F69\u7684\u4E0D\u900F\u660E\u5EA6",
  2937. type: "select",
  2938. defaultValue: "2",
  2939. options: {
  2940. 无遮罩: "0",
  2941. 低: "1",
  2942. 中: "2",
  2943. 高: "3",
  2944. },
  2945. },
  2946. showPreviousCitedReplies: {
  2947. title:
  2948. "\u88AB\u5F15\u7528\u7684\u56DE\u590D\u662F\u524D\u4E00\u4E2A\u697C\u5C42\u65F6",
  2949. type: "select",
  2950. defaultValue: "0",
  2951. options: {
  2952. 不显示: "0",
  2953. 始终显示: "1",
  2954. },
  2955. },
  2956. filterRepliesByUser: {
  2957. title:
  2958. "\u67E5\u770B\u7528\u6237\u5728\u5F53\u524D\u4E3B\u9898\u4E0B\u7684\u6240\u6709\u56DE\u590D\u4E0E\u88AB\u63D0\u53CA\u7684\u56DE\u590D",
  2959. description:
  2960. "\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",
  2961. defaultValue: true,
  2962. },
  2963. loadMultiPages: {
  2964. title: "\u9884\u52A0\u8F7D\u6240\u6709\u5206\u9875",
  2965. defaultValue: true,
  2966. },
  2967. uploadImage: {
  2968. title: "\u56DE\u590D\u65F6\u4E0A\u4F20\u56FE\u7247",
  2969. defaultValue: true,
  2970. },
  2971. dailyCheckIn: {
  2972. title: "\u6BCF\u65E5\u81EA\u52A8\u7B7E\u5230",
  2973. defaultValue: true,
  2974. },
  2975. lazyLoadAvatars: {
  2976. title: "\u61D2\u52A0\u8F7D\u7528\u6237\u5934\u50CF\u56FE\u7247",
  2977. defaultValue: false,
  2978. },
  2979. quickSendThank: {
  2980. title: "\u5FEB\u901F\u53D1\u9001\u611F\u8C22",
  2981. defaultValue: false,
  2982. },
  2983. alwaysShowThankButton: {
  2984. title: "\u4E00\u76F4\u663E\u793A\u611F\u8C22\u6309\u94AE",
  2985. defaultValue: false,
  2986. },
  2987. quickHideReply: {
  2988. title: "\u5FEB\u901F\u9690\u85CF\u56DE\u590D",
  2989. defaultValue: false,
  2990. },
  2991. alwaysShowHideButton: {
  2992. title: "\u4E00\u76F4\u663E\u793A\u9690\u85CF\u56DE\u590D\u6309\u94AE",
  2993. defaultValue: false,
  2994. },
  2995. removeLocationHash: {
  2996. title: "\u53BB\u6389 URL \u4E2D\u7684 #replyXX",
  2997. defaultValue: true,
  2998. },
  2999. stickyTopicButtons: {
  3000. title:
  3001. "\u4E3B\u9898\u5185\u5BB9\u5E95\u90E8\u56FA\u5B9A\u663E\u793A\u6309\u94AE\u680F",
  3002. defaultValue: true,
  3003. },
  3004. quickNavigation: {
  3005. title: "\u53CC\u51FB\u7A7A\u767D\u5904\u5FEB\u901F\u5BFC\u822A",
  3006. defaultValue: false,
  3007. },
  3008. replaceFavicon: {
  3009. title: "\u66F4\u6362 favicon \u56FE\u6807",
  3010. type: "select",
  3011. defaultValue: "default",
  3012. options: {
  3013. 默认: "default",
  3014. GitHub: "github",
  3015. 用户头像: "avatar",
  3016. },
  3017. },
  3018. }
  3019. var fixedReplyFloorNumbers = false
  3020. async function process3() {
  3021. const opaticyOfCitedReplies = getSettingsValue("opaticyOfCitedReplies")
  3022. if (doc.documentElement) {
  3023. doc.documentElement.dataset.vrOpaticyOfCitedReplies =
  3024. opaticyOfCitedReplies
  3025. }
  3026. const domReady =
  3027. doc.readyState === "interactive" || doc.readyState === "complete"
  3028. if (doc.readyState === "complete" && getSettingsValue("dailyCheckIn")) {
  3029. runOnce("dailyCheckIn", () => {
  3030. setTimeout(dailyCheckIn, 1e3)
  3031. })
  3032. }
  3033. replaceFavicon(getSettingsValue("replaceFavicon"))
  3034. if (/\/t\/\d+/.test(location.href)) {
  3035. const replyElements = getReplyElements()
  3036. for (const replyElement of replyElements) {
  3037. if (!$(".reply_content", replyElement)) {
  3038. continue
  3039. }
  3040. if (getSettingsValue("lazyLoadAvatars")) {
  3041. lazyLoadAvatars(replyElement)
  3042. }
  3043. addLinkToAvatars(replyElement)
  3044. if (getSettingsValue("replyWithFloorNumber")) {
  3045. replyWithFloorNumber(replyElement)
  3046. }
  3047. if (getSettingsValue("alwaysShowThankButton")) {
  3048. alwaysShowThankButton(replyElement)
  3049. }
  3050. if (getSettingsValue("alwaysShowHideButton")) {
  3051. alwaysShowHideButton(replyElement)
  3052. }
  3053. if (getSettingsValue("quickSendThank")) {
  3054. quickSendThank(replyElement)
  3055. }
  3056. if (getSettingsValue("quickHideReply")) {
  3057. quickHideReply(replyElement)
  3058. }
  3059. addlinkToCitedFloorNumbers(replyElement)
  3060. if (getSettingsValue("showCitedReplies")) {
  3061. showCitedReplies(
  3062. replyElement,
  3063. getSettingsValue("showPreviousCitedReplies")
  3064. )
  3065. }
  3066. }
  3067. if (domReady) {
  3068. showTopReplies(replyElements, getSettingsValue("showTopReplies"))
  3069. }
  3070. stickyTopicButtons(getSettingsValue("stickyTopicButtons"))
  3071. filterRepliesByUser(getSettingsValue("filterRepliesByUser"))
  3072. if (
  3073. domReady &&
  3074. getSettingsValue("fixReplyFloorNumbers") &&
  3075. !fixedReplyFloorNumbers
  3076. ) {
  3077. await fixReplyFloorNumbers(replyElements)
  3078. }
  3079. if (domReady && getSettingsValue("uploadImage")) {
  3080. uploadImage()
  3081. }
  3082. if (domReady && getSettingsValue("removeLocationHash")) {
  3083. runOnce("main:removeLocationHash", removeLocationHash)
  3084. }
  3085. if (domReady && getSettingsValue("quickNavigation")) {
  3086. quickNavigation()
  3087. }
  3088. if (doc.readyState === "complete" && getSettingsValue("loadMultiPages")) {
  3089. runOnce("main:loadMultiPages", () => {
  3090. setTimeout(loadMultiPages, 1e3)
  3091. })
  3092. }
  3093. }
  3094. }
  3095. async function main() {
  3096. await initSettings({
  3097. id: "v2ex.rep",
  3098. title: "V2EX.REP",
  3099. footer:
  3100. '\n <p>\u66F4\u6539\u8BBE\u7F6E\u540E\uFF0C\u9700\u8981\u91CD\u65B0\u52A0\u8F7D\u9875\u9762</p>\n <p>\n <a href="https://github.com/v2hot/v2ex.rep/issues" target="_blank">\n \u95EE\u9898\u53CD\u9988\n </a></p>\n <p>Made with \u2764\uFE0F by\n <a href="https://www.pipecraft.net/" target="_blank">\n Pipecraft\n </a></p>',
  3101. settingsTable: settingsTable2,
  3102. async onValueChange() {
  3103. await process3()
  3104. },
  3105. })
  3106. addStyle(content_default)
  3107. const resetCachedReplyElementsThenProcess = async () => {
  3108. resetCachedReplyElements()
  3109. await process3()
  3110. }
  3111. addEventListener(win, {
  3112. floorNumberUpdated() {
  3113. fixedReplyFloorNumbers = true
  3114. if (
  3115. getSettingsValue("replyWithFloorNumber") ||
  3116. getSettingsValue("showCitedReplies")
  3117. ) {
  3118. const replyElements = getReplyElements()
  3119. for (const replyElement of replyElements) {
  3120. if (getSettingsValue("replyWithFloorNumber")) {
  3121. replyWithFloorNumber(replyElement, true)
  3122. }
  3123. if (getSettingsValue("showCitedReplies")) {
  3124. showCitedReplies(
  3125. replyElement,
  3126. getSettingsValue("showPreviousCitedReplies"),
  3127. true
  3128. )
  3129. }
  3130. }
  3131. }
  3132. },
  3133. async replyElementsLengthUpdated() {
  3134. await resetCachedReplyElementsThenProcess()
  3135. const replyElements = getCachedReplyElements()
  3136. for (const replyElement of replyElements) {
  3137. if (getSettingsValue("showCitedReplies")) {
  3138. showCitedReplies(
  3139. replyElement,
  3140. getSettingsValue("showPreviousCitedReplies"),
  3141. true
  3142. )
  3143. }
  3144. }
  3145. showTopReplies(replyElements, getSettingsValue("showTopReplies"), true)
  3146. if (getSettingsValue("fixReplyFloorNumbers")) {
  3147. await fixReplyFloorNumbers(replyElements)
  3148. }
  3149. },
  3150. })
  3151. addEventListener(
  3152. doc,
  3153. "readystatechange",
  3154. resetCachedReplyElementsThenProcess
  3155. )
  3156. await process3()
  3157. const scanNodes = throttle(async () => {
  3158. await process3()
  3159. }, 500)
  3160. addEventListener(doc, "visibilitychange", async () => {
  3161. if (!doc.hidden) {
  3162. await process3()
  3163. }
  3164. })
  3165. const observer = new MutationObserver((mutationsList) => {
  3166. scanNodes()
  3167. })
  3168. observer.observe($("#Main") || doc, {
  3169. childList: true,
  3170. subtree: true,
  3171. })
  3172. }
  3173. runWhenBodyExists(async () => {
  3174. if (doc.documentElement.dataset.v2exRep === void 0) {
  3175. doc.documentElement.dataset.v2exRep = ""
  3176. await main()
  3177. }
  3178. })
  3179. })()