▲V2EX Polish - 体验更现代化的 V2EX 🟢

一款专为 V2EX 用户设计的浏览器插件,提供了丰富的扩展功能,让原生页面焕然一新!✨

  1. // ==UserScript==
  2. // @name ▲V2EX Polish - 体验更现代化的 V2EX 🟢
  3. // @namespace https://v2p.app
  4. // @version 2.0.5
  5. // @description 一款专为 V2EX 用户设计的浏览器插件,提供了丰富的扩展功能,让原生页面焕然一新!✨
  6. // @author LeoKu(https://leoku.dev)
  7. // @match https://*.v2ex.com/*
  8. // @match https://v2ex.com/*
  9. // @icon https://v2p.app/favicon.svg
  10. // @run-at document-start
  11. // @grant GM_addStyle
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. "use strict";
  16. var __getOwnPropNames = Object.getOwnPropertyNames;
  17. var __esm = (fn, res) => function __init() {
  18. return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
  19. };
  20.  
  21. // src/contents/polyfill.ts
  22. var init_polyfill = __esm({
  23. "src/contents/polyfill.ts"() {
  24. "use strict";
  25. {
  26. if (!window.requestIdleCallback) {
  27. window.requestIdleCallback = function(callback) {
  28. const start = Date.now();
  29. return setTimeout(function() {
  30. callback({
  31. didTimeout: false,
  32. timeRemaining: function() {
  33. return Math.max(0, 50 - (Date.now() - start));
  34. }
  35. });
  36. }, 1);
  37. };
  38. }
  39. if (!window.cancelIdleCallback) {
  40. window.cancelIdleCallback = function(id) {
  41. clearTimeout(id);
  42. };
  43. }
  44. }
  45. }
  46. });
  47.  
  48. // src/constants.ts
  49. var EXTENSION_NAME, emojiLinks, emoticons, READABLE_CONTENT_HEIGHT, MAX_CONTENT_HEIGHT, READING_CONTENT_LIMIT, dataExpiryTime, imgurClientIdPool, defaultOptions;
  50. var init_constants = __esm({
  51. "src/constants.ts"() {
  52. "use strict";
  53. EXTENSION_NAME = "V2EX_Polish";
  54. emojiLinks = {
  55. // B 站表情。
  56. ["[\u8131\u5355doge]" /* 脱单doge */]: {
  57. ld: "https://i.imgur.com/L62ZP7V.png",
  58. hd: "https://i.imgur.com/3mPhudo.png"
  59. },
  60. ["[doge]" /* doge */]: {
  61. ld: "https://i.imgur.com/agAJ0Rd.png",
  62. hd: "https://i.imgur.com/HZL0hOa.png"
  63. },
  64. ["[\u8FA3\u773C\u775B]" /* 辣眼睛 */]: {
  65. ld: "https://i.imgur.com/n119Wvk.png",
  66. hd: "https://i.imgur.com/A5WXoZJ.png"
  67. },
  68. ["[\u7591\u60D1]" /* 疑惑 */]: {
  69. ld: "https://i.imgur.com/U3hKhrT.png",
  70. hd: "https://i.imgur.com/3gCygBS.png"
  71. },
  72. ["[\u6342\u8138]" /* 捂脸 */]: {
  73. ld: "https://i.imgur.com/14cwgsI.png",
  74. hd: "https://i.imgur.com/fLp3t8s.png"
  75. },
  76. ["[\u54E6\u547C]" /* 哦呼 */]: {
  77. ld: "https://i.imgur.com/km62MY2.png",
  78. hd: "https://i.imgur.com/CXXgF4E.png"
  79. },
  80. ["[\u50B2\u5A07]" /* 傲娇 */]: {
  81. ld: "https://i.imgur.com/TkdeN49.png",
  82. hd: "https://i.imgur.com/m7IlCrD.png"
  83. },
  84. ["[\u601D\u8003]" /* 思考 */]: {
  85. ld: "https://i.imgur.com/MAyk5GN.png",
  86. hd: "https://i.imgur.com/eRJTCx7.png"
  87. },
  88. ["[\u5403\u74DC]" /* 吃瓜 */]: {
  89. ld: "https://i.imgur.com/Ug1iMq4.png",
  90. hd: "https://i.imgur.com/Gy3nwkC.png"
  91. },
  92. ["[\u65E0\u8BED]" /* 无语 */]: {
  93. ld: "https://i.imgur.com/e1q9ScT.png",
  94. hd: "https://i.imgur.com/wMfcBqD.png"
  95. },
  96. ["[\u5927\u54ED]" /* 大哭 */]: {
  97. ld: "https://i.imgur.com/YGIx7lh.png",
  98. hd: "https://i.imgur.com/SNHJxtv.png"
  99. },
  100. ["[\u9178\u4E86]" /* 酸了 */]: {
  101. ld: "https://i.imgur.com/5FDsp6L.png",
  102. hd: "https://i.imgur.com/wnQBodT.png"
  103. },
  104. ["[\u6253call]" /* 打call */]: {
  105. ld: "https://i.imgur.com/pmNOo2w.png",
  106. hd: "https://i.imgur.com/4GfTlV0.png"
  107. },
  108. ["[\u6B6A\u5634]" /* 歪嘴 */]: {
  109. ld: "https://i.imgur.com/XzEYBoY.png",
  110. hd: "https://i.imgur.com/84ycU43.png"
  111. },
  112. ["[\u661F\u661F\u773C]" /* 星星眼 */]: {
  113. ld: "https://i.imgur.com/2spsghH.png",
  114. hd: "https://i.imgur.com/oEIJRru.png"
  115. },
  116. ["[OK]" /* OK */]: {
  117. ld: "https://i.imgur.com/6DMydmQ.png",
  118. hd: "https://i.imgur.com/PE2dyjY.png"
  119. },
  120. ["[\u8DEA\u4E86]" /* 跪了 */]: {
  121. ld: "https://i.imgur.com/TYtySHv.png",
  122. hd: "https://i.imgur.com/0pjsMf0.png"
  123. },
  124. ["[\u54CD\u6307]" /* 响指 */]: {
  125. ld: "https://i.imgur.com/Ac88cMm.png",
  126. hd: "https://i.imgur.com/nkoevMu.png"
  127. },
  128. ["[\u8C03\u76AE]" /* 调皮 */]: {
  129. ld: "https://i.imgur.com/O6ZZSLk.png",
  130. hd: "https://i.imgur.com/ggHTLzH.png"
  131. },
  132. ["[\u7B11\u54ED]" /* 笑哭 */]: {
  133. ld: "https://i.imgur.com/NIvxivj.png",
  134. hd: "https://i.imgur.com/h8edr5G.png"
  135. },
  136. ["[\u55D1\u74DC\u5B50]" /* 嗑瓜子 */]: {
  137. ld: "https://i.imgur.com/rjR4rdr.png",
  138. hd: "https://i.imgur.com/GMzq0tq.png"
  139. },
  140. ["[\u559C\u6781\u800C\u6CE3]" /* 喜极而泣 */]: {
  141. ld: "https://i.imgur.com/N9E3iZ2.png",
  142. hd: "https://i.imgur.com/L1N27tb.png"
  143. },
  144. ["[\u60CA\u8BB6]" /* 惊讶 */]: {
  145. ld: "https://i.imgur.com/aptfuiN.png",
  146. hd: "https://i.imgur.com/cuzxGOI.png"
  147. },
  148. ["[\u7ED9\u5FC3\u5FC3]" /* 给心心 */]: {
  149. ld: "https://i.imgur.com/4aXVwxJ.png",
  150. hd: "https://i.imgur.com/q663Mor.png"
  151. },
  152. ["[\u5446]" /* 呆 */]: {
  153. ld: "https://i.imgur.com/c1Q76Cd.png",
  154. hd: "https://i.imgur.com/xMXlmxm.png"
  155. },
  156. // 小红薯表情。
  157. ["[\u54ED\u60F9R]" /* 哭惹 */]: {
  158. ld: "https://i.imgur.com/HgxsUD2.png",
  159. hd: "https://i.imgur.com/0aOdQJd.png"
  160. },
  161. ["[\u54C7R]" /* 哇 */]: {
  162. ld: "https://i.imgur.com/OZySWIG.png",
  163. hd: "https://i.imgur.com/ngoi2I6.png"
  164. },
  165. ["[\u6C57\u989CR]" /* 汗颜 */]: {
  166. ld: "https://i.imgur.com/jrVZoLi.png",
  167. hd: "https://i.imgur.com/O8alqc1.png"
  168. },
  169. ["[\u5BB3\u7F9ER]" /* 害羞 */]: {
  170. ld: "https://i.imgur.com/OVQjxIr.png",
  171. hd: "https://i.imgur.com/1PeoVR5.png"
  172. },
  173. ["[\u840C\u840C\u54D2R]" /* 萌萌哒 */]: {
  174. ld: "https://i.imgur.com/Ue1kikn.png",
  175. hd: "https://i.imgur.com/vOHzwus.png"
  176. },
  177. ["[\u5077\u7B11R]" /* 偷笑 */]: {
  178. ld: "https://i.imgur.com/aF7QiE5.png",
  179. hd: "https://i.imgur.com/WneGpK9.png"
  180. },
  181. ["[\u4E70\u7206R]" /* 买爆 */]: {
  182. ld: "https://i.imgur.com/2JhZFtb.png",
  183. hd: "https://i.imgur.com/za9t585.png"
  184. },
  185. ["[\u8272\u8272R]" /* 色色 */]: {
  186. ld: "https://i.imgur.com/ZA1jRv1.png",
  187. hd: "https://i.imgur.com/mEGRKJy.png"
  188. },
  189. ["[\u62A0\u9F3BR]" /* 抠鼻 */]: {
  190. ld: "https://i.imgur.com/pYtTFnj.png",
  191. hd: "https://i.imgur.com/ErnQrMJ.png"
  192. },
  193. ["[\u9ED1\u85AF\u95EE\u53F7R]" /* 黑薯问号 */]: {
  194. ld: "https://i.imgur.com/aCjmFLD.png",
  195. hd: "https://i.imgur.com/i4Wgtyv.png"
  196. },
  197. ["[\u6276\u5899R]" /* 扶墙 */]: {
  198. ld: "https://i.imgur.com/RV7y6tR.png",
  199. hd: "https://i.imgur.com/PjhjZsJ.png"
  200. },
  201. ["[\u9119\u89C6R]" /* 鄙视 */]: {
  202. ld: "https://i.imgur.com/LaO5dh3.png",
  203. hd: "https://i.imgur.com/StrGaFx.png"
  204. },
  205. ["[\u8E72R]" /* 蹲 */]: {
  206. ld: "https://i.imgur.com/t876WSv.png",
  207. hd: "https://i.imgur.com/jdTq0YI.png"
  208. },
  209. ["[\u5E86\u795DR]" /* 庆祝 */]: {
  210. ld: "https://i.imgur.com/wQw2kD0.png",
  211. hd: "https://i.imgur.com/lx6jrkm.png"
  212. },
  213. ["[\u516DR]" /* 六 */]: {
  214. ld: "https://i.imgur.com/JqoC4L5.png",
  215. hd: "https://i.imgur.com/cUVWKc2.png"
  216. },
  217. ["[\u53EFR]" /* 可 */]: {
  218. ld: "https://i.imgur.com/I70yy88.png",
  219. hd: "https://i.imgur.com/nRgXwUT.png"
  220. },
  221. ["[\u52A0\u4E00R]" /* 加一 */]: {
  222. ld: "https://i.imgur.com/hpVvbVh.png",
  223. hd: "https://i.imgur.com/abBCCK9.png"
  224. }
  225. };
  226. emoticons = [
  227. {
  228. title: "\u6D41\u884C",
  229. list: [
  230. "[\u8131\u5355doge]" /* 脱单doge */,
  231. "[doge]" /* doge */,
  232. "[\u6253call]" /* 打call */,
  233. "[\u661F\u661F\u773C]" /* 星星眼 */,
  234. "[\u5403\u74DC]" /* 吃瓜 */,
  235. "[OK]" /* OK */,
  236. "[\u54E6\u547C]" /* 哦呼 */,
  237. "[\u601D\u8003]" /* 思考 */,
  238. "[\u7591\u60D1]" /* 疑惑 */,
  239. "[\u8FA3\u773C\u775B]" /* 辣眼睛 */,
  240. "[\u50B2\u5A07]" /* 傲娇 */,
  241. "[\u6342\u8138]" /* 捂脸 */,
  242. "[\u65E0\u8BED]" /* 无语 */,
  243. "[\u5927\u54ED]" /* 大哭 */,
  244. "[\u9178\u4E86]" /* 酸了 */,
  245. "[\u6B6A\u5634]" /* 歪嘴 */,
  246. "[\u8C03\u76AE]" /* 调皮 */,
  247. "[\u7B11\u54ED]" /* 笑哭 */,
  248. "[\u55D1\u74DC\u5B50]" /* 嗑瓜子 */,
  249. "[\u559C\u6781\u800C\u6CE3]" /* 喜极而泣 */,
  250. "[\u60CA\u8BB6]" /* 惊讶 */,
  251. "[\u7ED9\u5FC3\u5FC3]" /* 给心心 */,
  252. "[\u5446]" /* 呆 */,
  253. "[\u8DEA\u4E86]" /* 跪了 */,
  254. "[\u54CD\u6307]" /* 响指 */,
  255. "[\u54C7R]" /* 哇 */,
  256. "[\u840C\u840C\u54D2R]" /* 萌萌哒 */,
  257. "[\u5BB3\u7F9ER]" /* 害羞 */,
  258. "[\u5077\u7B11R]" /* 偷笑 */,
  259. "[\u54ED\u60F9R]" /* 哭惹 */,
  260. "[\u6C57\u989CR]" /* 汗颜 */,
  261. "[\u8272\u8272R]" /* 色色 */,
  262. "[\u62A0\u9F3BR]" /* 抠鼻 */,
  263. "[\u9119\u89C6R]" /* 鄙视 */,
  264. "[\u4E70\u7206R]" /* 买爆 */,
  265. "[\u9ED1\u85AF\u95EE\u53F7R]" /* 黑薯问号 */,
  266. "[\u6276\u5899R]" /* 扶墙 */,
  267. "[\u8E72R]" /* 蹲 */,
  268. "[\u53EFR]" /* 可 */,
  269. "[\u516DR]" /* 六 */,
  270. "[\u52A0\u4E00R]" /* 加一 */,
  271. "[\u5E86\u795DR]" /* 庆祝 */
  272. ]
  273. },
  274. {
  275. title: "\u5C0F\u9EC4\u8138",
  276. list: [
  277. "\u{1F600}",
  278. "\u{1F601}",
  279. "\u{1F602}",
  280. "\u{1F923}",
  281. "\u{1F605}",
  282. "\u{1F60A}",
  283. "\u{1F60B}",
  284. "\u{1F618}",
  285. "\u{1F970}",
  286. "\u{1F617}",
  287. "\u{1F929}",
  288. "\u{1F914}",
  289. "\u{1F928}",
  290. "\u{1F610}",
  291. "\u{1F611}",
  292. "\u{1F644}",
  293. "\u{1F60F}",
  294. "\u{1F62A}",
  295. "\u{1F62B}",
  296. "\u{1F971}",
  297. "\u{1F61C}",
  298. "\u{1F612}",
  299. "\u{1F614}",
  300. "\u{1F628}",
  301. "\u{1F630}",
  302. "\u{1F631}",
  303. "\u{1F975}",
  304. "\u{1F621}",
  305. "\u{1F973}",
  306. "\u{1F97A}",
  307. "\u{1F92D}",
  308. "\u{1F9D0}",
  309. "\u{1F60E}",
  310. "\u{1F913}",
  311. "\u{1F62D}",
  312. "\u{1F911}",
  313. "\u{1F92E}"
  314. ]
  315. },
  316. {
  317. title: "\u624B\u52BF",
  318. list: [
  319. "\u{1F64B}",
  320. "\u{1F64E}",
  321. "\u{1F645}",
  322. "\u{1F647}",
  323. "\u{1F937}",
  324. "\u{1F90F}",
  325. "\u{1F449}",
  326. "\u270C\uFE0F",
  327. "\u{1F918}",
  328. "\u{1F919}",
  329. "\u{1F44C}",
  330. "\u{1F90C}",
  331. "\u{1F44D}",
  332. "\u{1F44E}",
  333. "\u{1F44B}",
  334. "\u{1F91D}",
  335. "\u{1F64F}",
  336. "\u{1F44F}"
  337. ]
  338. },
  339. {
  340. title: "\u5E86\u795D",
  341. list: ["\u2728", "\u{1F389}", "\u{1F38A}"]
  342. },
  343. {
  344. title: "\u5176\u4ED6",
  345. list: ["\u{1F47B}", "\u{1F921}", "\u{1F414}", "\u{1F440}", "\u{1F4A9}", "\u{1F434}", "\u{1F984}", "\u{1F427}", "\u{1F436}", "\u{1F412}", "\u{1F648}", "\u{1F649}", "\u{1F64A}", "\u{1F435}"]
  346. }
  347. ];
  348. READABLE_CONTENT_HEIGHT = 250;
  349. MAX_CONTENT_HEIGHT = 550;
  350. READING_CONTENT_LIMIT = 150;
  351. dataExpiryTime = 60 * 60 * 1e3;
  352. imgurClientIdPool = [
  353. "3107b9ef8b316f3",
  354. // 以下 Client ID 来自「V2EX Plus」
  355. "442b04f26eefc8a",
  356. "59cfebe717c09e4",
  357. "60605aad4a62882",
  358. "6c65ab1d3f5452a",
  359. "83e123737849aa9",
  360. "9311f6be1c10160",
  361. "c4a4a563f698595",
  362. "81be04b9e4a08ce"
  363. ];
  364. defaultOptions = {
  365. openInNewTab: false,
  366. autoCheckIn: {
  367. enabled: true
  368. },
  369. theme: {
  370. autoSwitch: false
  371. },
  372. reply: {
  373. preload: "auto",
  374. layout: "vertical"
  375. },
  376. replyContent: {
  377. autoFold: true,
  378. hideReplyTime: true,
  379. hideRefName: true,
  380. showImgInPage: true
  381. },
  382. nestedReply: {
  383. display: "indent",
  384. multipleInsideOne: "nested"
  385. },
  386. userTag: {
  387. display: "inline"
  388. },
  389. contextMenu: {
  390. enabled: true
  391. }
  392. };
  393. }
  394. });
  395.  
  396. // src/icons.ts
  397. var iconLogo, iconGitHub;
  398. var init_icons = __esm({
  399. "src/icons.ts"() {
  400. "use strict";
  401. iconLogo = `
  402. <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 88 88"><g style="mix-blend-mode:passthrough"><path d="M87.92 86.098v-.052a.592.592 0 0 0 0-.07L44.978.72l-.059-.105c-.16-.3-.415-.511-.705-.586a.961.961 0 0 0-.841.19 1.315 1.315 0 0 0-.336.378l-.058.115a2571.004 2571.004 0 0 1-8.695 17.172c-.59 1.024-.59 2.382 0 3.406 3.856 7.57 7.7 15.142 11.532 22.718.641 1.108.641 2.58 0 3.688C39.5 60.23 32.826 73.406 26.45 85.993c-.291.661-.086 1.482.46 1.84.16.104.341.158.525.158h18.52c.415.003.797-.272.992-.713l.635-1.285 8.585-17.023c.142-.317.383-.552.67-.653a.949.949 0 0 1 .855.116c.156.1.289.245.386.423l8.506 16.723.787 1.558c.199.433.575.702.985.704h.518c.087.009.175.009.263 0h17.74c.617 0 1.119-.601 1.123-1.347a1.615 1.615 0 0 0-.08-.396Z" fill="currentColor" style="mix-blend-mode:passthrough"/><path d="m38.551 48.541.62-1.232a3.095 3.095 0 0 0 0-3.02l-3.807-7.446-4.377-8.511c-.155-.308-.406-.527-.697-.61a.957.957 0 0 0-.85.17 1.252 1.252 0 0 0-.4.502L.132 86.002c-.29.658-.085 1.477.46 1.83.161.113.345.17.532.168h16.981c.41 0 .788-.27.985-.705l.65-1.302c.029-.048.055-.098.08-.15l.729-1.408c6.047-12.103 11.839-23.66 17.9-35.7.038-.062.072-.127.102-.194Z" fill="currentColor" style="mix-blend-mode:passthrough"/></g></svg>
  403. `;
  404. iconGitHub = `
  405. <svg viewBox="0 0 24 24" aria-hidden="true">
  406. <path fill="currentColor" clip-rule="evenodd" d="M12 2C6.477 2 2 6.463 2 11.97c0 4.404 2.865 8.14 6.839 9.458.5.092.682-.216.682-.48 0-.236-.008-.864-.013-1.695-2.782.602-3.369-1.337-3.369-1.337-.454-1.151-1.11-1.458-1.11-1.458-.908-.618.069-.606.069-.606 1.003.07 1.531 1.027 1.531 1.027.892 1.524 2.341 1.084 2.91.828.092-.643.35-1.083.636-1.332-2.22-.251-4.555-1.107-4.555-4.927 0-1.088.39-1.979 1.029-2.675-.103-.252-.446-1.266.098-2.638 0 0 .84-.268 2.75 1.022A9.607 9.607 0 0 1 12 6.82c.85.004 1.705.114 2.504.336 1.909-1.29 2.747-1.022 2.747-1.022.546 1.372.202 2.386.1 2.638.64.696 1.028 1.587 1.028 2.675 0 3.83-2.339 4.673-4.566 4.92.359.307.678.915.678 1.846 0 1.332-.012 2.407-.012 2.734 0 .267.18.577.688.48 3.97-1.32 6.833-5.054 6.833-9.458C22 6.463 17.522 2 12 2Z"></path>
  407. </svg>
  408. `;
  409. }
  410. });
  411.  
  412. // src/utils.ts
  413. function getOS() {
  414. const userAgent = window.navigator.userAgent.toLowerCase();
  415. const macosPlatforms = /(macintosh|macintel|macppc|mac68k|macos)/i;
  416. const windowsPlatforms = /(win32|win64|windows|wince)/i;
  417. const iosPlatforms = /(iphone|ipad|ipod)/i;
  418. let os = null;
  419. if (macosPlatforms.test(userAgent)) {
  420. os = "macos";
  421. } else if (iosPlatforms.test(userAgent)) {
  422. os = "ios";
  423. } else if (windowsPlatforms.test(userAgent)) {
  424. os = "windows";
  425. } else if (userAgent.includes("android")) {
  426. os = "android";
  427. } else if (userAgent.includes("linux")) {
  428. os = "linux";
  429. }
  430. return os;
  431. }
  432. function formatTimestamp(timestamp, { format = "YMD" } = {}) {
  433. const date = new Date(timestamp.toString().length === 10 ? timestamp * 1e3 : timestamp);
  434. const year = date.getFullYear().toString();
  435. const month = (date.getMonth() + 1).toString().padStart(2, "0");
  436. const day = date.getDate().toString().padStart(2, "0");
  437. const YMD = `${year}-${month}-${day}`;
  438. if (format === "YMDHM") {
  439. const hour = date.getHours().toString().padStart(2, "0");
  440. const minute = date.getMinutes().toString().padStart(2, "0");
  441. return `${YMD} ${hour}:${minute}`;
  442. }
  443. if (format === "YMDHMS") {
  444. const hour = date.getHours().toString().padStart(2, "0");
  445. const minute = date.getMinutes().toString().padStart(2, "0");
  446. const second = date.getSeconds().toString().padStart(2, "0");
  447. return `${YMD} ${hour}:${minute}:${second}`;
  448. }
  449. return YMD;
  450. }
  451. function isSameDay(timestamp1, timestamp2) {
  452. const date1 = new Date(timestamp1);
  453. const date2 = new Date(timestamp2);
  454. return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate();
  455. }
  456. function isObject(value) {
  457. return typeof value === "object" && value !== null && !Array.isArray(value);
  458. }
  459. function deepMerge(target, source) {
  460. const result = {};
  461. for (const key in target) {
  462. const targetProp = target[key];
  463. const sourceProp = source[key];
  464. if (isObject(targetProp) && isObject(sourceProp)) {
  465. result[key] = deepMerge(targetProp, sourceProp);
  466. } else if (Reflect.has(source, key)) {
  467. result[key] = sourceProp;
  468. } else {
  469. result[key] = targetProp;
  470. }
  471. }
  472. for (const key in source) {
  473. if (!Reflect.has(target, key)) {
  474. result[key] = source[key];
  475. }
  476. }
  477. return result;
  478. }
  479. function getRunEnv() {
  480. if (typeof chrome === "object" && typeof chrome.extension !== "undefined") {
  481. return "chrome";
  482. }
  483. if (typeof browser === "object" && typeof browser.extension !== "undefined") {
  484. return "web-ext";
  485. }
  486. return null;
  487. }
  488. function isBrowserExtension() {
  489. const runEnv = getRunEnv();
  490. return runEnv === "chrome" || runEnv === "web-ext";
  491. }
  492. function escapeHTML(htmlString) {
  493. return htmlString.replace(/[<>&"'']/g, (match) => {
  494. switch (match) {
  495. case "<":
  496. return "&lt;";
  497. case ">":
  498. return "&gt;";
  499. case "&":
  500. return "&amp;";
  501. case '"':
  502. return "&quot;";
  503. case "'":
  504. return "&#39;";
  505. default:
  506. return match;
  507. }
  508. });
  509. }
  510. function injectScript(scriptSrc) {
  511. const script = document.createElement("script");
  512. script.setAttribute("type", "text/javascript");
  513. script.setAttribute("src", scriptSrc);
  514. document.body.appendChild(script);
  515. }
  516. function isValidSettings(settings) {
  517. return !!settings && typeof settings === "object" && "options" /* Options */ in settings;
  518. }
  519. function sleep(ms) {
  520. return new Promise((resolve) => setTimeout(resolve, ms));
  521. }
  522. async function getV2P_Settings() {
  523. let noteId;
  524. {
  525. const res = await fetch(`${V2EX_ORIGIN}/notes`);
  526. const htmlText = await res.text();
  527. const $page = $(htmlText);
  528. const $note = $page.find('.note_item > .note_item_title > a[href^="/notes"]');
  529. $note.each((_, dom) => {
  530. const $dom = $(dom);
  531. if ($dom.text().startsWith(mark)) {
  532. const href = $dom.attr("href");
  533. if (typeof href === "string") {
  534. const id = href.split("/").at(2);
  535. noteId = id;
  536. }
  537. return false;
  538. }
  539. });
  540. }
  541. if (noteId) {
  542. const res = await fetch(`${V2EX_ORIGIN}/notes/edit/${noteId}`);
  543. const htmlText = await res.text();
  544. const $editor = $(htmlText).find("#note_content.note_editor");
  545. const value = $editor.val();
  546. if (typeof value === "string") {
  547. const syncSettings = JSON.parse(value.replace(mark, ""));
  548. if (isValidSettings(syncSettings)) {
  549. return { noteId, config: syncSettings };
  550. }
  551. }
  552. }
  553. }
  554. async function setV2P_Settings(storageSettings, signal) {
  555. const data = await getV2P_Settings();
  556. const updating = !!data;
  557. const formData = new FormData();
  558. const syncVersion = updating ? data.config["settings-sync" /* SyncInfo */].version + 1 : 1;
  559. const syncInfo = {
  560. version: syncVersion,
  561. lastSyncTime: Date.now()
  562. };
  563. formData.append(
  564. "content",
  565. mark + JSON.stringify({ ...storageSettings, ["settings-sync" /* SyncInfo */]: syncInfo })
  566. );
  567. formData.append("syntax", "0");
  568. if (updating) {
  569. const { noteId } = data;
  570. await fetch(`${V2EX_ORIGIN}/notes/edit/${noteId}`, {
  571. method: "POST",
  572. body: formData,
  573. signal
  574. });
  575. } else {
  576. formData.append("parent_id", "0");
  577. await fetch(`${V2EX_ORIGIN}/notes/new`, {
  578. method: "POST",
  579. body: formData,
  580. signal
  581. });
  582. }
  583. await setStorage("settings-sync" /* SyncInfo */, syncInfo);
  584. return syncInfo;
  585. }
  586. function getStorage(useCache = true) {
  587. return new Promise((resolve, reject) => {
  588. if (useCache) {
  589. if (window.__V2P_StorageCache) {
  590. resolve(window.__V2P_StorageCache);
  591. }
  592. }
  593. if (!isBrowserExtension()) {
  594. const data = { ["options" /* Options */]: defaultOptions };
  595. if (typeof window !== "undefined") {
  596. window.__V2P_StorageCache = data;
  597. }
  598. resolve(data);
  599. } else {
  600. chrome.storage.sync.get().then((items) => {
  601. let data;
  602. const options = items["options" /* Options */];
  603. if (options) {
  604. data = { ...items, ["options" /* Options */]: deepMerge(defaultOptions, options) };
  605. } else {
  606. data = { ...items, ["options" /* Options */]: defaultOptions };
  607. }
  608. if (typeof window !== "undefined") {
  609. window.__V2P_StorageCache = data;
  610. }
  611. resolve(data);
  612. }).catch(() => {
  613. reject(new Error("\u83B7\u53D6\u6269\u5C55\u914D\u7F6E\u5931\u8D25\u3002"));
  614. });
  615. }
  616. });
  617. }
  618. function getStorageSync() {
  619. const storage = window.__V2P_StorageCache;
  620. if (!storage) {
  621. throw new Error(`${EXTENSION_NAME}: \u65E0\u53EF\u7528\u7684 Storage \u7F13\u5B58\u6570\u636E`);
  622. }
  623. return storage;
  624. }
  625. async function setStorage(storageKey, storageItem) {
  626. switch (storageKey) {
  627. case "options" /* Options */:
  628. case "api" /* API */:
  629. case "daily" /* Daily */:
  630. case "member-tag" /* MemberTag */:
  631. case "settings-sync" /* SyncInfo */:
  632. case "reading-list" /* ReadingList */:
  633. try {
  634. await chrome.storage.sync.set({ [storageKey]: storageItem });
  635. if (storageKey !== "api" /* API */ && storageKey !== "settings-sync" /* SyncInfo */ && typeof $ !== "undefined") {
  636. const settings = await getStorage(false);
  637. if (controller) {
  638. controller.abort();
  639. }
  640. controller = new AbortController();
  641. setV2P_Settings(settings, controller.signal);
  642. }
  643. } catch (err) {
  644. if (String(err).includes("QUOTA_BYTES_PER_ITEM quota exceeded")) {
  645. console.error(
  646. `${EXTENSION_NAME}: \u65E0\u6CD5\u8BBE\u7F6E ${storageKey}\uFF0C \u5355\u4E2A item \u4E0D\u80FD\u8D85\u51FA 8 KB\uFF0C\u8BE6\u60C5\u67E5\u770B\uFF1Ahttps://developer.chrome.com/docs/extensions/reference/storage/#storage-areas`
  647. );
  648. }
  649. console.error(err);
  650. throw new Error(`\u274C \u65E0\u6CD5\u8BBE\u7F6E\uFF1A${storageKey}`);
  651. }
  652. break;
  653. default:
  654. throw new Error(`\u672A\u77E5\u7684 storageKey\uFF1A ${storageKey}`);
  655. }
  656. }
  657. var V2EX_ORIGIN, mark, controller;
  658. var init_utils = __esm({
  659. "src/utils.ts"() {
  660. "use strict";
  661. init_constants();
  662. V2EX_ORIGIN = typeof window !== "undefined" && window.location.origin.includes("v2ex.com") ? window.location.origin : "https://www.v2ex.com" /* Origin */;
  663. mark = `${EXTENSION_NAME}_settings`;
  664. controller = null;
  665. }
  666. });
  667.  
  668. // src/contents/globals.ts
  669. function updateCommentCells() {
  670. $commentCells = $commentBox.find('.cell[id^="r_"]');
  671. $commentTableRows = $commentCells.find("> table > tbody > tr");
  672. }
  673. var $body, $wrapper, $wrapperContent, $main, $topicList, $infoCard, $topicContentBox, $topicHeader, $commentBox, $commentCells, $commentTableRows, $replyBox, $replyForm, $replyTextArea, replyTextArea, loginName, topicOwnerName, pathTopicId;
  674. var init_globals = __esm({
  675. "src/contents/globals.ts"() {
  676. "use strict";
  677. $body = $(document.body);
  678. $wrapper = $("#Wrapper");
  679. $wrapperContent = $wrapper.find("> .content");
  680. $main = $("#Main");
  681. $topicList = $(
  682. "#Main #Tabs ~ .cell.item, #Main #TopicsNode > .cell, #Main .cell.item:has(.item_title > .topic-link)"
  683. );
  684. $infoCard = $('#Rightbar > .box:has("#member-activity")');
  685. $topicContentBox = $("#Main .box:has(.topic_buttons)");
  686. $topicHeader = $topicContentBox.find(".header");
  687. $commentBox = $('#Main .box:has(.cell[id^="r_"])');
  688. $commentCells = $commentBox.find('.cell[id^="r_"]');
  689. $commentTableRows = $commentCells.find("> table > tbody > tr");
  690. $replyBox = $("#reply-box");
  691. $replyForm = $replyBox.find('form[action^="/t"]');
  692. $replyTextArea = $("#reply_content");
  693. replyTextArea = document.querySelector("#reply_content");
  694. loginName = $('#Top .tools > a[href^="/member"]').text();
  695. topicOwnerName = $topicHeader.find('> small > a[href^="/member"]').text();
  696. pathTopicId = window.location.pathname.match(/\/t\/(\d+)/)?.at(1);
  697. }
  698. });
  699.  
  700. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/createElement.js
  701. var createElement, createElement$1;
  702. var init_createElement = __esm({
  703. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/createElement.js"() {
  704. "use strict";
  705. createElement = (tag, attrs, children = []) => {
  706. const element = document.createElementNS("http://www.w3.org/2000/svg", tag);
  707. Object.keys(attrs).forEach((name) => {
  708. element.setAttribute(name, String(attrs[name]));
  709. });
  710. if (children.length) {
  711. children.forEach((child) => {
  712. const childElement = createElement(...child);
  713. element.appendChild(childElement);
  714. });
  715. }
  716. return element;
  717. };
  718. createElement$1 = ([tag, attrs, children]) => createElement(tag, attrs, children);
  719. }
  720. });
  721.  
  722. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/replaceElement.js
  723. var getAttrs, getClassNames, combineClassNames, toPascalCase, replaceElement;
  724. var init_replaceElement = __esm({
  725. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/replaceElement.js"() {
  726. "use strict";
  727. init_createElement();
  728. getAttrs = (element) => Array.from(element.attributes).reduce((attrs, attr) => {
  729. attrs[attr.name] = attr.value;
  730. return attrs;
  731. }, {});
  732. getClassNames = (attrs) => {
  733. if (typeof attrs === "string")
  734. return attrs;
  735. if (!attrs || !attrs.class)
  736. return "";
  737. if (attrs.class && typeof attrs.class === "string") {
  738. return attrs.class.split(" ");
  739. }
  740. if (attrs.class && Array.isArray(attrs.class)) {
  741. return attrs.class;
  742. }
  743. return "";
  744. };
  745. combineClassNames = (arrayOfClassnames) => {
  746. const classNameArray = arrayOfClassnames.flatMap(getClassNames);
  747. return classNameArray.map((classItem) => classItem.trim()).filter(Boolean).filter((value, index, self) => self.indexOf(value) === index).join(" ");
  748. };
  749. toPascalCase = (string) => string.replace(/(\w)(\w*)(_|-|\s*)/g, (g0, g1, g2) => g1.toUpperCase() + g2.toLowerCase());
  750. replaceElement = (element, { nameAttr, icons, attrs }) => {
  751. const iconName = element.getAttribute(nameAttr);
  752. if (iconName == null)
  753. return;
  754. const ComponentName = toPascalCase(iconName);
  755. const iconNode = icons[ComponentName];
  756. if (!iconNode) {
  757. return console.warn(
  758. `${element.outerHTML} icon name was not found in the provided icons object.`
  759. );
  760. }
  761. const elementAttrs = getAttrs(element);
  762. const [tag, iconAttributes, children] = iconNode;
  763. const iconAttrs = {
  764. ...iconAttributes,
  765. "data-lucide": iconName,
  766. ...attrs,
  767. ...elementAttrs
  768. };
  769. const classNames = combineClassNames(["lucide", `lucide-${iconName}`, elementAttrs, attrs]);
  770. if (classNames) {
  771. Object.assign(iconAttrs, {
  772. class: classNames
  773. });
  774. }
  775. const svgElement = createElement$1([tag, iconAttrs, children]);
  776. return element.parentNode?.replaceChild(svgElement, element);
  777. };
  778. }
  779. });
  780.  
  781. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/defaultAttributes.js
  782. var defaultAttributes;
  783. var init_defaultAttributes = __esm({
  784. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/defaultAttributes.js"() {
  785. "use strict";
  786. defaultAttributes = {
  787. xmlns: "http://www.w3.org/2000/svg",
  788. width: 24,
  789. height: 24,
  790. viewBox: "0 0 24 24",
  791. fill: "none",
  792. stroke: "currentColor",
  793. "stroke-width": 2,
  794. "stroke-linecap": "round",
  795. "stroke-linejoin": "round"
  796. };
  797. }
  798. });
  799.  
  800. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/book-open-check.js
  801. var BookOpenCheck;
  802. var init_book_open_check = __esm({
  803. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/book-open-check.js"() {
  804. "use strict";
  805. init_defaultAttributes();
  806. BookOpenCheck = [
  807. "svg",
  808. defaultAttributes,
  809. [
  810. ["path", { d: "M12 21V7" }],
  811. ["path", { d: "m16 12 2 2 4-4" }],
  812. [
  813. "path",
  814. {
  815. d: "M22 6V4a1 1 0 0 0-1-1h-5a4 4 0 0 0-4 4 4 4 0 0 0-4-4H3a1 1 0 0 0-1 1v13a1 1 0 0 0 1 1h6a3 3 0 0 1 3 3 3 3 0 0 1 3-3h6a1 1 0 0 0 1-1v-1.3"
  816. }
  817. ]
  818. ]
  819. ];
  820. }
  821. });
  822.  
  823. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/chevron-down.js
  824. var ChevronDown;
  825. var init_chevron_down = __esm({
  826. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/chevron-down.js"() {
  827. "use strict";
  828. init_defaultAttributes();
  829. ChevronDown = ["svg", defaultAttributes, [["path", { d: "m6 9 6 6 6-6" }]]];
  830. }
  831. });
  832.  
  833. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/chevrons-up.js
  834. var ChevronsUp;
  835. var init_chevrons_up = __esm({
  836. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/chevrons-up.js"() {
  837. "use strict";
  838. init_defaultAttributes();
  839. ChevronsUp = [
  840. "svg",
  841. defaultAttributes,
  842. [
  843. ["path", { d: "m17 11-5-5-5 5" }],
  844. ["path", { d: "m17 18-5-5-5 5" }]
  845. ]
  846. ];
  847. }
  848. });
  849.  
  850. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/eye-off.js
  851. var EyeOff;
  852. var init_eye_off = __esm({
  853. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/eye-off.js"() {
  854. "use strict";
  855. init_defaultAttributes();
  856. EyeOff = [
  857. "svg",
  858. defaultAttributes,
  859. [
  860. [
  861. "path",
  862. {
  863. d: "M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"
  864. }
  865. ],
  866. ["path", { d: "M14.084 14.158a3 3 0 0 1-4.242-4.242" }],
  867. [
  868. "path",
  869. {
  870. d: "M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"
  871. }
  872. ],
  873. ["path", { d: "m2 2 20 20" }]
  874. ]
  875. ];
  876. }
  877. });
  878.  
  879. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/heart.js
  880. var Heart;
  881. var init_heart = __esm({
  882. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/heart.js"() {
  883. "use strict";
  884. init_defaultAttributes();
  885. Heart = [
  886. "svg",
  887. defaultAttributes,
  888. [
  889. [
  890. "path",
  891. {
  892. d: "M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"
  893. }
  894. ]
  895. ]
  896. ];
  897. }
  898. });
  899.  
  900. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/house.js
  901. var House;
  902. var init_house = __esm({
  903. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/house.js"() {
  904. "use strict";
  905. init_defaultAttributes();
  906. House = [
  907. "svg",
  908. defaultAttributes,
  909. [
  910. ["path", { d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" }],
  911. [
  912. "path",
  913. {
  914. d: "M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"
  915. }
  916. ]
  917. ]
  918. ];
  919. }
  920. });
  921.  
  922. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/message-square-plus.js
  923. var MessageSquarePlus;
  924. var init_message_square_plus = __esm({
  925. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/message-square-plus.js"() {
  926. "use strict";
  927. init_defaultAttributes();
  928. MessageSquarePlus = [
  929. "svg",
  930. defaultAttributes,
  931. [
  932. ["path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }],
  933. ["path", { d: "M12 7v6" }],
  934. ["path", { d: "M9 10h6" }]
  935. ]
  936. ];
  937. }
  938. });
  939.  
  940. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/message-square.js
  941. var MessageSquare;
  942. var init_message_square = __esm({
  943. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/message-square.js"() {
  944. "use strict";
  945. init_defaultAttributes();
  946. MessageSquare = [
  947. "svg",
  948. defaultAttributes,
  949. [["path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }]]
  950. ];
  951. }
  952. });
  953.  
  954. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/moon.js
  955. var Moon;
  956. var init_moon = __esm({
  957. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/moon.js"() {
  958. "use strict";
  959. init_defaultAttributes();
  960. Moon = [
  961. "svg",
  962. defaultAttributes,
  963. [["path", { d: "M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" }]]
  964. ];
  965. }
  966. });
  967.  
  968. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/package-plus.js
  969. var PackagePlus;
  970. var init_package_plus = __esm({
  971. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/package-plus.js"() {
  972. "use strict";
  973. init_defaultAttributes();
  974. PackagePlus = [
  975. "svg",
  976. defaultAttributes,
  977. [
  978. ["path", { d: "M16 16h6" }],
  979. ["path", { d: "M19 13v6" }],
  980. [
  981. "path",
  982. {
  983. d: "M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14"
  984. }
  985. ],
  986. ["path", { d: "m7.5 4.27 9 5.15" }],
  987. ["polyline", { points: "3.29 7 12 12 20.71 7" }],
  988. ["line", { x1: "12", x2: "12", y1: "22", y2: "12" }]
  989. ]
  990. ];
  991. }
  992. });
  993.  
  994. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/panel-right.js
  995. var PanelRight;
  996. var init_panel_right = __esm({
  997. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/panel-right.js"() {
  998. "use strict";
  999. init_defaultAttributes();
  1000. PanelRight = [
  1001. "svg",
  1002. defaultAttributes,
  1003. [
  1004. ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }],
  1005. ["path", { d: "M15 3v18" }]
  1006. ]
  1007. ];
  1008. }
  1009. });
  1010.  
  1011. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/panel-top.js
  1012. var PanelTop;
  1013. var init_panel_top = __esm({
  1014. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/panel-top.js"() {
  1015. "use strict";
  1016. init_defaultAttributes();
  1017. PanelTop = [
  1018. "svg",
  1019. defaultAttributes,
  1020. [
  1021. ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }],
  1022. ["path", { d: "M3 9h18" }]
  1023. ]
  1024. ];
  1025. }
  1026. });
  1027.  
  1028. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/smile.js
  1029. var Smile;
  1030. var init_smile = __esm({
  1031. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/smile.js"() {
  1032. "use strict";
  1033. init_defaultAttributes();
  1034. Smile = [
  1035. "svg",
  1036. defaultAttributes,
  1037. [
  1038. ["circle", { cx: "12", cy: "12", r: "10" }],
  1039. ["path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }],
  1040. ["line", { x1: "9", x2: "9.01", y1: "9", y2: "9" }],
  1041. ["line", { x1: "15", x2: "15.01", y1: "9", y2: "9" }]
  1042. ]
  1043. ];
  1044. }
  1045. });
  1046.  
  1047. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/square-arrow-up-right.js
  1048. var SquareArrowUpRight;
  1049. var init_square_arrow_up_right = __esm({
  1050. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/square-arrow-up-right.js"() {
  1051. "use strict";
  1052. init_defaultAttributes();
  1053. SquareArrowUpRight = [
  1054. "svg",
  1055. defaultAttributes,
  1056. [
  1057. ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }],
  1058. ["path", { d: "M8 8h8v8" }],
  1059. ["path", { d: "m8 16 8-8" }]
  1060. ]
  1061. ];
  1062. }
  1063. });
  1064.  
  1065. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/star.js
  1066. var Star;
  1067. var init_star = __esm({
  1068. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/star.js"() {
  1069. "use strict";
  1070. init_defaultAttributes();
  1071. Star = [
  1072. "svg",
  1073. defaultAttributes,
  1074. [
  1075. [
  1076. "polygon",
  1077. {
  1078. points: "12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
  1079. }
  1080. ]
  1081. ]
  1082. ];
  1083. }
  1084. });
  1085.  
  1086. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/sun.js
  1087. var Sun;
  1088. var init_sun = __esm({
  1089. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/sun.js"() {
  1090. "use strict";
  1091. init_defaultAttributes();
  1092. Sun = [
  1093. "svg",
  1094. defaultAttributes,
  1095. [
  1096. ["circle", { cx: "12", cy: "12", r: "4" }],
  1097. ["path", { d: "M12 2v2" }],
  1098. ["path", { d: "M12 20v2" }],
  1099. ["path", { d: "m4.93 4.93 1.41 1.41" }],
  1100. ["path", { d: "m17.66 17.66 1.41 1.41" }],
  1101. ["path", { d: "M2 12h2" }],
  1102. ["path", { d: "M20 12h2" }],
  1103. ["path", { d: "m6.34 17.66-1.41 1.41" }],
  1104. ["path", { d: "m19.07 4.93-1.41 1.41" }]
  1105. ]
  1106. ];
  1107. }
  1108. });
  1109.  
  1110. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/twitter.js
  1111. var Twitter;
  1112. var init_twitter = __esm({
  1113. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/twitter.js"() {
  1114. "use strict";
  1115. init_defaultAttributes();
  1116. Twitter = [
  1117. "svg",
  1118. defaultAttributes,
  1119. [
  1120. [
  1121. "path",
  1122. {
  1123. d: "M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"
  1124. }
  1125. ]
  1126. ]
  1127. ];
  1128. }
  1129. });
  1130.  
  1131. // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/lucide.js
  1132. var createIcons;
  1133. var init_lucide = __esm({
  1134. "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/lucide.js"() {
  1135. "use strict";
  1136. init_replaceElement();
  1137. init_createElement();
  1138. init_book_open_check();
  1139. init_chevron_down();
  1140. init_chevrons_up();
  1141. init_eye_off();
  1142. init_heart();
  1143. init_house();
  1144. init_message_square_plus();
  1145. init_message_square();
  1146. init_moon();
  1147. init_package_plus();
  1148. init_panel_right();
  1149. init_panel_top();
  1150. init_smile();
  1151. init_square_arrow_up_right();
  1152. init_star();
  1153. init_sun();
  1154. init_twitter();
  1155. createIcons = ({ icons = {}, nameAttr = "data-lucide", attrs = {} } = {}) => {
  1156. if (!Object.values(icons).length) {
  1157. throw new Error(
  1158. "Please provide an icons object.\nIf you want to use all the icons you can import it like:\n `import { createIcons, icons } from 'lucide';\nlucide.createIcons({icons});`"
  1159. );
  1160. }
  1161. if (typeof document === "undefined") {
  1162. throw new Error("`createIcons()` only works in a browser environment.");
  1163. }
  1164. const elementsToReplace = document.querySelectorAll(`[${nameAttr}]`);
  1165. Array.from(elementsToReplace).forEach(
  1166. (element) => replaceElement(element, { nameAttr, icons, attrs })
  1167. );
  1168. if (nameAttr === "data-lucide") {
  1169. const deprecatedElements = document.querySelectorAll("[icon-name]");
  1170. if (deprecatedElements.length > 0) {
  1171. console.warn(
  1172. "[Lucide] Some icons were found with the now deprecated icon-name attribute. These will still be replaced for backwards compatibility, but will no longer be supported in v1.0 and you should switch to data-lucide"
  1173. );
  1174. Array.from(deprecatedElements).forEach(
  1175. (element) => replaceElement(element, { nameAttr: "icon-name", icons, attrs })
  1176. );
  1177. }
  1178. }
  1179. };
  1180. }
  1181. });
  1182.  
  1183. // src/components/toast.ts
  1184. function createToast(props) {
  1185. const { message, duration = 3e3 } = props;
  1186. const $existTosat = $(".v2p-toast");
  1187. if ($existTosat.length > 0) {
  1188. $existTosat.remove();
  1189. }
  1190. const $toast = $(`<div class="v2p-toast">${message}</div>`).hide();
  1191. $body.append($toast);
  1192. $toast.fadeIn("fast");
  1193. if (duration !== 0) {
  1194. setTimeout(() => {
  1195. $toast.fadeOut("fast", () => {
  1196. $toast.remove();
  1197. });
  1198. }, duration);
  1199. }
  1200. return {
  1201. clear() {
  1202. $toast.remove();
  1203. }
  1204. };
  1205. }
  1206. var init_toast = __esm({
  1207. "src/components/toast.ts"() {
  1208. "use strict";
  1209. init_globals();
  1210. }
  1211. });
  1212.  
  1213. // src/contents/helpers.ts
  1214. function focusReplyInput() {
  1215. if (replyTextArea instanceof HTMLTextAreaElement) {
  1216. replyTextArea.focus();
  1217. }
  1218. }
  1219. function insertTextToReplyInput(text) {
  1220. if (replyTextArea instanceof HTMLTextAreaElement) {
  1221. const startPos = replyTextArea.selectionStart;
  1222. const endPos = replyTextArea.selectionEnd;
  1223. const valueToStart = replyTextArea.value.substring(0, startPos);
  1224. const valueFromEnd = replyTextArea.value.substring(endPos, replyTextArea.value.length);
  1225. replyTextArea.value = `${valueToStart}${text}${valueFromEnd}`;
  1226. focusReplyInput();
  1227. replyTextArea.selectionStart = replyTextArea.selectionEnd = startPos + text.length;
  1228. }
  1229. }
  1230. async function setMemberTags(params) {
  1231. const { memberName, memberAvatar, tags } = params;
  1232. const storage = await getStorage(false);
  1233. const tagData = storage["member-tag" /* MemberTag */];
  1234. if (!isBrowserExtension()) {
  1235. return;
  1236. }
  1237. if (tags && tags.length > 0) {
  1238. const newTagData = {
  1239. ...tagData,
  1240. [memberName]: { tags, avatar: memberAvatar || tagData?.[memberName]?.avatar }
  1241. };
  1242. await setStorage("member-tag" /* MemberTag */, newTagData);
  1243. } else {
  1244. if (tagData && Reflect.has(tagData, memberName)) {
  1245. delete tagData[memberName];
  1246. await setStorage("member-tag" /* MemberTag */, tagData);
  1247. }
  1248. }
  1249. }
  1250. async function addToReadingList(params) {
  1251. const { url, title, content } = params;
  1252. if (!(typeof url === "string" || typeof title === "string" || typeof content === "string")) {
  1253. const message = "\u65E0\u6CD5\u8BC6\u522B\u5C06\u8BE5\u4E3B\u9898\u7684\u5143\u6570\u636E";
  1254. createToast({ message });
  1255. throw new Error(message);
  1256. }
  1257. const storage = await getStorage();
  1258. const currentData = storage["reading-list" /* ReadingList */]?.data || [];
  1259. const exist = currentData.findIndex((it) => it.url === url) !== -1;
  1260. if (exist) {
  1261. createToast({ message: "\u8BE5\u4E3B\u9898\u5DF2\u5B58\u5728\u4E8E\u7A0D\u540E\u9605\u8BFB" });
  1262. } else {
  1263. if (window.__V2P_AddingReading !== true) {
  1264. window.__V2P_AddingReading = true;
  1265. try {
  1266. await setStorage("reading-list" /* ReadingList */, {
  1267. data: [
  1268. {
  1269. url,
  1270. title: title.replace(" - V2EX", ""),
  1271. content: content.length > READING_CONTENT_LIMIT ? content.substring(0, READING_CONTENT_LIMIT) + "..." : content,
  1272. addedTime: Date.now()
  1273. },
  1274. ...currentData
  1275. ]
  1276. });
  1277. createToast({ message: "\u2705 \u5DF2\u6DFB\u52A0\u8FDB\u7A0D\u540E\u9605\u8BFB" });
  1278. await sleep(500);
  1279. } finally {
  1280. window.__V2P_AddingReading = false;
  1281. }
  1282. }
  1283. }
  1284. }
  1285. function customEscape(str) {
  1286. return str.replace(
  1287. /[^a-zA-Z0-9_.!~*'()-]/g,
  1288. (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`
  1289. );
  1290. }
  1291. function decodeBase64TopicPage() {
  1292. const dataTitle = "\u70B9\u51FB\u590D\u5236";
  1293. if (window.__V2P_DecodeStatus === "decodeed") {
  1294. createToast({ message: "\u5DF2\u89E3\u6790\u5B8C\u672C\u9875\u6240\u6709\u7684 Base64 \u5B57\u7B26\u4E32" });
  1295. } else {
  1296. const $topicContentBox2 = $("#Main .box:has(.topic_content)");
  1297. const $commentBox2 = $('#Main .box:has(.cell[id^="r_"])');
  1298. const $commentCells2 = $commentBox2.find('.cell[id^="r_"]');
  1299. let count = 0;
  1300. const excludeList = [
  1301. "boss",
  1302. "bilibili",
  1303. "Bilibili",
  1304. "Encrypto",
  1305. "encrypto",
  1306. "Window10",
  1307. "airpords",
  1308. "Windows7"
  1309. ];
  1310. const convertHTMLText = (text, excludeTextList) => {
  1311. if (text.length % 4 !== 0 || text.length <= 8) {
  1312. return text;
  1313. }
  1314. if (excludeList.includes(text)) {
  1315. return text;
  1316. }
  1317. if (text.includes("=")) {
  1318. const paddingIndex = text.indexOf("=");
  1319. if (paddingIndex !== text.length - 1 && paddingIndex !== text.length - 2) {
  1320. return text;
  1321. }
  1322. }
  1323. if (excludeTextList?.some((excludeText) => excludeText.includes(text))) {
  1324. return text;
  1325. }
  1326. try {
  1327. const decodedStr = decodeURIComponent(customEscape(window.atob(text)));
  1328. count += 1;
  1329. return `${text}<span class="v2p-decode-block">(<ins class="v2p-decode" data-title="${dataTitle}">${decodedStr}</ins>)</span>`;
  1330. } catch (err) {
  1331. if (err instanceof Error) {
  1332. console.error(`\u89E3\u6790 Base64 \u51FA\u9519\uFF1A${err.message}`);
  1333. }
  1334. return text;
  1335. }
  1336. };
  1337. const base64regex = /[A-z0-9+/=]+/g;
  1338. const contentHandler = (_, content) => {
  1339. const excludeTextList = [
  1340. ...content.getElementsByTagName("a"),
  1341. ...content.getElementsByTagName("img")
  1342. ].map((ele) => ele.outerHTML);
  1343. content.innerHTML = content.innerHTML.replace(
  1344. base64regex,
  1345. (htmlText) => convertHTMLText(htmlText, excludeTextList)
  1346. );
  1347. };
  1348. $commentCells2.find(".reply_content").each(contentHandler);
  1349. $topicContentBox2.find(".topic_content").each(contentHandler);
  1350. if (count === 0) {
  1351. createToast({ message: "\u672C\u9875\u672A\u53D1\u73B0 Base64 \u5B57\u7B26\u4E32" });
  1352. } else {
  1353. window.__V2P_DecodeStatus = "decodeed";
  1354. createToast({ message: `\u2705 \u5DF2\u89E3\u6790\u672C\u9875\u6240\u6709\u7684 Base64 \u5B57\u7B26\u4E32\uFF0C\u5171 ${count} \u6761` });
  1355. }
  1356. $(".v2p-decode").on("click", (ev) => {
  1357. const text = ev.target.innerText;
  1358. void navigator.clipboard.writeText(text).then(() => {
  1359. ev.target.dataset.title = "\u2705 \u5DF2\u590D\u5236";
  1360. setTimeout(() => {
  1361. ev.target.dataset.title = dataTitle;
  1362. }, 1e3);
  1363. });
  1364. });
  1365. }
  1366. }
  1367. function postTask(expression, callback) {
  1368. if (!isBrowserExtension()) {
  1369. const result = Function(`"use strict"; ${expression}`)();
  1370. callback?.(result);
  1371. } else {
  1372. if (callback) {
  1373. if (window.__V2P_Tasks) {
  1374. window.__V2P_Tasks.set(Date.now(), callback);
  1375. } else {
  1376. window.__V2P_Tasks = /* @__PURE__ */ new Map([[Date.now(), callback]]);
  1377. }
  1378. }
  1379. const messageData = {
  1380. from: 0 /* Content */,
  1381. payload: { task: { id: Date.now(), expression } }
  1382. };
  1383. window.postMessage(messageData);
  1384. }
  1385. }
  1386. function loadIcons() {
  1387. setTimeout(() => {
  1388. createIcons({
  1389. attrs: {
  1390. width: "100%",
  1391. height: "100%"
  1392. },
  1393. icons: {
  1394. MessageSquarePlus,
  1395. MessageSquare,
  1396. BookOpenCheck,
  1397. ChevronsUp,
  1398. Heart,
  1399. EyeOff,
  1400. Sun,
  1401. Moon,
  1402. Smile,
  1403. PackagePlus,
  1404. Star,
  1405. Twitter,
  1406. ChevronDown,
  1407. ArrowUpRightSquare: SquareArrowUpRight,
  1408. House
  1409. }
  1410. });
  1411. }, 0);
  1412. }
  1413. function transformEmoji(textValue) {
  1414. return textValue.replace(/\[[^\]]+\]/g, (x) => {
  1415. if (Object.hasOwn(emojiLinks, x)) {
  1416. const emojiLink = emojiLinks[x].ld;
  1417. if (typeof emojiLink === "string") {
  1418. return `${emojiLink} `;
  1419. }
  1420. }
  1421. return x;
  1422. });
  1423. }
  1424. function getTagsText(tags) {
  1425. return tags.map((it) => it.name).join("\uFF0C");
  1426. }
  1427. function setTheme(type) {
  1428. $body.get(0)?.classList.forEach((cls) => {
  1429. if (cls.startsWith("v2p-theme-")) {
  1430. $body.removeClass(cls);
  1431. }
  1432. });
  1433. $body.addClass(`v2p-theme-${type}`);
  1434. }
  1435. function toggleTheme({
  1436. $toggle,
  1437. prefersDark,
  1438. themeType = "light-default"
  1439. }) {
  1440. const isPageDark = $wrapper.hasClass("Night");
  1441. const shouldSync = prefersDark !== isPageDark;
  1442. if (shouldSync) {
  1443. const toggleThemeUrl = $toggle.attr("href");
  1444. if (typeof toggleThemeUrl === "string") {
  1445. fetch(toggleThemeUrl);
  1446. }
  1447. if (prefersDark) {
  1448. $toggle.prop("title", "\u4F7F\u7528\u6D45\u8272\u4E3B\u9898");
  1449. $toggle.html('<i data-lucide="sun"></i>');
  1450. } else {
  1451. $toggle.prop("title", "\u4F7F\u7528\u6DF1\u8272\u4E3B\u9898");
  1452. $toggle.html('<i data-lucide="moon"></i>');
  1453. }
  1454. }
  1455. if (prefersDark) {
  1456. setTheme("dark-default");
  1457. $wrapper.addClass("Night");
  1458. } else {
  1459. setTheme(themeType);
  1460. $wrapper.removeClass("Night");
  1461. }
  1462. loadIcons();
  1463. }
  1464. function replaceEmojiWithHD($emojiImgs) {
  1465. if ($emojiImgs.length > 0) {
  1466. const srcMap = /* @__PURE__ */ new Map();
  1467. Object.values(emojiLinks).forEach(({ ld, hd }) => {
  1468. srcMap.set(ld, hd);
  1469. });
  1470. $emojiImgs.each((_, img) => {
  1471. const $img = $(img);
  1472. const src = $img.attr("src");
  1473. if (typeof src === "string") {
  1474. const hd = srcMap.get(src);
  1475. if (typeof hd === "string") {
  1476. $img.attr("src", hd);
  1477. $img.css({
  1478. width: "var(--v2p-emoji-size)",
  1479. height: "var(--v2p-emoji-size)"
  1480. });
  1481. }
  1482. }
  1483. });
  1484. }
  1485. }
  1486. function replaceCommentEmojiWithHD($cells = $commentCells) {
  1487. const srcMap = /* @__PURE__ */ new Map();
  1488. Object.values(emojiLinks).forEach(({ ld, hd }) => {
  1489. srcMap.set(ld, hd);
  1490. });
  1491. const $embedImages = $cells.find(`.reply_content .embedded_image, .payload .embedded_image`);
  1492. if ($embedImages.length > 0) {
  1493. replaceEmojiWithHD($embedImages);
  1494. }
  1495. }
  1496. function openV2PSettings() {
  1497. chrome.runtime.sendMessage({ ["showOptions" /* showOptions */]: true });
  1498. }
  1499. function getRegisterDays(created) {
  1500. const registerDays = Math.ceil((Date.now() / 1e3 - created) / (60 * 60 * 24));
  1501. return registerDays;
  1502. }
  1503. var init_helpers = __esm({
  1504. "src/contents/helpers.ts"() {
  1505. "use strict";
  1506. init_lucide();
  1507. init_toast();
  1508. init_constants();
  1509. init_utils();
  1510. init_globals();
  1511. }
  1512. });
  1513.  
  1514. // src/contents/common.ts
  1515. var common_exports = {};
  1516. var init_common = __esm({
  1517. "src/contents/common.ts"() {
  1518. "use strict";
  1519. init_polyfill();
  1520. init_constants();
  1521. init_icons();
  1522. init_utils();
  1523. init_globals();
  1524. init_helpers();
  1525. if ($("#site-header").length > 0) {
  1526. $body.addClass("v2p-mobile");
  1527. }
  1528. void (async () => {
  1529. const isBrowserExt = isBrowserExtension();
  1530. const storage = await getStorage();
  1531. const options = storage["options" /* Options */];
  1532. if (options.theme.mode === "compact") {
  1533. $body.addClass("v2p-mode-compact");
  1534. }
  1535. const $toggle = $("#Rightbar .light-toggle").addClass("v2p-color-mode-toggle");
  1536. const colorMedia = window.matchMedia("(prefers-color-scheme: dark)");
  1537. const handleColorSchemeChange = ({ matches }) => {
  1538. getStorage(false).then((storage2) => {
  1539. const options2 = storage2["options" /* Options */];
  1540. const themeType2 = options2.theme.type || "light-default";
  1541. toggleTheme({
  1542. $toggle,
  1543. prefersDark: matches,
  1544. themeType: matches ? "dark-default" : themeType2
  1545. });
  1546. });
  1547. };
  1548. const themeType = options.theme.type || "light-default";
  1549. if (options.theme.autoSwitch) {
  1550. const prefersDark = colorMedia.matches;
  1551. toggleTheme({
  1552. $toggle,
  1553. prefersDark,
  1554. themeType: prefersDark ? "dark-default" : themeType
  1555. });
  1556. colorMedia.addEventListener("change", handleColorSchemeChange);
  1557. } else {
  1558. const prefersDark = isBrowserExt ? themeType === "dark-default" : $wrapper.hasClass("Night");
  1559. toggleTheme({ $toggle, prefersDark, themeType });
  1560. }
  1561. $toggle.on("click", () => {
  1562. const wrapperDark = $wrapper.hasClass("Night");
  1563. const newTheme = {
  1564. type: wrapperDark ? "light-default" : "dark-default",
  1565. autoSwitch: false
  1566. // 当用户主动设置颜色主题后,取消自动跟随系统。
  1567. };
  1568. void setStorage("options" /* Options */, deepMerge(options, { theme: newTheme }));
  1569. });
  1570. const syncInfo = storage["settings-sync" /* SyncInfo */];
  1571. if (syncInfo) {
  1572. const lastCheckTime = syncInfo.lastCheckTime;
  1573. const twoHours = 2 * 60 * 1e3 * 60;
  1574. const neverChecked = !lastCheckTime;
  1575. if (lastCheckTime && Date.now() - lastCheckTime >= twoHours || neverChecked) {
  1576. const isSignInPage = window.location.href.includes("/signin");
  1577. if (!isSignInPage) {
  1578. void getV2P_Settings().then(async (res) => {
  1579. const settings = res?.config;
  1580. const remoteSyncInfo = settings?.["settings-sync" /* SyncInfo */];
  1581. if (settings && remoteSyncInfo) {
  1582. if (syncInfo.version < remoteSyncInfo.version || neverChecked) {
  1583. await chrome.storage.sync.set(
  1584. deepMerge(storage, {
  1585. ...settings,
  1586. ["settings-sync" /* SyncInfo */]: {
  1587. ...settings["settings-sync" /* SyncInfo */],
  1588. lastCheckTime: Date.now()
  1589. }
  1590. })
  1591. );
  1592. }
  1593. }
  1594. });
  1595. }
  1596. }
  1597. }
  1598. {
  1599. const $toggleImg = $toggle.find("> img");
  1600. const alt = $toggleImg.prop("alt");
  1601. if (alt === "Light") {
  1602. $toggle.prop("title", "\u4F7F\u7528\u6DF1\u8272\u4E3B\u9898");
  1603. $toggleImg.replaceWith('<i data-lucide="moon"></i>');
  1604. } else if (alt === "Dark") {
  1605. $toggle.prop("title", "\u4F7F\u7528\u6D45\u8272\u4E3B\u9898");
  1606. $toggleImg.replaceWith('<i data-lucide="sun"></i>');
  1607. }
  1608. }
  1609. {
  1610. $("#Top .site-nav .tools > .top").addClass("v2p-hover-btn");
  1611. }
  1612. if (options.hideAccount) {
  1613. const faviconLink = $("link[rel~='icon']");
  1614. faviconLink.prop("href", "https://v2p.app/favicon.svg");
  1615. $("#Logo").append(`<i data-lucide="house"></i>`).addClass("v2p-logo");
  1616. $("#Top").find('a[href^="/member/"]').remove();
  1617. $infoCard.find('a[href^="/member/"], table:nth-of-type(1) td:nth-of-type(3) .fade').addClass("v2p-hide-account");
  1618. $infoCard.find(".balance_area").addClass("v2p-hide-balance");
  1619. }
  1620. if (isBrowserExt) {
  1621. injectScript(chrome.runtime.getURL("scripts/web_accessible_resources.min.js"));
  1622. window.addEventListener("message", (ev) => {
  1623. if (ev.data.from === 1 /* Web */) {
  1624. const payload = ev.data.payload;
  1625. const task = payload?.task;
  1626. if (payload?.status === "ready") {
  1627. postTask('if (typeof window.once === "string") { return window.once; }', (result) => {
  1628. if (typeof result === "string") {
  1629. window.once = result;
  1630. }
  1631. });
  1632. }
  1633. if (task) {
  1634. window.__V2P_Tasks?.get(task.id)?.(task.result);
  1635. }
  1636. }
  1637. });
  1638. }
  1639. {
  1640. const $extraFooter = $(`
  1641. <div class="v2p-footer">
  1642. <div class="v2p-footer-text">\u6269\u5C55\u81EA V2EX Polish </div>
  1643.  
  1644. <div class="v2p-footer-links">
  1645. <a class="v2p-footer-link v2p-hover-btn" href="${"https://v2p.app" /* Home */}" target="_blank">\u63D2\u4EF6\u5B98\u7F51</a>
  1646. <a class="v2p-footer-link v2p-hover-btn" href="${"https://github.com/coolpace/V2EX_Polish/discussions/1?sort=new" /* Feedback */}" target="_blank">\u95EE\u9898\u53CD\u9988</a>
  1647. <a class="v2p-footer-link v2p-hover-btn" href="${"https://v2p.app/support" /* Support */}" target="_blank">\u8D5E\u8D4F\u652F\u6301</a>
  1648. <a class="v2p-footer-link v2p-hover-btn v2p-optbtn" href="javascript:void(0);">\u9009\u9879\u8BBE\u7F6E</a>
  1649. </div>
  1650.  
  1651. <div class="v2p-footer-brand">
  1652. <a
  1653. href="https://github.com/coolpace/V2EX_Polish"
  1654. target="_blank"
  1655. title="GitHub \u4ED3\u5E93"
  1656. class="v2p-hover-btn v2p-github-ref"
  1657. >
  1658. ${iconGitHub}
  1659. </a>
  1660. </div>
  1661. </div>
  1662. `);
  1663. $extraFooter.find(".v2p-optbtn").on("click", () => {
  1664. openV2PSettings();
  1665. });
  1666. $(`<div class="v2p-footer-logo">${iconLogo}</div>`).prependTo($extraFooter);
  1667. $("#Bottom .content").append($extraFooter);
  1668. }
  1669. chrome.storage.onChanged.addListener((changes, storageType) => {
  1670. if (storageType === "sync") {
  1671. const options2 = changes["options" /* Options */];
  1672. if (options2) {
  1673. const { newValue, oldValue } = options2;
  1674. const newOptions = newValue;
  1675. const oldOptions = oldValue;
  1676. const newTheme = newOptions?.theme;
  1677. const oldTheme = oldOptions?.theme;
  1678. if (newTheme && oldTheme) {
  1679. const prefersDark = newTheme.autoSwitch ? window.matchMedia("(prefers-color-scheme: dark)").matches : isBrowserExt ? newTheme.type === "dark-default" : $wrapper.hasClass("Night");
  1680. if (newTheme.type !== oldTheme.type) {
  1681. toggleTheme({
  1682. $toggle,
  1683. prefersDark,
  1684. themeType: newTheme.type
  1685. });
  1686. } else {
  1687. if (newTheme.autoSwitch !== oldTheme.autoSwitch) {
  1688. if (newTheme.autoSwitch) {
  1689. colorMedia.addEventListener("change", handleColorSchemeChange);
  1690. } else {
  1691. colorMedia.removeEventListener("change", handleColorSchemeChange);
  1692. }
  1693. toggleTheme({
  1694. $toggle,
  1695. prefersDark,
  1696. themeType: prefersDark ? "dark-default" : newTheme.type
  1697. });
  1698. }
  1699. }
  1700. if (newTheme.mode !== oldTheme.mode) {
  1701. if (newTheme.mode === "compact") {
  1702. $body.addClass("v2p-mode-compact");
  1703. } else {
  1704. $body.removeClass("v2p-mode-compact");
  1705. }
  1706. }
  1707. }
  1708. }
  1709. }
  1710. });
  1711. })();
  1712. }
  1713. });
  1714.  
  1715. // src/services.ts
  1716. async function legacyRequest(url, options) {
  1717. const res = await fetch(url, options);
  1718. if (res.ok) {
  1719. return res.json();
  1720. }
  1721. throw new Error("\u8C03\u7528 V2EX API v1 \u51FA\u9519", { cause: res.status });
  1722. }
  1723. async function fetchUserInfo(memberName, options) {
  1724. try {
  1725. const member = await legacyRequest(
  1726. `${V2EX_LEGACY_API}/members/show.json?username=${memberName}`,
  1727. options
  1728. );
  1729. return member;
  1730. } catch (err) {
  1731. if (err instanceof Error) {
  1732. if (err.name === "AbortError") {
  1733. throw new Error("\u8BF7\u6C42\u88AB\u53D6\u6D88");
  1734. } else if (err.cause === 404) {
  1735. throw new Error("\u67E5\u65E0\u6B64\u7528\u6237\uFF0C\u7591\u4F3C\u5DF2\u88AB\u5C01\u7981", { cause: err.cause });
  1736. }
  1737. }
  1738. throw new Error("\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25");
  1739. }
  1740. }
  1741. async function request(url, options) {
  1742. const storage = await getStorage();
  1743. const PAT = storage["api" /* API */]?.pat;
  1744. const res = await fetch(url, {
  1745. ...options,
  1746. headers: { Authorization: PAT ? `Bearer ${PAT}` : "", ...options?.headers }
  1747. });
  1748. {
  1749. const limit = res.headers.get("X-Rate-Limit-Limit");
  1750. const reset = res.headers.get("X-Rate-Limit-Reset");
  1751. const remaining = res.headers.get("X-Rate-Limit-Remaining");
  1752. const api = {
  1753. pat: PAT,
  1754. limit: limit ? Number(limit) : void 0,
  1755. reset: reset ? Number(reset) : void 0,
  1756. remaining: remaining ? Number(remaining) : void 0
  1757. };
  1758. void setStorage("api" /* API */, api);
  1759. }
  1760. const resultData = await res.json();
  1761. if (typeof resultData.success === "boolean" && !resultData.success) {
  1762. throw new Error(resultData.message, { cause: resultData });
  1763. }
  1764. return resultData;
  1765. }
  1766. function fetchTopic(topicId, options) {
  1767. return request(`${V2EX_API}/topics/${topicId}`, { method: "GET", ...options });
  1768. }
  1769. async function uploadImage(file) {
  1770. const formData = new FormData();
  1771. formData.append("image", file);
  1772. const randomIndex = Math.floor(Math.random() * imgurClientIdPool.length);
  1773. const clidenId = imgurClientIdPool[randomIndex];
  1774. const res = await fetch("https://api.imgur.com/3/upload", {
  1775. method: "POST",
  1776. headers: { Authorization: `Client-ID ${clidenId}` },
  1777. body: formData
  1778. });
  1779. if (res.ok) {
  1780. const resData = await res.json();
  1781. if (resData.success) {
  1782. return resData.data.link;
  1783. }
  1784. }
  1785. throw new Error("\u4E0A\u4F20\u5931\u8D25");
  1786. }
  1787. async function refreshMoney() {
  1788. const res = await fetch("/ajax/money", { method: "POST" });
  1789. const data = await res.text();
  1790. $("#money").html(data);
  1791. }
  1792. async function thankReply(params) {
  1793. try {
  1794. const res = await fetch(`/thank/reply/${params.replyId}?once=${window.once}`, {
  1795. method: "POST"
  1796. });
  1797. const data = await res.json();
  1798. postTask(`window.once = ${data.once}`);
  1799. window.once = data.once;
  1800. if (data.success) {
  1801. $("#thank_area_" + params.replyId).addClass("thanked").html("\u611F\u8C22\u5DF2\u53D1\u9001");
  1802. params.onSuccess?.();
  1803. await refreshMoney();
  1804. } else {
  1805. alert(data.message);
  1806. }
  1807. } catch {
  1808. params.onFail?.();
  1809. }
  1810. }
  1811. async function crawlTopicPage(path, page = "1") {
  1812. const res = await fetch(`${V2EX_ORIGIN}${path}?p=${page}`);
  1813. const htmlText = await res.text();
  1814. return htmlText;
  1815. }
  1816. async function getCommentPreview(params) {
  1817. const formData = new FormData();
  1818. formData.append("text", params.text);
  1819. const res = await fetch(`${V2EX_ORIGIN}/preview/${params.syntax}`, {
  1820. method: "POST",
  1821. body: formData
  1822. });
  1823. if (res.ok) {
  1824. const renderedContent = await res.text();
  1825. return renderedContent;
  1826. } else {
  1827. throw new Error("\u9884\u89C8\u5931\u8D25");
  1828. }
  1829. }
  1830. var V2EX_LEGACY_API, V2EX_API;
  1831. var init_services = __esm({
  1832. "src/services.ts"() {
  1833. "use strict";
  1834. init_constants();
  1835. init_helpers();
  1836. init_utils();
  1837. V2EX_LEGACY_API = `${V2EX_ORIGIN}/api`;
  1838. V2EX_API = `${V2EX_ORIGIN}/api/v2`;
  1839. }
  1840. });
  1841.  
  1842. // src/components/button.ts
  1843. function createButton(props) {
  1844. const { children, className = "", type = "button", tag = "button" } = props;
  1845. const $button = $(`<${tag} class="normal button ${className}">${children}</${tag}>`);
  1846. if (tag === "button") {
  1847. $button.prop("type", type);
  1848. }
  1849. return $button;
  1850. }
  1851. var init_button = __esm({
  1852. "src/components/button.ts"() {
  1853. "use strict";
  1854. }
  1855. });
  1856.  
  1857. // src/components/modal.ts
  1858. function createModal(props) {
  1859. const { root, title, onMount, onOpen, onClose } = props;
  1860. const $mask = $('<div class="v2p-modal-mask">');
  1861. const $content = $('<div class="v2p-modal-content">');
  1862. const $closeBtn = createButton({
  1863. children: "\u5173\u95ED<kbd>Esc</kbd>",
  1864. className: "v2p-modal-close-btn"
  1865. });
  1866. const $title = $(`<div class="v2p-modal-title">${title ?? ""}</div>`);
  1867. const $actions = $('<div class="v2p-modal-actions">').append($closeBtn);
  1868. const $header = $('<div class="v2p-modal-header">').append($title, $actions);
  1869. const $main2 = $('<div class="v2p-modal-main">').append($header, $content).on("click", (ev) => {
  1870. ev.stopPropagation();
  1871. });
  1872. const $container = $mask.append($main2).hide();
  1873. const modalElements = {
  1874. $mask,
  1875. $main: $main2,
  1876. $header,
  1877. $container,
  1878. $title,
  1879. $actions,
  1880. $content
  1881. };
  1882. let boundEvent = false;
  1883. let mouseDownTarget;
  1884. const mouseDownHandler = (ev) => {
  1885. mouseDownTarget = ev.target;
  1886. };
  1887. const mouseUpHandler = (ev) => {
  1888. if (mouseDownTarget === $mask.get(0) && ev.target === $mask.get(0) && ev.currentTarget === ev.target) {
  1889. handleModalClose();
  1890. }
  1891. };
  1892. const keyupHandler = (ev) => {
  1893. if (ev.key === "Escape") {
  1894. handleModalClose();
  1895. }
  1896. };
  1897. const handleModalClose = () => {
  1898. $mask.off("mousedown", mouseDownHandler);
  1899. $mask.off("mouseup", mouseUpHandler);
  1900. $(document).off("keydown", keyupHandler);
  1901. boundEvent = false;
  1902. $container.fadeOut("fast");
  1903. document.body.classList.remove("v2p-modal-open");
  1904. onClose?.(modalElements);
  1905. };
  1906. const handleModalOpen = () => {
  1907. if (root && !$container.parent().length) {
  1908. root.append($container);
  1909. $closeBtn.on("click", handleModalClose);
  1910. onMount?.(modalElements);
  1911. }
  1912. setTimeout(() => {
  1913. if (!boundEvent) {
  1914. $mask.on("mousedown", mouseDownHandler);
  1915. $mask.on("mouseup", mouseUpHandler);
  1916. $(document).on("keydown", keyupHandler);
  1917. boundEvent = true;
  1918. }
  1919. });
  1920. $container.fadeIn("fast");
  1921. document.body.classList.add("v2p-modal-open");
  1922. onOpen?.(modalElements);
  1923. };
  1924. return { ...modalElements, open: handleModalOpen, close: handleModalClose };
  1925. }
  1926. var init_modal = __esm({
  1927. "src/components/modal.ts"() {
  1928. "use strict";
  1929. init_button();
  1930. }
  1931. });
  1932.  
  1933. // src/contents/topic/content.ts
  1934. function handleTopicImgHeight() {
  1935. const $topicContentImgs = $topicContentBox.find(".topic_content .embedded_image");
  1936. $topicContentImgs.each((_, img) => {
  1937. const $img = $(img);
  1938. const height = $img.height() ?? 0;
  1939. const shouldWrap = height > 600;
  1940. if (shouldWrap) {
  1941. const collapsedCSS = {
  1942. maxHeight: `${READABLE_CONTENT_HEIGHT}px`,
  1943. overflow: "hidden",
  1944. paddingBottom: "0",
  1945. "--bg-reply": "var(--v2p-color-bg-content)"
  1946. };
  1947. const $contentBox = $('<div class="v2p-reply-content v2p-collapsed">').css(collapsedCSS);
  1948. const $expandBtn = createButton({ children: "\u5C55\u5F00\u56FE\u7247", className: "v2p-expand-btn" });
  1949. const toggleContent = () => {
  1950. const collapsed = $contentBox.hasClass("v2p-collapsed");
  1951. $contentBox.toggleClass("v2p-collapsed").css(
  1952. collapsed ? { maxHeight: "none", overflow: "auto", paddingBottom: "40px" } : collapsedCSS
  1953. );
  1954. $expandBtn.html(collapsed ? "\u6536\u8D77\u56FE\u7247" : "\u5C55\u5F00\u56FE\u7247");
  1955. };
  1956. $expandBtn.on("click", () => {
  1957. toggleContent();
  1958. });
  1959. $contentBox.append($img.clone()).replaceAll($img).append($expandBtn);
  1960. }
  1961. });
  1962. }
  1963. function handleContent() {
  1964. const storage = getStorageSync();
  1965. const options = storage["options" /* Options */];
  1966. if (options.openInNewTab) {
  1967. $topicContentBox.find(".topic_content a[href]").prop("target", "_blank").prop("rel", "noopener noreferrer");
  1968. }
  1969. {
  1970. const $topicContents = $topicContentBox.find(".subtle > .topic_content");
  1971. const textLength = $topicContents.text().length;
  1972. if (textLength >= 200) {
  1973. $topicContents.each((_, topicContent) => {
  1974. if (textLength >= 400) {
  1975. topicContent.style.fontSize = "14px";
  1976. }
  1977. topicContent.style.fontSize = "14.5px";
  1978. });
  1979. }
  1980. }
  1981. {
  1982. const $topicBtns = $(".topic_buttons");
  1983. const $topicBtn = $topicBtns.find(".tb").addClass("v2p-tb v2p-hover-btn");
  1984. const $favoriteBtn = $topicBtn.eq(0);
  1985. $favoriteBtn.append('<span class="v2p-tb-icon"><i data-lucide="star"></i></span>');
  1986. $topicBtn.eq(1).append('<span class="v2p-tb-icon"><i data-lucide="twitter"></i></span>');
  1987. $topicBtn.eq(2).append('<span class="v2p-tb-icon"><i data-lucide="eye-off"></i></span>');
  1988. $topicBtn.eq(3).append('<span class="v2p-tb-icon"><i data-lucide="heart"></i></span>');
  1989. if (pathTopicId) {
  1990. $topicBtns.append(
  1991. ` &nbsp;<a href="${"https://v2p.app/share" /* Share */}/${pathTopicId}" target="_blank" class="tb v2p-tb v2p-hover-btn">\u5206\u4EAB<span class="v2p-tb-icon"><i data-lucide="arrow-up-right-square"></i></span></a>`
  1992. );
  1993. }
  1994. loadIcons();
  1995. }
  1996. window.requestIdleCallback(() => {
  1997. handleTopicImgHeight();
  1998. });
  1999. }
  2000. function processReplyContent($cellDom) {
  2001. if ($cellDom.find(".v2p-reply-content").length > 0) {
  2002. return;
  2003. }
  2004. const $replyContent = $cellDom.find(".reply_content");
  2005. const contentHeight = $replyContent.height() ?? 0;
  2006. const shouldCollapsed = contentHeight + READABLE_CONTENT_HEIGHT >= MAX_CONTENT_HEIGHT;
  2007. if (shouldCollapsed) {
  2008. const collapsedCSS = {
  2009. maxHeight: `${READABLE_CONTENT_HEIGHT}px`,
  2010. overflow: "hidden",
  2011. paddingBottom: "0"
  2012. };
  2013. const $contentBox = $('<div class="v2p-reply-content v2p-collapsed">').css(collapsedCSS);
  2014. const $expandBtn = createButton({ children: "\u5C55\u5F00\u56DE\u590D", className: "v2p-expand-btn" });
  2015. const toggleContent = () => {
  2016. const collapsed = $contentBox.hasClass("v2p-collapsed");
  2017. $contentBox.toggleClass("v2p-collapsed").css(
  2018. collapsed ? { maxHeight: "none", overflow: "auto", paddingBottom: "40px" } : collapsedCSS
  2019. );
  2020. $expandBtn.html(collapsed ? "\u6536\u8D77\u56DE\u590D" : "\u5C55\u5F00\u56DE\u590D");
  2021. };
  2022. $expandBtn.on("click", () => {
  2023. toggleContent();
  2024. });
  2025. $contentBox.append($replyContent.clone()).replaceAll($replyContent).append($expandBtn);
  2026. }
  2027. }
  2028. function updateMemberTag(params) {
  2029. const { memberName, memberAvatar, tags, options, ...callbacks } = params;
  2030. const $v2pTags = $(`.v2p-tags-${memberName}`);
  2031. const tagsText = tags ? getTagsText(tags) : void 0;
  2032. if ($v2pTags.length > 0) {
  2033. if (tagsText) {
  2034. $v2pTags.html(`<b>#</b>&nbsp;${tagsText}`);
  2035. } else {
  2036. $v2pTags.remove();
  2037. callbacks.onRemoveExistingTagBlock?.();
  2038. }
  2039. } else {
  2040. if (tagsText) {
  2041. const $tags = $(
  2042. `<div class="v2p-reply-tags v2p-tags-${memberName}" title="${tagsText}"><b>#</b>&nbsp;${tagsText}</div>`
  2043. );
  2044. $tags.on("click", () => {
  2045. openTagsSetter({ memberName, memberAvatar, ...callbacks });
  2046. });
  2047. if (callbacks.onInsertNewTagBlock) {
  2048. callbacks.onInsertNewTagBlock({ $tags });
  2049. } else {
  2050. if (memberName === topicOwnerName) {
  2051. $topicHeader.append($tags.clone(true));
  2052. }
  2053. if (options.userTag.display === "inline") {
  2054. $tags.addClass("v2p-reply-tags-inline").insertBefore(
  2055. $commentCells.filter(`:has(> table strong > a[href="/member/${memberName}"])`).find("> table .badges")
  2056. );
  2057. } else {
  2058. $tags.insertBefore(
  2059. $commentCells.filter(`:has(> table strong > a[href="/member/${memberName}"])`).find("> table .reply_content")
  2060. );
  2061. }
  2062. }
  2063. }
  2064. }
  2065. }
  2066. function openTagsSetter(params) {
  2067. const { memberName, memberAvatar, ...callbacks } = params;
  2068. void (async () => {
  2069. const storage = await getStorage(false);
  2070. const latestTagsData = storage["member-tag" /* MemberTag */];
  2071. const options = storage["options" /* Options */];
  2072. const memberTagData = latestTagsData?.[memberName];
  2073. const tagsValue = memberTagData ? Reflect.has(latestTagsData, memberName) ? memberTagData.tags ? getTagsText(memberTagData.tags) : void 0 : void 0 : void 0;
  2074. const newTagsValue = window.prompt(
  2075. `\u5BF9 @${memberName} \u8BBE\u7F6E\u6807\u7B7E\uFF0C\u591A\u4E2A\u6807\u7B7E\u4EE5\u9017\u53F7\uFF08\uFF0C\uFF09\u5206\u9694\u3002`,
  2076. tagsValue
  2077. );
  2078. if (newTagsValue !== null) {
  2079. const tags = newTagsValue.trim().length > 0 ? newTagsValue.split(/,|,/g).filter((it) => it.trim().length > 0).map((it) => ({ name: it })) : void 0;
  2080. await setMemberTags({ memberName, memberAvatar: memberTagData?.avatar || memberAvatar, tags });
  2081. updateMemberTag({ memberName, memberAvatar, tags, options, ...callbacks });
  2082. }
  2083. })();
  2084. }
  2085. function handleTopicImg() {
  2086. const $imgs = $(".embedded_image");
  2087. if ($imgs.length > 0) {
  2088. const modal = createModal({
  2089. root: $body,
  2090. onMount: ({ $main: $main2, $header, $content }) => {
  2091. $main2.css({
  2092. width: "auto",
  2093. height: "auto",
  2094. display: "flex",
  2095. "justify-content": "center",
  2096. "background-color": "transparent",
  2097. "pointer-events": "none"
  2098. });
  2099. $header.remove();
  2100. $content.css({
  2101. display: "flex",
  2102. "justify-content": "center",
  2103. "align-items": "center",
  2104. "pointer-events": "none"
  2105. });
  2106. },
  2107. onOpen: ({ $content }) => {
  2108. $content.empty();
  2109. }
  2110. });
  2111. $imgs.each((_, img) => {
  2112. const $img = $(img);
  2113. if (img instanceof HTMLImageElement) {
  2114. $img.parent().removeAttr("href");
  2115. if (img.clientWidth !== img.naturalWidth) {
  2116. $img.css({ cursor: "zoom-in" });
  2117. $img.on("click", () => {
  2118. const $clonedImg = $img.clone();
  2119. $clonedImg.css({ cursor: "default", "pointer-events": "auto" });
  2120. modal.open();
  2121. modal.$content.append($clonedImg);
  2122. });
  2123. }
  2124. }
  2125. });
  2126. }
  2127. }
  2128. function handleLinkPreview() {
  2129. const $topicLinks = $(
  2130. '.topic_content a[href*="v2ex.com/t/"], .reply_content a[href*="v2ex.com/t/"], .topic_content a[href^="/t/"], .reply_content a[href^="/t/"]'
  2131. );
  2132. const { handlePreview } = useTopicPreview();
  2133. const $previewBtn = $('<span class="v2p-link-preview-btn">\u9884\u89C8</span>');
  2134. $topicLinks.each((_, link) => {
  2135. const $link = $(link);
  2136. const $cloned = $previewBtn.clone(true);
  2137. $cloned.on("click", (ev) => {
  2138. ev.preventDefault();
  2139. const linkHref = $link.attr("href");
  2140. const match = linkHref?.match(/\/t\/(\d+)/);
  2141. const topicId = match?.at(1);
  2142. handlePreview({ topicId, linkHref });
  2143. });
  2144. const $previewBtnInLink = $link.find(".v2p-link-preview-btn");
  2145. if ($previewBtnInLink.length > 0) {
  2146. $previewBtnInLink.replaceWith($cloned);
  2147. } else {
  2148. $link.append($cloned);
  2149. }
  2150. });
  2151. }
  2152. var init_content = __esm({
  2153. "src/contents/topic/content.ts"() {
  2154. "use strict";
  2155. init_button();
  2156. init_modal();
  2157. init_constants();
  2158. init_use_topic_preview();
  2159. init_utils();
  2160. init_globals();
  2161. init_helpers();
  2162. }
  2163. });
  2164.  
  2165. // src/contents/dom.ts
  2166. function getCommentDataList({
  2167. options,
  2168. $commentTableRows: $commentTableRows2,
  2169. $commentCells: $commentCells2
  2170. }) {
  2171. return $commentTableRows2.map((idx, tr) => {
  2172. const id = $commentCells2[idx].id;
  2173. const $tr = $(tr);
  2174. const $td = $tr.find("> td:nth-child(3)");
  2175. const thanked = $tr.find("> td:last-of-type > .fr").find("> .thank_area").hasClass("thanked");
  2176. const $member = $td.find("> strong > a");
  2177. const memberName = $member.text();
  2178. const memberLink = $member.prop("href");
  2179. const memberAvatar = $tr.find(".avatar").prop("src");
  2180. const $content = $td.find("> .reply_content");
  2181. const content = $content.text();
  2182. const likes = Number($td.find("span.small").text());
  2183. const floor = $td.find("span.no").text();
  2184. const memberNameMatches = Array.from(content.matchAll(/@([a-zA-Z0-9]+)/g));
  2185. const refMemberNames = memberNameMatches.length > 0 ? memberNameMatches.map(([, name]) => {
  2186. return name;
  2187. }) : void 0;
  2188. const floorNumberMatches = Array.from(content.matchAll(/#(\d+)/g));
  2189. const refFloors = floorNumberMatches.length > 0 ? floorNumberMatches.map(([, floor2]) => {
  2190. return floor2;
  2191. }) : void 0;
  2192. let contentHtml = void 0;
  2193. if (refMemberNames) {
  2194. const canHideRefName = options.nestedReply.display === "indent" && !!options.replyContent.hideRefName;
  2195. if (canHideRefName) {
  2196. if (refMemberNames.length === 1) {
  2197. contentHtml = $content.html();
  2198. const pattern = /(@<a href="\/member\/\w+">[\w\s]+<\/a>)\s+/g;
  2199. const replacement = '<span class="v2p-member-ref">$1</span> ';
  2200. contentHtml = contentHtml.replace(pattern, replacement);
  2201. }
  2202. }
  2203. }
  2204. return {
  2205. id,
  2206. memberName,
  2207. memberLink,
  2208. memberAvatar,
  2209. content,
  2210. contentHtml,
  2211. likes,
  2212. floor,
  2213. index: idx,
  2214. refMemberNames,
  2215. refFloors,
  2216. thanked
  2217. };
  2218. }).get();
  2219. }
  2220. function handleNestedComment({
  2221. options,
  2222. $commentCells: $commentCells2,
  2223. commentDataList: commentDataList2
  2224. }) {
  2225. const display = options.nestedReply.display;
  2226. if (display !== "off") {
  2227. $commentCells2.each((i, cellDom) => {
  2228. const $cellDom = $(cellDom);
  2229. const dataFromIndex = commentDataList2.at(i);
  2230. if (options.replyContent.autoFold) {
  2231. processReplyContent($cellDom);
  2232. }
  2233. const currentComment = dataFromIndex?.id === cellDom.id ? dataFromIndex : commentDataList2.find((data) => data.id === cellDom.id);
  2234. if (currentComment) {
  2235. const { refMemberNames, refFloors } = currentComment;
  2236. if (!refMemberNames || refMemberNames.length === 0) {
  2237. return;
  2238. }
  2239. const moreThanOneRefMember = refMemberNames.length > 1;
  2240. if (options.nestedReply.multipleInsideOne === "off" && refMemberNames.length > 1) {
  2241. return;
  2242. }
  2243. for (const refName of moreThanOneRefMember ? refMemberNames.toReversed() : refMemberNames) {
  2244. for (let j = i - 1; j >= 0; j--) {
  2245. const { memberName: compareName, floor: eachFloor } = commentDataList2.at(j) || {};
  2246. if (compareName === refName) {
  2247. let refCommentIdx = j;
  2248. const firstRefFloor = moreThanOneRefMember ? refFloors?.toReversed().at(0) : refFloors?.at(0);
  2249. if (firstRefFloor && firstRefFloor !== eachFloor) {
  2250. const targetIdx = commentDataList2.slice(0, j).findIndex((data) => data.floor === firstRefFloor && data.memberName === refName);
  2251. if (targetIdx >= 0) {
  2252. refCommentIdx = targetIdx;
  2253. }
  2254. }
  2255. if (display === "indent") {
  2256. cellDom.classList.add("v2p-indent");
  2257. }
  2258. $commentCells2.eq(refCommentIdx).append(cellDom);
  2259. return;
  2260. }
  2261. }
  2262. }
  2263. }
  2264. });
  2265. }
  2266. }
  2267. var init_dom = __esm({
  2268. "src/contents/dom.ts"() {
  2269. "use strict";
  2270. init_content();
  2271. }
  2272. });
  2273.  
  2274. // src/use-topic-preview.ts
  2275. function useTopicPreview() {
  2276. const storage = getStorageSync();
  2277. const options = storage["options" /* Options */];
  2278. const PAT = storage["api" /* API */]?.pat;
  2279. let abortController = null;
  2280. const $detailBtn = createButton({
  2281. children: "\u8FDB\u5165\u4E3B\u9898",
  2282. className: "special",
  2283. tag: "a"
  2284. });
  2285. if (options.openInNewTab) {
  2286. $detailBtn.prop("target", "_blank");
  2287. }
  2288. const modal = createModal({
  2289. root: $body,
  2290. onMount: ({ $actions }) => {
  2291. $actions.prepend($detailBtn);
  2292. },
  2293. onClose: ({ $title, $content }) => {
  2294. $title.empty();
  2295. $content.empty();
  2296. abortController?.abort();
  2297. }
  2298. });
  2299. const handlePreview = (params) => {
  2300. const { topicId, topicTitle = "", linkHref } = params;
  2301. if (topicId) {
  2302. modal.open();
  2303. $detailBtn.prop("href", linkHref);
  2304. const $titleLink = $(`
  2305. <a class="v2p-topic-preview-title-link" title="${topicTitle}" href="${linkHref || ""}">
  2306. ${topicTitle}
  2307. </a>
  2308. `);
  2309. if (options.openInNewTab) {
  2310. $titleLink.prop("target", "_blank");
  2311. }
  2312. modal.$title.empty().append($titleLink);
  2313. if (PAT) {
  2314. const load = async () => {
  2315. let cacheData = topicDataCache.get(topicId);
  2316. if (!cacheData || Date.now() - cacheData.cacheTime > 1e3 * 60 * 10) {
  2317. abortController = new AbortController();
  2318. modal.$content.empty().append(`
  2319. <div class="v2p-tpr-loading">
  2320. <div class="v2p-tpr-info">
  2321. <div class="v2p-tpr v2p-tpr-info-avatar"></div>
  2322. <div class="v2p-tpr v2p-tpr-info-text"></div>
  2323. </div>
  2324.  
  2325. <div class="v2p-tpr-content">
  2326. <div class="v2p-tpr v2p-tpr-content-p"></div>
  2327. <div class="v2p-tpr v2p-tpr-content-p"></div>
  2328. <div class="v2p-tpr v2p-tpr-content-p" style="width: 70%;"></div>
  2329. </div>
  2330.  
  2331. <div class="v2p-tpr-cmt">
  2332. <div class="v2p-tpr v2p-tpr-cmt-avatar"></div>
  2333. <div class="v2p-tpr-cmt-right">
  2334. <div class="v2p-tpr v2p-tpr-cmt-header"></div>
  2335. <div class="v2p-tpr v2p-tpr-cmt-p"></div>
  2336. <div class="v2p-tpr v2p-tpr-cmt-p" style="width: 70%;"></div>
  2337. </div>
  2338. </div>
  2339.  
  2340. <div class="v2p-tpr-cmt" style="opacity: 0.8;">
  2341. <div class="v2p-tpr v2p-tpr-cmt-avatar"></div>
  2342. <div class="v2p-tpr-cmt-right">
  2343. <div class="v2p-tpr v2p-tpr-cmt-header"></div>
  2344. <div class="v2p-tpr v2p-tpr-cmt-p"></div>
  2345. <div class="v2p-tpr v2p-tpr-cmt-p" style="width: 70%;"></div>
  2346. </div>
  2347. </div>
  2348.  
  2349. <div class="v2p-tpr-cmt" style="opacity: 0.6;">
  2350. <div class="v2p-tpr v2p-tpr-cmt-avatar"></div>
  2351. <div class="v2p-tpr-cmt-right">
  2352. <div class="v2p-tpr v2p-tpr-cmt-header"></div>
  2353. <div class="v2p-tpr v2p-tpr-cmt-p"></div>
  2354. <div class="v2p-tpr v2p-tpr-cmt-p" style="width: 70%;"></div>
  2355. </div>
  2356. </div>
  2357. <div class="v2p-tpr-cmt" style="opacity: 0.4;">
  2358. <div class="v2p-tpr v2p-tpr-cmt-avatar"></div>
  2359. <div class="v2p-tpr-cmt-right">
  2360. <div class="v2p-tpr v2p-tpr-cmt-header"></div>
  2361. <div class="v2p-tpr v2p-tpr-cmt-p"></div>
  2362. <div class="v2p-tpr v2p-tpr-cmt-p" style="width: 70%;"></div>
  2363. </div>
  2364. </div>
  2365. </div>
  2366. `);
  2367. const promises = [
  2368. fetchTopic(topicId, { signal: abortController.signal }),
  2369. crawlTopicPage(`/t/${topicId}`)
  2370. ];
  2371. try {
  2372. const [{ result: topic }, topicPageText] = await Promise.all(promises);
  2373. const data = {
  2374. topic,
  2375. cacheTime: Date.now(),
  2376. topicPageText
  2377. };
  2378. topicDataCache.set(topicId, data);
  2379. cacheData = data;
  2380. } catch (err) {
  2381. const $errorTip = $('<div style="padding: 20px; text-align: center;">');
  2382. if (err instanceof Error) {
  2383. if (
  2384. // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
  2385. err.message === "Invalid token" /* InvalidToken */ || // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
  2386. err.message === "Token expired" /* TokenExpired */
  2387. ) {
  2388. $errorTip.html(
  2389. invalidTemplate(
  2390. '\u60A8\u7684 Token \u5DF2\u5931\u6548\uFF0C\u8BF7<a class="v2p-topic-preview-retry" href="https://www.v2ex.com/settings/tokens" target="_blank">\u91CD\u65B0\u8BBE\u7F6E</a>\u3002'
  2391. )
  2392. );
  2393. }
  2394. } else {
  2395. $errorTip.html('\u52A0\u8F7D\u4E3B\u9898\u5931\u8D25\uFF0C<a class="v2p-topic-preview-retry">\u70B9\u51FB\u91CD\u8BD5</a>\u3002');
  2396. $errorTip.find(".v2p-topic-preview-retry").on("click", () => {
  2397. load();
  2398. });
  2399. }
  2400. modal.$content.empty().append($errorTip);
  2401. }
  2402. }
  2403. if (cacheData) {
  2404. const { topic, topicPageText } = cacheData;
  2405. if (!topicTitle) {
  2406. $titleLink.attr("title", topic.title);
  2407. $titleLink.text(topic.title);
  2408. }
  2409. const $page = $(topicPageText);
  2410. const $topicPreview = $('<div id="Main" class="v2p-topic-preview">');
  2411. const $infoBar = $(`
  2412. <div class="v2p-tp-info-bar">
  2413. <div class="v2p-tp-info">
  2414. <a class="v2p-tp-member" href="${topic.member.url}">
  2415. <img class="v2p-tp-avatar" src="${topic.member.avatar}">
  2416. <span>${topic.member.username}</span>
  2417. </a>
  2418.  
  2419. <span>
  2420. ${formatTimestamp(topic.created, { format: "YMDHM" })}
  2421. </span>
  2422.  
  2423. <span>${topic.replies} \u6761\u56DE\u590D</span>
  2424. </div>
  2425. </div>
  2426. `);
  2427. const iconBook = createElement$1(BookOpenCheck);
  2428. iconBook.setAttribute("width", "100%");
  2429. iconBook.setAttribute("height", "100%");
  2430. const $readingBtn = $(
  2431. '<div class="v2p-tp-read"><span class="v2p-tp-read-icon"></span>\u7A0D\u540E\u9605\u8BFB</div>'
  2432. );
  2433. $readingBtn.find(".v2p-tp-read-icon").append(iconBook);
  2434. $readingBtn.on("click", () => {
  2435. void addToReadingList({
  2436. url: topic.url,
  2437. title: topic.title,
  2438. content: topic.content
  2439. });
  2440. }).appendTo($infoBar);
  2441. $topicPreview.append($infoBar);
  2442. const $topicMain = $page.find("#Main");
  2443. const $topicContent = $topicMain.find("> .box > .cell > .topic_content");
  2444. const $topicSubtle = $topicMain.find("> .box >.subtle");
  2445. if ($topicContent.length <= 0) {
  2446. $topicPreview.append(`
  2447. <div class="v2p-empty-content">
  2448. <div class="v2p-text-emoji">\xAF\\_(\u30C4)_/\xAF</div>
  2449. <p>\u8BE5\u4E3B\u9898\u6CA1\u6709\u6B63\u6587\u5185\u5BB9</p>
  2450. </div>
  2451. `);
  2452. } else {
  2453. $topicPreview.append($topicContent);
  2454. $topicContent.wrap('<div class="cell">');
  2455. }
  2456. $topicPreview.append($topicSubtle);
  2457. const $topicReplyBox = $topicMain.find('.box:has(.cell[id^="r_"])');
  2458. if ($topicReplyBox.length > 0) {
  2459. $topicReplyBox.css({ "margin-top": "20px", padding: "0" });
  2460. const $commentCells2 = $topicReplyBox.find('.cell[id^="r_"]');
  2461. const $commentTableRows2 = $commentCells2.find("> table > tbody > tr");
  2462. const commentDataList2 = getCommentDataList({
  2463. options,
  2464. $commentTableRows: $commentTableRows2,
  2465. $commentCells: $commentCells2
  2466. });
  2467. $commentCells2.each((i, cellDom) => {
  2468. const currentComment = commentDataList2.at(i);
  2469. if (currentComment?.id !== cellDom.id) {
  2470. return;
  2471. }
  2472. const $cellDom = $(cellDom);
  2473. const { memberName, thanked } = currentComment;
  2474. if (memberName === loginName && !options.hideAccount) {
  2475. $cellDom.find(".badges").append(
  2476. `<div class="badge ${memberName === topic.member.username ? "mod" : "you"}">YOU</div>`
  2477. );
  2478. }
  2479. const $likesBox = $cellDom.find(".small.fade").addClass("v2p-likes-box");
  2480. $likesBox.find('img[alt="\u2764\uFE0F"]').replaceWith('<span class="v2p-icon-heart"><i data-lucide="heart"></i></span>');
  2481. if (thanked) {
  2482. $likesBox.addClass("v2p-thanked");
  2483. }
  2484. });
  2485. handleNestedComment({ options, $commentCells: $commentCells2, commentDataList: commentDataList2 });
  2486. const $rightArea = $topicReplyBox.find('.cell[id^="r_"] > table > tbody > tr > td:last-of-type > .fr').children(":not(.no)");
  2487. $rightArea.remove();
  2488. $topicPreview.append($topicReplyBox);
  2489. if (topic.replies > 100) {
  2490. $topicPreview.append(`
  2491. <div class="v2p-topic-reply-tip">
  2492. <a
  2493. href="${linkHref || ""}"
  2494. style="color: currentColor;"
  2495. >
  2496. \u5728\u4E3B\u9898\u5185\u67E5\u770B\u5B8C\u6574\u8BC4\u8BBA...
  2497. </a>
  2498. </div>
  2499. `);
  2500. }
  2501. replaceCommentEmojiWithHD($commentCells2);
  2502. }
  2503. if (options.openInNewTab) {
  2504. $topicPreview.find("a").prop("target", "_blank");
  2505. }
  2506. modal.$content.empty().append($topicPreview);
  2507. handleLinkPreview();
  2508. modal.$content.attr("tabindex", "0");
  2509. modal.$content.trigger("focus");
  2510. loadIcons();
  2511. }
  2512. };
  2513. load();
  2514. } else {
  2515. modal.$content.empty().append(invalidTemplate("\u60A8\u9700\u8981\u5148\u8BBE\u7F6E PAT \u624D\u80FD\u83B7\u53D6\u9884\u89C8\u5185\u5BB9\u3002"));
  2516. }
  2517. }
  2518. };
  2519. return {
  2520. handlePreview
  2521. };
  2522. }
  2523. var invalidTemplate, topicDataCache;
  2524. var init_use_topic_preview = __esm({
  2525. "src/use-topic-preview.ts"() {
  2526. "use strict";
  2527. init_lucide();
  2528. init_button();
  2529. init_modal();
  2530. init_constants();
  2531. init_dom();
  2532. init_globals();
  2533. init_helpers();
  2534. init_content();
  2535. init_icons();
  2536. init_services();
  2537. init_utils();
  2538. invalidTemplate = (tip) => `
  2539. <div class="v2p-no-pat">
  2540. <div class="v2p-no-pat-title">${tip}</div>
  2541. <div class="v2p-no-pat-desc">
  2542. \u8BF7\u524D\u5F80<span class="v2p-no-pat-block"><span class="v2p-icon-logo">${iconLogo}</span> <span style="margin: 0 5px;">></span> \u8BBE\u7F6E</span> \u8FDB\u884C\u8BBE\u7F6E\u3002
  2543. </div>
  2544.  
  2545. <div class="v2p-no-pat-steps">
  2546. <div class="v2p-no-pat-step">
  2547. <div style="font-weight:bold;margin-bottom:10px;font-size:15px;">1. \u5728\u6269\u5C55\u7A0B\u5E8F\u5217\u8868\u4E2D\u627E\u5230\u5E76\u70B9\u51FB\u300CV2EX Polish\u300D\u3002</div>
  2548. <img class="v2p-no-pat-img" src="https://i.imgur.com/UfNkuTF.png">
  2549. </div>
  2550. <div class="v2p-no-pat-step">
  2551. <div style="font-weight:bold;margin-bottom:10px;font-size:15px;">2. \u5728\u5F39\u51FA\u7684\u5C0F\u7A97\u53E3\u4E2D\u627E\u5230\u300C\u2699\uFE0F \u6309\u94AE\u300D\uFF0C\u8F93\u5165 PAT\u3002</div>
  2552. <img class="v2p-no-pat-img" src="https://i.imgur.com/O6hP86A.png">
  2553. </div>
  2554. </div>
  2555. </div>
  2556. `;
  2557. topicDataCache = /* @__PURE__ */ new Map();
  2558. }
  2559. });
  2560.  
  2561. // src/contents/home/topic-list.ts
  2562. function handleTopicList() {
  2563. if (!isBrowserExtension()) {
  2564. return;
  2565. }
  2566. const storage = getStorageSync();
  2567. const PAT = storage["api" /* API */]?.pat;
  2568. const { handlePreview } = useTopicPreview();
  2569. const $previewBtn = $('<button class="v2p-topic-preview-btn">\u9884\u89C8</button>');
  2570. const $ignoreBtn = $('<span class="v2p-topic-ignore-btn">\u5C4F\u853D</span>');
  2571. const $topicActions = $('<span class="v2p-topic-actions" />');
  2572. $topicList.each((_, topicItem) => {
  2573. const $topicItem = $(topicItem);
  2574. const $itemTitle = $topicItem.find(".item_title");
  2575. const $topicInfo = $topicItem.find(".topic_info");
  2576. const topicTitle = $itemTitle.find(".topic-link").text();
  2577. const linkHref = $topicItem.find(".topic-link").attr("href");
  2578. const match = linkHref?.match(/\/t\/(\d+)/);
  2579. const topicId = match?.at(1);
  2580. const $lastChild = $topicInfo.find("> span:first-of-type");
  2581. const $clonedTopicActions = $topicActions.clone();
  2582. $clonedTopicActions.insertAfter($lastChild);
  2583. $ignoreBtn.clone().on("click", () => {
  2584. if (confirm(`\u786E\u5B9A\u5C4F\u853D\u4E3B\u9898 \u2308${topicTitle}\u230B\uFF1F`)) {
  2585. if (typeof topicId === "string") {
  2586. void (async () => {
  2587. const toast = createToast({ message: `\u6B63\u5728\u5C4F\u853D\u4E3B\u9898 \u2308${topicTitle}\u230B`, duration: 0 });
  2588. const pageText = await crawlTopicPage(`/t/${topicId}`, "0");
  2589. const $ignoreBtn2 = $(pageText).find(".topic_buttons a:nth-of-type(3)");
  2590. const txt = $ignoreBtn2.attr("onclick");
  2591. if (txt) {
  2592. const match2 = txt.match(/'\/.*'/);
  2593. if (match2) {
  2594. const result = match2[0].slice(1, -1);
  2595. if (result.startsWith("/ignore/topic")) {
  2596. try {
  2597. await fetch(`${"https://www.v2ex.com" /* Origin */}${result}`);
  2598. createToast({ message: `\u2705 \u5DF2\u5C4F\u853D` });
  2599. $topicItem.remove();
  2600. } finally {
  2601. toast.clear();
  2602. }
  2603. }
  2604. }
  2605. }
  2606. })();
  2607. }
  2608. }
  2609. }).appendTo($clonedTopicActions);
  2610. $previewBtn.clone().on("click", () => {
  2611. handlePreview({ topicId, topicTitle, linkHref });
  2612. }).appendTo($clonedTopicActions);
  2613. });
  2614. if (PAT) {
  2615. $("#TopicsHot,#my-recent-topics").find(".cell .item_hot_topic_title").each((_, topicTitle) => {
  2616. const $topicItem = $(topicTitle);
  2617. $previewBtn.clone().on("click", () => {
  2618. const $link = $topicItem.find("> a");
  2619. const linkHref = $link.attr("href");
  2620. const match = linkHref?.match(/\/t\/(\d+)/);
  2621. const topicId = match?.at(1);
  2622. const topicTitle2 = $link.text();
  2623. handlePreview({ topicId, topicTitle: topicTitle2, linkHref });
  2624. }).appendTo($topicItem);
  2625. });
  2626. }
  2627. }
  2628. var init_topic_list = __esm({
  2629. "src/contents/home/topic-list.ts"() {
  2630. "use strict";
  2631. init_toast();
  2632. init_constants();
  2633. init_services();
  2634. init_use_topic_preview();
  2635. init_utils();
  2636. init_globals();
  2637. }
  2638. });
  2639.  
  2640. // src/contents/home/index.ts
  2641. var home_exports = {};
  2642. var init_home = __esm({
  2643. "src/contents/home/index.ts"() {
  2644. "use strict";
  2645. init_constants();
  2646. init_utils();
  2647. init_globals();
  2648. init_helpers();
  2649. init_topic_list();
  2650. void (async () => {
  2651. const storage = await getStorage();
  2652. const options = storage["options" /* Options */];
  2653. {
  2654. $("#Main .tab").addClass("v2p-hover-btn");
  2655. if (options.openInNewTab) {
  2656. $('#Main .topic-link, .item_hot_topic_title > a, .item_node, a[href="/write"]').prop(
  2657. "target",
  2658. "_blank"
  2659. );
  2660. }
  2661. }
  2662. handleTopicList();
  2663. {
  2664. const dailyInfo = storage["daily" /* Daily */];
  2665. if (dailyInfo?.lastCheckInTime) {
  2666. if (isSameDay(dailyInfo.lastCheckInTime, Date.now())) {
  2667. const $info = $(`
  2668. <a class="cell v2p-info-row" href="/mission/daily">
  2669. \u4ECA\u65E5\u5DF2\u81EA\u52A8\u7B7E\u5230
  2670. </a>
  2671. `);
  2672. $infoCard.append($info);
  2673. }
  2674. }
  2675. }
  2676. loadIcons();
  2677. })();
  2678. }
  2679. });
  2680.  
  2681. // node_modules/.pnpm/@floating-ui+utils@0.2.3/node_modules/@floating-ui/utils/dist/floating-ui.utils.mjs
  2682. function clamp(start, value, end) {
  2683. return max(start, min(value, end));
  2684. }
  2685. function evaluate(value, param) {
  2686. return typeof value === "function" ? value(param) : value;
  2687. }
  2688. function getSide(placement) {
  2689. return placement.split("-")[0];
  2690. }
  2691. function getAlignment(placement) {
  2692. return placement.split("-")[1];
  2693. }
  2694. function getOppositeAxis(axis) {
  2695. return axis === "x" ? "y" : "x";
  2696. }
  2697. function getAxisLength(axis) {
  2698. return axis === "y" ? "height" : "width";
  2699. }
  2700. function getSideAxis(placement) {
  2701. return ["top", "bottom"].includes(getSide(placement)) ? "y" : "x";
  2702. }
  2703. function getAlignmentAxis(placement) {
  2704. return getOppositeAxis(getSideAxis(placement));
  2705. }
  2706. function getAlignmentSides(placement, rects, rtl) {
  2707. if (rtl === void 0) {
  2708. rtl = false;
  2709. }
  2710. const alignment = getAlignment(placement);
  2711. const alignmentAxis = getAlignmentAxis(placement);
  2712. const length = getAxisLength(alignmentAxis);
  2713. let mainAlignmentSide = alignmentAxis === "x" ? alignment === (rtl ? "end" : "start") ? "right" : "left" : alignment === "start" ? "bottom" : "top";
  2714. if (rects.reference[length] > rects.floating[length]) {
  2715. mainAlignmentSide = getOppositePlacement(mainAlignmentSide);
  2716. }
  2717. return [mainAlignmentSide, getOppositePlacement(mainAlignmentSide)];
  2718. }
  2719. function getExpandedPlacements(placement) {
  2720. const oppositePlacement = getOppositePlacement(placement);
  2721. return [getOppositeAlignmentPlacement(placement), oppositePlacement, getOppositeAlignmentPlacement(oppositePlacement)];
  2722. }
  2723. function getOppositeAlignmentPlacement(placement) {
  2724. return placement.replace(/start|end/g, (alignment) => oppositeAlignmentMap[alignment]);
  2725. }
  2726. function getSideList(side, isStart, rtl) {
  2727. const lr = ["left", "right"];
  2728. const rl = ["right", "left"];
  2729. const tb = ["top", "bottom"];
  2730. const bt = ["bottom", "top"];
  2731. switch (side) {
  2732. case "top":
  2733. case "bottom":
  2734. if (rtl) return isStart ? rl : lr;
  2735. return isStart ? lr : rl;
  2736. case "left":
  2737. case "right":
  2738. return isStart ? tb : bt;
  2739. default:
  2740. return [];
  2741. }
  2742. }
  2743. function getOppositeAxisPlacements(placement, flipAlignment, direction, rtl) {
  2744. const alignment = getAlignment(placement);
  2745. let list = getSideList(getSide(placement), direction === "start", rtl);
  2746. if (alignment) {
  2747. list = list.map((side) => side + "-" + alignment);
  2748. if (flipAlignment) {
  2749. list = list.concat(list.map(getOppositeAlignmentPlacement));
  2750. }
  2751. }
  2752. return list;
  2753. }
  2754. function getOppositePlacement(placement) {
  2755. return placement.replace(/left|right|bottom|top/g, (side) => oppositeSideMap[side]);
  2756. }
  2757. function expandPaddingObject(padding) {
  2758. return {
  2759. top: 0,
  2760. right: 0,
  2761. bottom: 0,
  2762. left: 0,
  2763. ...padding
  2764. };
  2765. }
  2766. function getPaddingObject(padding) {
  2767. return typeof padding !== "number" ? expandPaddingObject(padding) : {
  2768. top: padding,
  2769. right: padding,
  2770. bottom: padding,
  2771. left: padding
  2772. };
  2773. }
  2774. function rectToClientRect(rect) {
  2775. const {
  2776. x,
  2777. y,
  2778. width,
  2779. height
  2780. } = rect;
  2781. return {
  2782. width,
  2783. height,
  2784. top: y,
  2785. left: x,
  2786. right: x + width,
  2787. bottom: y + height,
  2788. x,
  2789. y
  2790. };
  2791. }
  2792. var min, max, oppositeSideMap, oppositeAlignmentMap;
  2793. var init_floating_ui_utils = __esm({
  2794. "node_modules/.pnpm/@floating-ui+utils@0.2.3/node_modules/@floating-ui/utils/dist/floating-ui.utils.mjs"() {
  2795. "use strict";
  2796. min = Math.min;
  2797. max = Math.max;
  2798. oppositeSideMap = {
  2799. left: "right",
  2800. right: "left",
  2801. bottom: "top",
  2802. top: "bottom"
  2803. };
  2804. oppositeAlignmentMap = {
  2805. start: "end",
  2806. end: "start"
  2807. };
  2808. }
  2809. });
  2810.  
  2811. // node_modules/.pnpm/@floating-ui+core@1.6.3/node_modules/@floating-ui/core/dist/floating-ui.core.mjs
  2812. function computeCoordsFromPlacement(_ref, placement, rtl) {
  2813. let {
  2814. reference,
  2815. floating
  2816. } = _ref;
  2817. const sideAxis = getSideAxis(placement);
  2818. const alignmentAxis = getAlignmentAxis(placement);
  2819. const alignLength = getAxisLength(alignmentAxis);
  2820. const side = getSide(placement);
  2821. const isVertical = sideAxis === "y";
  2822. const commonX = reference.x + reference.width / 2 - floating.width / 2;
  2823. const commonY = reference.y + reference.height / 2 - floating.height / 2;
  2824. const commonAlign = reference[alignLength] / 2 - floating[alignLength] / 2;
  2825. let coords;
  2826. switch (side) {
  2827. case "top":
  2828. coords = {
  2829. x: commonX,
  2830. y: reference.y - floating.height
  2831. };
  2832. break;
  2833. case "bottom":
  2834. coords = {
  2835. x: commonX,
  2836. y: reference.y + reference.height
  2837. };
  2838. break;
  2839. case "right":
  2840. coords = {
  2841. x: reference.x + reference.width,
  2842. y: commonY
  2843. };
  2844. break;
  2845. case "left":
  2846. coords = {
  2847. x: reference.x - floating.width,
  2848. y: commonY
  2849. };
  2850. break;
  2851. default:
  2852. coords = {
  2853. x: reference.x,
  2854. y: reference.y
  2855. };
  2856. }
  2857. switch (getAlignment(placement)) {
  2858. case "start":
  2859. coords[alignmentAxis] -= commonAlign * (rtl && isVertical ? -1 : 1);
  2860. break;
  2861. case "end":
  2862. coords[alignmentAxis] += commonAlign * (rtl && isVertical ? -1 : 1);
  2863. break;
  2864. }
  2865. return coords;
  2866. }
  2867. async function detectOverflow(state, options) {
  2868. var _await$platform$isEle;
  2869. if (options === void 0) {
  2870. options = {};
  2871. }
  2872. const {
  2873. x,
  2874. y,
  2875. platform: platform2,
  2876. rects,
  2877. elements,
  2878. strategy
  2879. } = state;
  2880. const {
  2881. boundary = "clippingAncestors",
  2882. rootBoundary = "viewport",
  2883. elementContext = "floating",
  2884. altBoundary = false,
  2885. padding = 0
  2886. } = evaluate(options, state);
  2887. const paddingObject = getPaddingObject(padding);
  2888. const altContext = elementContext === "floating" ? "reference" : "floating";
  2889. const element = elements[altBoundary ? altContext : elementContext];
  2890. const clippingClientRect = rectToClientRect(await platform2.getClippingRect({
  2891. element: ((_await$platform$isEle = await (platform2.isElement == null ? void 0 : platform2.isElement(element))) != null ? _await$platform$isEle : true) ? element : element.contextElement || await (platform2.getDocumentElement == null ? void 0 : platform2.getDocumentElement(elements.floating)),
  2892. boundary,
  2893. rootBoundary,
  2894. strategy
  2895. }));
  2896. const rect = elementContext === "floating" ? {
  2897. x,
  2898. y,
  2899. width: rects.floating.width,
  2900. height: rects.floating.height
  2901. } : rects.reference;
  2902. const offsetParent = await (platform2.getOffsetParent == null ? void 0 : platform2.getOffsetParent(elements.floating));
  2903. const offsetScale = await (platform2.isElement == null ? void 0 : platform2.isElement(offsetParent)) ? await (platform2.getScale == null ? void 0 : platform2.getScale(offsetParent)) || {
  2904. x: 1,
  2905. y: 1
  2906. } : {
  2907. x: 1,
  2908. y: 1
  2909. };
  2910. const elementClientRect = rectToClientRect(platform2.convertOffsetParentRelativeRectToViewportRelativeRect ? await platform2.convertOffsetParentRelativeRectToViewportRelativeRect({
  2911. elements,
  2912. rect,
  2913. offsetParent,
  2914. strategy
  2915. }) : rect);
  2916. return {
  2917. top: (clippingClientRect.top - elementClientRect.top + paddingObject.top) / offsetScale.y,
  2918. bottom: (elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom) / offsetScale.y,
  2919. left: (clippingClientRect.left - elementClientRect.left + paddingObject.left) / offsetScale.x,
  2920. right: (elementClientRect.right - clippingClientRect.right + paddingObject.right) / offsetScale.x
  2921. };
  2922. }
  2923. async function convertValueToCoords(state, options) {
  2924. const {
  2925. placement,
  2926. platform: platform2,
  2927. elements
  2928. } = state;
  2929. const rtl = await (platform2.isRTL == null ? void 0 : platform2.isRTL(elements.floating));
  2930. const side = getSide(placement);
  2931. const alignment = getAlignment(placement);
  2932. const isVertical = getSideAxis(placement) === "y";
  2933. const mainAxisMulti = ["left", "top"].includes(side) ? -1 : 1;
  2934. const crossAxisMulti = rtl && isVertical ? -1 : 1;
  2935. const rawValue = evaluate(options, state);
  2936. let {
  2937. mainAxis,
  2938. crossAxis,
  2939. alignmentAxis
  2940. } = typeof rawValue === "number" ? {
  2941. mainAxis: rawValue,
  2942. crossAxis: 0,
  2943. alignmentAxis: null
  2944. } : {
  2945. mainAxis: 0,
  2946. crossAxis: 0,
  2947. alignmentAxis: null,
  2948. ...rawValue
  2949. };
  2950. if (alignment && typeof alignmentAxis === "number") {
  2951. crossAxis = alignment === "end" ? alignmentAxis * -1 : alignmentAxis;
  2952. }
  2953. return isVertical ? {
  2954. x: crossAxis * crossAxisMulti,
  2955. y: mainAxis * mainAxisMulti
  2956. } : {
  2957. x: mainAxis * mainAxisMulti,
  2958. y: crossAxis * crossAxisMulti
  2959. };
  2960. }
  2961. var computePosition, flip, offset, shift;
  2962. var init_floating_ui_core = __esm({
  2963. "node_modules/.pnpm/@floating-ui+core@1.6.3/node_modules/@floating-ui/core/dist/floating-ui.core.mjs"() {
  2964. "use strict";
  2965. init_floating_ui_utils();
  2966. init_floating_ui_utils();
  2967. computePosition = async (reference, floating, config) => {
  2968. const {
  2969. placement = "bottom",
  2970. strategy = "absolute",
  2971. middleware = [],
  2972. platform: platform2
  2973. } = config;
  2974. const validMiddleware = middleware.filter(Boolean);
  2975. const rtl = await (platform2.isRTL == null ? void 0 : platform2.isRTL(floating));
  2976. let rects = await platform2.getElementRects({
  2977. reference,
  2978. floating,
  2979. strategy
  2980. });
  2981. let {
  2982. x,
  2983. y
  2984. } = computeCoordsFromPlacement(rects, placement, rtl);
  2985. let statefulPlacement = placement;
  2986. let middlewareData = {};
  2987. let resetCount = 0;
  2988. for (let i = 0; i < validMiddleware.length; i++) {
  2989. const {
  2990. name,
  2991. fn
  2992. } = validMiddleware[i];
  2993. const {
  2994. x: nextX,
  2995. y: nextY,
  2996. data,
  2997. reset
  2998. } = await fn({
  2999. x,
  3000. y,
  3001. initialPlacement: placement,
  3002. placement: statefulPlacement,
  3003. strategy,
  3004. middlewareData,
  3005. rects,
  3006. platform: platform2,
  3007. elements: {
  3008. reference,
  3009. floating
  3010. }
  3011. });
  3012. x = nextX != null ? nextX : x;
  3013. y = nextY != null ? nextY : y;
  3014. middlewareData = {
  3015. ...middlewareData,
  3016. [name]: {
  3017. ...middlewareData[name],
  3018. ...data
  3019. }
  3020. };
  3021. if (reset && resetCount <= 50) {
  3022. resetCount++;
  3023. if (typeof reset === "object") {
  3024. if (reset.placement) {
  3025. statefulPlacement = reset.placement;
  3026. }
  3027. if (reset.rects) {
  3028. rects = reset.rects === true ? await platform2.getElementRects({
  3029. reference,
  3030. floating,
  3031. strategy
  3032. }) : reset.rects;
  3033. }
  3034. ({
  3035. x,
  3036. y
  3037. } = computeCoordsFromPlacement(rects, statefulPlacement, rtl));
  3038. }
  3039. i = -1;
  3040. }
  3041. }
  3042. return {
  3043. x,
  3044. y,
  3045. placement: statefulPlacement,
  3046. strategy,
  3047. middlewareData
  3048. };
  3049. };
  3050. flip = function(options) {
  3051. if (options === void 0) {
  3052. options = {};
  3053. }
  3054. return {
  3055. name: "flip",
  3056. options,
  3057. async fn(state) {
  3058. var _middlewareData$arrow, _middlewareData$flip;
  3059. const {
  3060. placement,
  3061. middlewareData,
  3062. rects,
  3063. initialPlacement,
  3064. platform: platform2,
  3065. elements
  3066. } = state;
  3067. const {
  3068. mainAxis: checkMainAxis = true,
  3069. crossAxis: checkCrossAxis = true,
  3070. fallbackPlacements: specifiedFallbackPlacements,
  3071. fallbackStrategy = "bestFit",
  3072. fallbackAxisSideDirection = "none",
  3073. flipAlignment = true,
  3074. ...detectOverflowOptions
  3075. } = evaluate(options, state);
  3076. if ((_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) {
  3077. return {};
  3078. }
  3079. const side = getSide(placement);
  3080. const initialSideAxis = getSideAxis(initialPlacement);
  3081. const isBasePlacement = getSide(initialPlacement) === initialPlacement;
  3082. const rtl = await (platform2.isRTL == null ? void 0 : platform2.isRTL(elements.floating));
  3083. const fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipAlignment ? [getOppositePlacement(initialPlacement)] : getExpandedPlacements(initialPlacement));
  3084. const hasFallbackAxisSideDirection = fallbackAxisSideDirection !== "none";
  3085. if (!specifiedFallbackPlacements && hasFallbackAxisSideDirection) {
  3086. fallbackPlacements.push(...getOppositeAxisPlacements(initialPlacement, flipAlignment, fallbackAxisSideDirection, rtl));
  3087. }
  3088. const placements2 = [initialPlacement, ...fallbackPlacements];
  3089. const overflow = await detectOverflow(state, detectOverflowOptions);
  3090. const overflows = [];
  3091. let overflowsData = ((_middlewareData$flip = middlewareData.flip) == null ? void 0 : _middlewareData$flip.overflows) || [];
  3092. if (checkMainAxis) {
  3093. overflows.push(overflow[side]);
  3094. }
  3095. if (checkCrossAxis) {
  3096. const sides2 = getAlignmentSides(placement, rects, rtl);
  3097. overflows.push(overflow[sides2[0]], overflow[sides2[1]]);
  3098. }
  3099. overflowsData = [...overflowsData, {
  3100. placement,
  3101. overflows
  3102. }];
  3103. if (!overflows.every((side2) => side2 <= 0)) {
  3104. var _middlewareData$flip2, _overflowsData$filter;
  3105. const nextIndex = (((_middlewareData$flip2 = middlewareData.flip) == null ? void 0 : _middlewareData$flip2.index) || 0) + 1;
  3106. const nextPlacement = placements2[nextIndex];
  3107. if (nextPlacement) {
  3108. return {
  3109. data: {
  3110. index: nextIndex,
  3111. overflows: overflowsData
  3112. },
  3113. reset: {
  3114. placement: nextPlacement
  3115. }
  3116. };
  3117. }
  3118. let resetPlacement = (_overflowsData$filter = overflowsData.filter((d) => d.overflows[0] <= 0).sort((a, b) => a.overflows[1] - b.overflows[1])[0]) == null ? void 0 : _overflowsData$filter.placement;
  3119. if (!resetPlacement) {
  3120. switch (fallbackStrategy) {
  3121. case "bestFit": {
  3122. var _overflowsData$filter2;
  3123. const placement2 = (_overflowsData$filter2 = overflowsData.filter((d) => {
  3124. if (hasFallbackAxisSideDirection) {
  3125. const currentSideAxis = getSideAxis(d.placement);
  3126. return currentSideAxis === initialSideAxis || // Create a bias to the `y` side axis due to horizontal
  3127. // reading directions favoring greater width.
  3128. currentSideAxis === "y";
  3129. }
  3130. return true;
  3131. }).map((d) => [d.placement, d.overflows.filter((overflow2) => overflow2 > 0).reduce((acc, overflow2) => acc + overflow2, 0)]).sort((a, b) => a[1] - b[1])[0]) == null ? void 0 : _overflowsData$filter2[0];
  3132. if (placement2) {
  3133. resetPlacement = placement2;
  3134. }
  3135. break;
  3136. }
  3137. case "initialPlacement":
  3138. resetPlacement = initialPlacement;
  3139. break;
  3140. }
  3141. }
  3142. if (placement !== resetPlacement) {
  3143. return {
  3144. reset: {
  3145. placement: resetPlacement
  3146. }
  3147. };
  3148. }
  3149. }
  3150. return {};
  3151. }
  3152. };
  3153. };
  3154. offset = function(options) {
  3155. if (options === void 0) {
  3156. options = 0;
  3157. }
  3158. return {
  3159. name: "offset",
  3160. options,
  3161. async fn(state) {
  3162. var _middlewareData$offse, _middlewareData$arrow;
  3163. const {
  3164. x,
  3165. y,
  3166. placement,
  3167. middlewareData
  3168. } = state;
  3169. const diffCoords = await convertValueToCoords(state, options);
  3170. if (placement === ((_middlewareData$offse = middlewareData.offset) == null ? void 0 : _middlewareData$offse.placement) && (_middlewareData$arrow = middlewareData.arrow) != null && _middlewareData$arrow.alignmentOffset) {
  3171. return {};
  3172. }
  3173. return {
  3174. x: x + diffCoords.x,
  3175. y: y + diffCoords.y,
  3176. data: {
  3177. ...diffCoords,
  3178. placement
  3179. }
  3180. };
  3181. }
  3182. };
  3183. };
  3184. shift = function(options) {
  3185. if (options === void 0) {
  3186. options = {};
  3187. }
  3188. return {
  3189. name: "shift",
  3190. options,
  3191. async fn(state) {
  3192. const {
  3193. x,
  3194. y,
  3195. placement
  3196. } = state;
  3197. const {
  3198. mainAxis: checkMainAxis = true,
  3199. crossAxis: checkCrossAxis = false,
  3200. limiter = {
  3201. fn: (_ref) => {
  3202. let {
  3203. x: x2,
  3204. y: y2
  3205. } = _ref;
  3206. return {
  3207. x: x2,
  3208. y: y2
  3209. };
  3210. }
  3211. },
  3212. ...detectOverflowOptions
  3213. } = evaluate(options, state);
  3214. const coords = {
  3215. x,
  3216. y
  3217. };
  3218. const overflow = await detectOverflow(state, detectOverflowOptions);
  3219. const crossAxis = getSideAxis(getSide(placement));
  3220. const mainAxis = getOppositeAxis(crossAxis);
  3221. let mainAxisCoord = coords[mainAxis];
  3222. let crossAxisCoord = coords[crossAxis];
  3223. if (checkMainAxis) {
  3224. const minSide = mainAxis === "y" ? "top" : "left";
  3225. const maxSide = mainAxis === "y" ? "bottom" : "right";
  3226. const min3 = mainAxisCoord + overflow[minSide];
  3227. const max3 = mainAxisCoord - overflow[maxSide];
  3228. mainAxisCoord = clamp(min3, mainAxisCoord, max3);
  3229. }
  3230. if (checkCrossAxis) {
  3231. const minSide = crossAxis === "y" ? "top" : "left";
  3232. const maxSide = crossAxis === "y" ? "bottom" : "right";
  3233. const min3 = crossAxisCoord + overflow[minSide];
  3234. const max3 = crossAxisCoord - overflow[maxSide];
  3235. crossAxisCoord = clamp(min3, crossAxisCoord, max3);
  3236. }
  3237. const limitedCoords = limiter.fn({
  3238. ...state,
  3239. [mainAxis]: mainAxisCoord,
  3240. [crossAxis]: crossAxisCoord
  3241. });
  3242. return {
  3243. ...limitedCoords,
  3244. data: {
  3245. x: limitedCoords.x - x,
  3246. y: limitedCoords.y - y
  3247. }
  3248. };
  3249. }
  3250. };
  3251. };
  3252. }
  3253. });
  3254.  
  3255. // node_modules/.pnpm/@floating-ui+dom@1.4.5/node_modules/@floating-ui/dom/dist/floating-ui.dom.mjs
  3256. function getWindow(node) {
  3257. var _node$ownerDocument;
  3258. return (node == null ? void 0 : (_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.defaultView) || window;
  3259. }
  3260. function getComputedStyle$1(element) {
  3261. return getWindow(element).getComputedStyle(element);
  3262. }
  3263. function isNode(value) {
  3264. return value instanceof getWindow(value).Node;
  3265. }
  3266. function getNodeName(node) {
  3267. if (isNode(node)) {
  3268. return (node.nodeName || "").toLowerCase();
  3269. }
  3270. return "#document";
  3271. }
  3272. function isHTMLElement(value) {
  3273. return value instanceof HTMLElement || value instanceof getWindow(value).HTMLElement;
  3274. }
  3275. function isShadowRoot(node) {
  3276. if (typeof ShadowRoot === "undefined") {
  3277. return false;
  3278. }
  3279. return node instanceof getWindow(node).ShadowRoot || node instanceof ShadowRoot;
  3280. }
  3281. function isOverflowElement(element) {
  3282. const {
  3283. overflow,
  3284. overflowX,
  3285. overflowY,
  3286. display
  3287. } = getComputedStyle$1(element);
  3288. return /auto|scroll|overlay|hidden|clip/.test(overflow + overflowY + overflowX) && !["inline", "contents"].includes(display);
  3289. }
  3290. function isTableElement(element) {
  3291. return ["table", "td", "th"].includes(getNodeName(element));
  3292. }
  3293. function isContainingBlock(element) {
  3294. const safari = isSafari();
  3295. const css = getComputedStyle$1(element);
  3296. return css.transform !== "none" || css.perspective !== "none" || (css.containerType ? css.containerType !== "normal" : false) || !safari && (css.backdropFilter ? css.backdropFilter !== "none" : false) || !safari && (css.filter ? css.filter !== "none" : false) || ["transform", "perspective", "filter"].some((value) => (css.willChange || "").includes(value)) || ["paint", "layout", "strict", "content"].some((value) => (css.contain || "").includes(value));
  3297. }
  3298. function isSafari() {
  3299. if (typeof CSS === "undefined" || !CSS.supports) return false;
  3300. return CSS.supports("-webkit-backdrop-filter", "none");
  3301. }
  3302. function isLastTraversableNode(node) {
  3303. return ["html", "body", "#document"].includes(getNodeName(node));
  3304. }
  3305. function getCssDimensions(element) {
  3306. const css = getComputedStyle$1(element);
  3307. let width = parseFloat(css.width) || 0;
  3308. let height = parseFloat(css.height) || 0;
  3309. const hasOffset = isHTMLElement(element);
  3310. const offsetWidth = hasOffset ? element.offsetWidth : width;
  3311. const offsetHeight = hasOffset ? element.offsetHeight : height;
  3312. const shouldFallback = round(width) !== offsetWidth || round(height) !== offsetHeight;
  3313. if (shouldFallback) {
  3314. width = offsetWidth;
  3315. height = offsetHeight;
  3316. }
  3317. return {
  3318. width,
  3319. height,
  3320. $: shouldFallback
  3321. };
  3322. }
  3323. function isElement(value) {
  3324. return value instanceof Element || value instanceof getWindow(value).Element;
  3325. }
  3326. function unwrapElement(element) {
  3327. return !isElement(element) ? element.contextElement : element;
  3328. }
  3329. function getScale(element) {
  3330. const domElement = unwrapElement(element);
  3331. if (!isHTMLElement(domElement)) {
  3332. return createCoords(1);
  3333. }
  3334. const rect = domElement.getBoundingClientRect();
  3335. const {
  3336. width,
  3337. height,
  3338. $: $2
  3339. } = getCssDimensions(domElement);
  3340. let x = ($2 ? round(rect.width) : rect.width) / width;
  3341. let y = ($2 ? round(rect.height) : rect.height) / height;
  3342. if (!x || !Number.isFinite(x)) {
  3343. x = 1;
  3344. }
  3345. if (!y || !Number.isFinite(y)) {
  3346. y = 1;
  3347. }
  3348. return {
  3349. x,
  3350. y
  3351. };
  3352. }
  3353. function getVisualOffsets(element) {
  3354. const win = getWindow(element);
  3355. if (!isSafari() || !win.visualViewport) {
  3356. return noOffsets;
  3357. }
  3358. return {
  3359. x: win.visualViewport.offsetLeft,
  3360. y: win.visualViewport.offsetTop
  3361. };
  3362. }
  3363. function shouldAddVisualOffsets(element, isFixed, floatingOffsetParent) {
  3364. if (isFixed === void 0) {
  3365. isFixed = false;
  3366. }
  3367. if (!floatingOffsetParent || isFixed && floatingOffsetParent !== getWindow(element)) {
  3368. return false;
  3369. }
  3370. return isFixed;
  3371. }
  3372. function getBoundingClientRect(element, includeScale, isFixedStrategy, offsetParent) {
  3373. if (includeScale === void 0) {
  3374. includeScale = false;
  3375. }
  3376. if (isFixedStrategy === void 0) {
  3377. isFixedStrategy = false;
  3378. }
  3379. const clientRect = element.getBoundingClientRect();
  3380. const domElement = unwrapElement(element);
  3381. let scale = createCoords(1);
  3382. if (includeScale) {
  3383. if (offsetParent) {
  3384. if (isElement(offsetParent)) {
  3385. scale = getScale(offsetParent);
  3386. }
  3387. } else {
  3388. scale = getScale(element);
  3389. }
  3390. }
  3391. const visualOffsets = shouldAddVisualOffsets(domElement, isFixedStrategy, offsetParent) ? getVisualOffsets(domElement) : createCoords(0);
  3392. let x = (clientRect.left + visualOffsets.x) / scale.x;
  3393. let y = (clientRect.top + visualOffsets.y) / scale.y;
  3394. let width = clientRect.width / scale.x;
  3395. let height = clientRect.height / scale.y;
  3396. if (domElement) {
  3397. const win = getWindow(domElement);
  3398. const offsetWin = offsetParent && isElement(offsetParent) ? getWindow(offsetParent) : offsetParent;
  3399. let currentIFrame = win.frameElement;
  3400. while (currentIFrame && offsetParent && offsetWin !== win) {
  3401. const iframeScale = getScale(currentIFrame);
  3402. const iframeRect = currentIFrame.getBoundingClientRect();
  3403. const css = getComputedStyle(currentIFrame);
  3404. const left = iframeRect.left + (currentIFrame.clientLeft + parseFloat(css.paddingLeft)) * iframeScale.x;
  3405. const top = iframeRect.top + (currentIFrame.clientTop + parseFloat(css.paddingTop)) * iframeScale.y;
  3406. x *= iframeScale.x;
  3407. y *= iframeScale.y;
  3408. width *= iframeScale.x;
  3409. height *= iframeScale.y;
  3410. x += left;
  3411. y += top;
  3412. currentIFrame = getWindow(currentIFrame).frameElement;
  3413. }
  3414. }
  3415. return rectToClientRect({
  3416. width,
  3417. height,
  3418. x,
  3419. y
  3420. });
  3421. }
  3422. function getNodeScroll(element) {
  3423. if (isElement(element)) {
  3424. return {
  3425. scrollLeft: element.scrollLeft,
  3426. scrollTop: element.scrollTop
  3427. };
  3428. }
  3429. return {
  3430. scrollLeft: element.pageXOffset,
  3431. scrollTop: element.pageYOffset
  3432. };
  3433. }
  3434. function getDocumentElement(node) {
  3435. var _ref;
  3436. return (_ref = (isNode(node) ? node.ownerDocument : node.document) || window.document) == null ? void 0 : _ref.documentElement;
  3437. }
  3438. function convertOffsetParentRelativeRectToViewportRelativeRect(_ref) {
  3439. let {
  3440. rect,
  3441. offsetParent,
  3442. strategy
  3443. } = _ref;
  3444. const isOffsetParentAnElement = isHTMLElement(offsetParent);
  3445. const documentElement = getDocumentElement(offsetParent);
  3446. if (offsetParent === documentElement) {
  3447. return rect;
  3448. }
  3449. let scroll = {
  3450. scrollLeft: 0,
  3451. scrollTop: 0
  3452. };
  3453. let scale = createCoords(1);
  3454. const offsets = createCoords(0);
  3455. if (isOffsetParentAnElement || !isOffsetParentAnElement && strategy !== "fixed") {
  3456. if (getNodeName(offsetParent) !== "body" || isOverflowElement(documentElement)) {
  3457. scroll = getNodeScroll(offsetParent);
  3458. }
  3459. if (isHTMLElement(offsetParent)) {
  3460. const offsetRect = getBoundingClientRect(offsetParent);
  3461. scale = getScale(offsetParent);
  3462. offsets.x = offsetRect.x + offsetParent.clientLeft;
  3463. offsets.y = offsetRect.y + offsetParent.clientTop;
  3464. }
  3465. }
  3466. return {
  3467. width: rect.width * scale.x,
  3468. height: rect.height * scale.y,
  3469. x: rect.x * scale.x - scroll.scrollLeft * scale.x + offsets.x,
  3470. y: rect.y * scale.y - scroll.scrollTop * scale.y + offsets.y
  3471. };
  3472. }
  3473. function getClientRects(element) {
  3474. return Array.from(element.getClientRects());
  3475. }
  3476. function getWindowScrollBarX(element) {
  3477. return getBoundingClientRect(getDocumentElement(element)).left + getNodeScroll(element).scrollLeft;
  3478. }
  3479. function getDocumentRect(element) {
  3480. const html = getDocumentElement(element);
  3481. const scroll = getNodeScroll(element);
  3482. const body = element.ownerDocument.body;
  3483. const width = max2(html.scrollWidth, html.clientWidth, body.scrollWidth, body.clientWidth);
  3484. const height = max2(html.scrollHeight, html.clientHeight, body.scrollHeight, body.clientHeight);
  3485. let x = -scroll.scrollLeft + getWindowScrollBarX(element);
  3486. const y = -scroll.scrollTop;
  3487. if (getComputedStyle$1(body).direction === "rtl") {
  3488. x += max2(html.clientWidth, body.clientWidth) - width;
  3489. }
  3490. return {
  3491. width,
  3492. height,
  3493. x,
  3494. y
  3495. };
  3496. }
  3497. function getParentNode(node) {
  3498. if (getNodeName(node) === "html") {
  3499. return node;
  3500. }
  3501. const result = (
  3502. // Step into the shadow DOM of the parent of a slotted node.
  3503. node.assignedSlot || // DOM Element detected.
  3504. node.parentNode || // ShadowRoot detected.
  3505. isShadowRoot(node) && node.host || // Fallback.
  3506. getDocumentElement(node)
  3507. );
  3508. return isShadowRoot(result) ? result.host : result;
  3509. }
  3510. function getNearestOverflowAncestor(node) {
  3511. const parentNode = getParentNode(node);
  3512. if (isLastTraversableNode(parentNode)) {
  3513. return node.ownerDocument ? node.ownerDocument.body : node.body;
  3514. }
  3515. if (isHTMLElement(parentNode) && isOverflowElement(parentNode)) {
  3516. return parentNode;
  3517. }
  3518. return getNearestOverflowAncestor(parentNode);
  3519. }
  3520. function getOverflowAncestors(node, list) {
  3521. var _node$ownerDocument;
  3522. if (list === void 0) {
  3523. list = [];
  3524. }
  3525. const scrollableAncestor = getNearestOverflowAncestor(node);
  3526. const isBody = scrollableAncestor === ((_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.body);
  3527. const win = getWindow(scrollableAncestor);
  3528. if (isBody) {
  3529. return list.concat(win, win.visualViewport || [], isOverflowElement(scrollableAncestor) ? scrollableAncestor : []);
  3530. }
  3531. return list.concat(scrollableAncestor, getOverflowAncestors(scrollableAncestor));
  3532. }
  3533. function getViewportRect(element, strategy) {
  3534. const win = getWindow(element);
  3535. const html = getDocumentElement(element);
  3536. const visualViewport = win.visualViewport;
  3537. let width = html.clientWidth;
  3538. let height = html.clientHeight;
  3539. let x = 0;
  3540. let y = 0;
  3541. if (visualViewport) {
  3542. width = visualViewport.width;
  3543. height = visualViewport.height;
  3544. const visualViewportBased = isSafari();
  3545. if (!visualViewportBased || visualViewportBased && strategy === "fixed") {
  3546. x = visualViewport.offsetLeft;
  3547. y = visualViewport.offsetTop;
  3548. }
  3549. }
  3550. return {
  3551. width,
  3552. height,
  3553. x,
  3554. y
  3555. };
  3556. }
  3557. function getInnerBoundingClientRect(element, strategy) {
  3558. const clientRect = getBoundingClientRect(element, true, strategy === "fixed");
  3559. const top = clientRect.top + element.clientTop;
  3560. const left = clientRect.left + element.clientLeft;
  3561. const scale = isHTMLElement(element) ? getScale(element) : createCoords(1);
  3562. const width = element.clientWidth * scale.x;
  3563. const height = element.clientHeight * scale.y;
  3564. const x = left * scale.x;
  3565. const y = top * scale.y;
  3566. return {
  3567. width,
  3568. height,
  3569. x,
  3570. y
  3571. };
  3572. }
  3573. function getClientRectFromClippingAncestor(element, clippingAncestor, strategy) {
  3574. let rect;
  3575. if (clippingAncestor === "viewport") {
  3576. rect = getViewportRect(element, strategy);
  3577. } else if (clippingAncestor === "document") {
  3578. rect = getDocumentRect(getDocumentElement(element));
  3579. } else if (isElement(clippingAncestor)) {
  3580. rect = getInnerBoundingClientRect(clippingAncestor, strategy);
  3581. } else {
  3582. const visualOffsets = getVisualOffsets(element);
  3583. rect = {
  3584. ...clippingAncestor,
  3585. x: clippingAncestor.x - visualOffsets.x,
  3586. y: clippingAncestor.y - visualOffsets.y
  3587. };
  3588. }
  3589. return rectToClientRect(rect);
  3590. }
  3591. function hasFixedPositionAncestor(element, stopNode) {
  3592. const parentNode = getParentNode(element);
  3593. if (parentNode === stopNode || !isElement(parentNode) || isLastTraversableNode(parentNode)) {
  3594. return false;
  3595. }
  3596. return getComputedStyle$1(parentNode).position === "fixed" || hasFixedPositionAncestor(parentNode, stopNode);
  3597. }
  3598. function getClippingElementAncestors(element, cache) {
  3599. const cachedResult = cache.get(element);
  3600. if (cachedResult) {
  3601. return cachedResult;
  3602. }
  3603. let result = getOverflowAncestors(element).filter((el) => isElement(el) && getNodeName(el) !== "body");
  3604. let currentContainingBlockComputedStyle = null;
  3605. const elementIsFixed = getComputedStyle$1(element).position === "fixed";
  3606. let currentNode = elementIsFixed ? getParentNode(element) : element;
  3607. while (isElement(currentNode) && !isLastTraversableNode(currentNode)) {
  3608. const computedStyle = getComputedStyle$1(currentNode);
  3609. const currentNodeIsContaining = isContainingBlock(currentNode);
  3610. if (!currentNodeIsContaining && computedStyle.position === "fixed") {
  3611. currentContainingBlockComputedStyle = null;
  3612. }
  3613. const shouldDropCurrentNode = elementIsFixed ? !currentNodeIsContaining && !currentContainingBlockComputedStyle : !currentNodeIsContaining && computedStyle.position === "static" && !!currentContainingBlockComputedStyle && ["absolute", "fixed"].includes(currentContainingBlockComputedStyle.position) || isOverflowElement(currentNode) && !currentNodeIsContaining && hasFixedPositionAncestor(element, currentNode);
  3614. if (shouldDropCurrentNode) {
  3615. result = result.filter((ancestor) => ancestor !== currentNode);
  3616. } else {
  3617. currentContainingBlockComputedStyle = computedStyle;
  3618. }
  3619. currentNode = getParentNode(currentNode);
  3620. }
  3621. cache.set(element, result);
  3622. return result;
  3623. }
  3624. function getClippingRect(_ref) {
  3625. let {
  3626. element,
  3627. boundary,
  3628. rootBoundary,
  3629. strategy
  3630. } = _ref;
  3631. const elementClippingAncestors = boundary === "clippingAncestors" ? getClippingElementAncestors(element, this._c) : [].concat(boundary);
  3632. const clippingAncestors = [...elementClippingAncestors, rootBoundary];
  3633. const firstClippingAncestor = clippingAncestors[0];
  3634. const clippingRect = clippingAncestors.reduce((accRect, clippingAncestor) => {
  3635. const rect = getClientRectFromClippingAncestor(element, clippingAncestor, strategy);
  3636. accRect.top = max2(rect.top, accRect.top);
  3637. accRect.right = min2(rect.right, accRect.right);
  3638. accRect.bottom = min2(rect.bottom, accRect.bottom);
  3639. accRect.left = max2(rect.left, accRect.left);
  3640. return accRect;
  3641. }, getClientRectFromClippingAncestor(element, firstClippingAncestor, strategy));
  3642. return {
  3643. width: clippingRect.right - clippingRect.left,
  3644. height: clippingRect.bottom - clippingRect.top,
  3645. x: clippingRect.left,
  3646. y: clippingRect.top
  3647. };
  3648. }
  3649. function getDimensions(element) {
  3650. return getCssDimensions(element);
  3651. }
  3652. function getRectRelativeToOffsetParent(element, offsetParent, strategy) {
  3653. const isOffsetParentAnElement = isHTMLElement(offsetParent);
  3654. const documentElement = getDocumentElement(offsetParent);
  3655. const isFixed = strategy === "fixed";
  3656. const rect = getBoundingClientRect(element, true, isFixed, offsetParent);
  3657. let scroll = {
  3658. scrollLeft: 0,
  3659. scrollTop: 0
  3660. };
  3661. const offsets = createCoords(0);
  3662. if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {
  3663. if (getNodeName(offsetParent) !== "body" || isOverflowElement(documentElement)) {
  3664. scroll = getNodeScroll(offsetParent);
  3665. }
  3666. if (isHTMLElement(offsetParent)) {
  3667. const offsetRect = getBoundingClientRect(offsetParent, true, isFixed, offsetParent);
  3668. offsets.x = offsetRect.x + offsetParent.clientLeft;
  3669. offsets.y = offsetRect.y + offsetParent.clientTop;
  3670. } else if (documentElement) {
  3671. offsets.x = getWindowScrollBarX(documentElement);
  3672. }
  3673. }
  3674. return {
  3675. x: rect.left + scroll.scrollLeft - offsets.x,
  3676. y: rect.top + scroll.scrollTop - offsets.y,
  3677. width: rect.width,
  3678. height: rect.height
  3679. };
  3680. }
  3681. function getTrueOffsetParent(element, polyfill) {
  3682. if (!isHTMLElement(element) || getComputedStyle$1(element).position === "fixed") {
  3683. return null;
  3684. }
  3685. if (polyfill) {
  3686. return polyfill(element);
  3687. }
  3688. return element.offsetParent;
  3689. }
  3690. function getContainingBlock(element) {
  3691. let currentNode = getParentNode(element);
  3692. while (isHTMLElement(currentNode) && !isLastTraversableNode(currentNode)) {
  3693. if (isContainingBlock(currentNode)) {
  3694. return currentNode;
  3695. } else {
  3696. currentNode = getParentNode(currentNode);
  3697. }
  3698. }
  3699. return null;
  3700. }
  3701. function getOffsetParent(element, polyfill) {
  3702. const window2 = getWindow(element);
  3703. if (!isHTMLElement(element)) {
  3704. return window2;
  3705. }
  3706. let offsetParent = getTrueOffsetParent(element, polyfill);
  3707. while (offsetParent && isTableElement(offsetParent) && getComputedStyle$1(offsetParent).position === "static") {
  3708. offsetParent = getTrueOffsetParent(offsetParent, polyfill);
  3709. }
  3710. if (offsetParent && (getNodeName(offsetParent) === "html" || getNodeName(offsetParent) === "body" && getComputedStyle$1(offsetParent).position === "static" && !isContainingBlock(offsetParent))) {
  3711. return window2;
  3712. }
  3713. return offsetParent || getContainingBlock(element) || window2;
  3714. }
  3715. function isRTL(element) {
  3716. return getComputedStyle(element).direction === "rtl";
  3717. }
  3718. var min2, max2, round, createCoords, noOffsets, getElementRects, platform, computePosition2;
  3719. var init_floating_ui_dom = __esm({
  3720. "node_modules/.pnpm/@floating-ui+dom@1.4.5/node_modules/@floating-ui/dom/dist/floating-ui.dom.mjs"() {
  3721. "use strict";
  3722. init_floating_ui_core();
  3723. init_floating_ui_core();
  3724. min2 = Math.min;
  3725. max2 = Math.max;
  3726. round = Math.round;
  3727. createCoords = (v) => ({
  3728. x: v,
  3729. y: v
  3730. });
  3731. noOffsets = /* @__PURE__ */ createCoords(0);
  3732. getElementRects = async function(_ref) {
  3733. let {
  3734. reference,
  3735. floating,
  3736. strategy
  3737. } = _ref;
  3738. const getOffsetParentFn = this.getOffsetParent || getOffsetParent;
  3739. const getDimensionsFn = this.getDimensions;
  3740. return {
  3741. reference: getRectRelativeToOffsetParent(reference, await getOffsetParentFn(floating), strategy),
  3742. floating: {
  3743. x: 0,
  3744. y: 0,
  3745. ...await getDimensionsFn(floating)
  3746. }
  3747. };
  3748. };
  3749. platform = {
  3750. convertOffsetParentRelativeRectToViewportRelativeRect,
  3751. getDocumentElement,
  3752. getClippingRect,
  3753. getOffsetParent,
  3754. getElementRects,
  3755. getClientRects,
  3756. getDimensions,
  3757. getScale,
  3758. isElement,
  3759. isRTL
  3760. };
  3761. computePosition2 = (reference, floating, options) => {
  3762. const cache = /* @__PURE__ */ new Map();
  3763. const mergedOptions = {
  3764. platform,
  3765. ...options
  3766. };
  3767. const platformWithCache = {
  3768. ...mergedOptions.platform,
  3769. _c: cache
  3770. };
  3771. return computePosition(reference, floating, {
  3772. ...mergedOptions,
  3773. platform: platformWithCache
  3774. });
  3775. };
  3776. }
  3777. });
  3778.  
  3779. // src/components/popup.ts
  3780. function createPopup(props) {
  3781. const {
  3782. root,
  3783. trigger,
  3784. triggerType = "click",
  3785. content,
  3786. options,
  3787. onOpen,
  3788. onClose,
  3789. placement = "bottom-start",
  3790. offsetOptions = { mainAxis: 5, crossAxis: 5 }
  3791. } = props;
  3792. const $popupContent = $('<div class="v2p-popup-content">');
  3793. const $popup = $('<div class="v2p-popup" tabindex="0">').css("visibility", "hidden").append($popupContent);
  3794. root.append($popup);
  3795. if (content) {
  3796. $popup.append(content);
  3797. }
  3798. const popup = $popup.get(0);
  3799. const handleClickOutside = (ev) => {
  3800. if ($(ev.target).closest(popup).length === 0) {
  3801. handlePopupClose();
  3802. }
  3803. };
  3804. const handlePopupClose = () => {
  3805. $popup.css("visibility", "hidden");
  3806. $(document).off("click", handleClickOutside);
  3807. onClose?.();
  3808. popupControl2.onClose?.();
  3809. };
  3810. const handlePopupOpen = ($reference) => {
  3811. if (!$reference) {
  3812. return;
  3813. }
  3814. setTimeout(() => {
  3815. $(document).on("click", handleClickOutside);
  3816. });
  3817. const referenceElement = $reference.get(0);
  3818. computePosition2(referenceElement, popup, {
  3819. placement,
  3820. middleware: [offset(offsetOptions), flip(), shift({ padding: 8 })],
  3821. ...options
  3822. }).then(({ x, y }) => {
  3823. Object.assign(popup.style, {
  3824. left: `${x}px`,
  3825. top: `${y}px`
  3826. });
  3827. $popup.css("visibility", "visible");
  3828. }).catch(() => {
  3829. handlePopupClose();
  3830. createToast({ message: "\u274C Popup \u6E32\u67D3\u5931\u8D25" });
  3831. });
  3832. onOpen?.();
  3833. };
  3834. const popupControl2 = {
  3835. $content: $popupContent,
  3836. isOver: false,
  3837. open: (reference) => {
  3838. handlePopupOpen(reference);
  3839. },
  3840. close: handlePopupClose
  3841. };
  3842. if (triggerType === "hover") {
  3843. $popup.on("mouseover", () => {
  3844. if (!popupControl2.isOver) {
  3845. popupControl2.isOver = true;
  3846. $popup.off("mouseleave").on("mouseleave", () => {
  3847. popupControl2.isOver = false;
  3848. setTimeout(() => {
  3849. if (!popupControl2.isOver) {
  3850. popupControl2.close();
  3851. }
  3852. }, hoverDelay);
  3853. });
  3854. }
  3855. });
  3856. }
  3857. trigger?.on("click", () => {
  3858. if (popup.style.visibility !== "hidden") {
  3859. handlePopupClose();
  3860. } else {
  3861. handlePopupOpen(trigger);
  3862. }
  3863. });
  3864. return popupControl2;
  3865. }
  3866. var hoverDelay;
  3867. var init_popup = __esm({
  3868. "src/components/popup.ts"() {
  3869. "use strict";
  3870. init_floating_ui_dom();
  3871. init_toast();
  3872. hoverDelay = 350;
  3873. }
  3874. });
  3875.  
  3876. // src/contents/topic/avatar.ts
  3877. function processAvatar(params) {
  3878. const {
  3879. $trigger,
  3880. popupControl: popupControl2,
  3881. commentData,
  3882. shouldWrap = true,
  3883. openInNewTab = false,
  3884. onSetTagsClick
  3885. } = params;
  3886. const { memberName, memberAvatar, memberLink } = commentData;
  3887. let abortController = null;
  3888. const handleOver = () => {
  3889. popupControl2.close();
  3890. popupControl2.open($trigger);
  3891. const $content = $(`
  3892. <div class="v2p-member-card">
  3893. <div class="v2p-info">
  3894. <div class="v2p-info-left">
  3895. <a class="v2p-avatar-box" href="${memberLink}" target="${openInNewTab ? "_blank" : "_self"}">
  3896. <img class="v2p-avatar" src="${memberAvatar}">
  3897. </a>
  3898. </div>
  3899.  
  3900. <div class="v2p-info-right">
  3901. <div class="v2p-username">
  3902. <a href="${memberLink}" target="${openInNewTab ? "_blank" : "_self"}">${memberName}</a>
  3903. </div>
  3904. <div class="v2p-no v2p-loading"></div>
  3905. <div class="v2p-created-date v2p-loading"></div>
  3906. </div>
  3907.  
  3908. </div>
  3909.  
  3910. <div class="v2p-bio" style="disply:none;"></div>
  3911.  
  3912. <div class="v2p-member-card-actions"></div>
  3913. </div>
  3914. `);
  3915. popupControl2.$content.empty().append($content);
  3916. createButton({ children: "\u6DFB\u52A0\u7528\u6237\u6807\u7B7E" }).on("click", () => {
  3917. popupControl2.close();
  3918. openTagsSetter({ memberName, memberAvatar });
  3919. onSetTagsClick?.();
  3920. }).appendTo($(".v2p-member-card-actions"));
  3921. void (async () => {
  3922. if (!memberDataCache.has(memberName)) {
  3923. abortController = new AbortController();
  3924. popupControl2.onClose = () => {
  3925. abortController?.abort();
  3926. };
  3927. try {
  3928. const memberData = await fetchUserInfo(memberName, {
  3929. signal: abortController.signal
  3930. });
  3931. memberDataCache.set(memberName, {
  3932. ...memberData,
  3933. registerDays: getRegisterDays(memberData.created)
  3934. });
  3935. } catch (err) {
  3936. if (err instanceof Error) {
  3937. $content.html(`<span>${err.message}</span>`);
  3938. if (err.cause === 404) {
  3939. memberDataCache.set(memberName, banned);
  3940. }
  3941. }
  3942. return null;
  3943. }
  3944. }
  3945. const data = memberDataCache.get(memberName);
  3946. if (typeof data === "object") {
  3947. $content.find(".v2p-no").removeClass("v2p-loading").text(`V2EX \u7B2C ${data.id} \u53F7\u4F1A\u5458`);
  3948. $content.find(".v2p-created-date").removeClass("v2p-loading").text(`\u52A0\u5165\u4E8E ${formatTimestamp(data.created)}`);
  3949. if (data.registerDays <= 30) {
  3950. $content.find(".v2p-info-right").append(
  3951. `<div class="v2p-register-days" style="margin: 5px 0;">${data.registerDays <= 15 ? "15" : "30"} \u5929\u5185\u6CE8\u518C</div>`
  3952. );
  3953. }
  3954. if (data.bio && data.bio.trim().length > 0) {
  3955. $content.find(".v2p-bio").css("disply", "block").text(data.bio);
  3956. }
  3957. } else if (typeof data === "symbol" && data === banned) {
  3958. $content.html("<span>\u67E5\u65E0\u6B64\u7528\u6237\uFF0C\u7591\u4F3C\u5DF2\u88AB\u5C01\u7981</span>");
  3959. }
  3960. })();
  3961. };
  3962. let isOver = false;
  3963. $trigger.on("mouseover", () => {
  3964. isOver = true;
  3965. setTimeout(() => {
  3966. if (isOver) {
  3967. handleOver();
  3968. }
  3969. }, hoverDelay);
  3970. }).on("mouseleave", () => {
  3971. isOver = false;
  3972. setTimeout(() => {
  3973. if (!popupControl2.isOver && !isOver) {
  3974. popupControl2.close();
  3975. }
  3976. }, hoverDelay);
  3977. });
  3978. if (shouldWrap) {
  3979. $trigger.wrap(
  3980. `<a href="/member/${commentData.memberName}" target="_blank" style="cursor: pointer;">`
  3981. );
  3982. }
  3983. }
  3984. var banned, memberDataCache;
  3985. var init_avatar = __esm({
  3986. "src/contents/topic/avatar.ts"() {
  3987. "use strict";
  3988. init_button();
  3989. init_popup();
  3990. init_services();
  3991. init_utils();
  3992. init_helpers();
  3993. init_content();
  3994. banned = Symbol();
  3995. memberDataCache = /* @__PURE__ */ new Map();
  3996. }
  3997. });
  3998.  
  3999. // src/contents/topic/comment.ts
  4000. function handleFilteredComments() {
  4001. const iconHeart = createElement$1(Heart);
  4002. iconHeart.setAttribute("width", "100%");
  4003. iconHeart.setAttribute("height", "100%");
  4004. const $commentsBtn = $(
  4005. `<span class="v2p-tool v2p-hover-btn"><span class="v2p-tool-icon"></span>\u70ED\u95E8\u56DE\u590D</span>`
  4006. );
  4007. $commentsBtn.find(".v2p-tool-icon").append(iconHeart);
  4008. $(".v2p-tools").prepend($commentsBtn);
  4009. const popularCommentData = commentDataList.filter(({ likes }) => likes > 0).sort((a, b) => b.likes - a.likes);
  4010. const popularCount = popularCommentData.length;
  4011. const modal = createModal({
  4012. root: $main,
  4013. onMount: ({ $title, $content }) => {
  4014. const $template = $('<div class="v2p-modal-comments">');
  4015. const $t1 = $template.clone().attr("data-tab-key", "hot").addClass("v2p-tab-content-active");
  4016. const $t2 = $template.clone().attr("data-tab-key", "recent");
  4017. const $t3 = $template.clone().attr("data-tab-key", "op");
  4018. {
  4019. if (popularCount > 0) {
  4020. popularCommentData.forEach(({ index, refMemberNames }) => {
  4021. const $clonedCell = $commentCells.eq(index).clone();
  4022. $clonedCell.find(".v2p-controls > a:has(.v2p-control-reply)").remove();
  4023. $clonedCell.find(".no").css("pointer-events", "none");
  4024. const firstRefMember = refMemberNames?.at(0);
  4025. if (firstRefMember) {
  4026. const replyMember = commentDataList.findLast(
  4027. (it, idx) => idx < index && it.memberName === firstRefMember
  4028. );
  4029. if (replyMember) {
  4030. $clonedCell.prepend(
  4031. $(`
  4032. <div class="v2p-topic-reply-ref">
  4033. <div class="v2p-topic-reply">
  4034. <div class="v2p-topic-reply-member">
  4035. <a href="${replyMember.memberLink}" target="_blank">
  4036. <img class="v2p-topic-reply-avatar" src="${replyMember.memberAvatar}">
  4037. <span>${replyMember.memberName}</span>
  4038. </a>\uFF1A
  4039. </div>
  4040. <div class="v2p-topic-reply-content">${escapeHTML(replyMember.content)}</div>
  4041. </div>
  4042. </div>
  4043. `)
  4044. );
  4045. }
  4046. }
  4047. $t1.append($clonedCell);
  4048. });
  4049. } else {
  4050. $t1.append($("<div>\u6682\u65E0\u70ED\u95E8\u56DE\u590D</div>").css({ padding: "20px", textAlign: "center" }));
  4051. }
  4052. $content.append($t1);
  4053. }
  4054. {
  4055. const len = commentDataList.length;
  4056. const displayNum = len < 10 ? len : len <= 10 ? 5 : len <= 30 ? 10 : len <= 60 ? 20 : len <= 100 ? 40 : len <= 200 ? 60 : 90;
  4057. if (displayNum > 0) {
  4058. const recentCommentData = commentDataList.slice(-1 * displayNum).reverse();
  4059. recentCommentData.forEach(({ index, refMemberNames }) => {
  4060. const $clonedCell = $commentCells.eq(index).clone();
  4061. $clonedCell.find(".v2p-controls > a:has(.v2p-control-reply)").remove();
  4062. $clonedCell.find(".no").css("pointer-events", "none");
  4063. const firstRefMember = refMemberNames?.at(0);
  4064. if (firstRefMember) {
  4065. const replyMember = commentDataList.findLast(
  4066. (it, idx) => idx < index && it.memberName === firstRefMember
  4067. );
  4068. if (replyMember) {
  4069. $clonedCell.prepend(
  4070. $(`
  4071. <div class="v2p-topic-reply-ref">
  4072. <div class="v2p-topic-reply">
  4073. <div class="v2p-topic-reply-member">
  4074. <a href="${replyMember.memberLink}" target="_blank">
  4075. <img class="v2p-topic-reply-avatar" src="${replyMember.memberAvatar}">
  4076. <span>${replyMember.memberName}</span>
  4077. </a>\uFF1A
  4078. </div>
  4079. <div class="v2p-topic-reply-content">${escapeHTML(replyMember.content)}</div>
  4080. </div>
  4081. </div>
  4082. `)
  4083. );
  4084. }
  4085. }
  4086. $t2.append($clonedCell);
  4087. });
  4088. } else {
  4089. $t2.append($("<div>\u6682\u65E0\u6700\u8FD1\u56DE\u590D</div>").css({ padding: "20px", textAlign: "center" }));
  4090. }
  4091. $content.append($t2);
  4092. }
  4093. {
  4094. const opCommentData = commentDataList.filter(
  4095. ({ memberName }) => memberName === topicOwnerName
  4096. );
  4097. if (opCommentData.length > 0) {
  4098. opCommentData.forEach(({ index, refMemberNames }) => {
  4099. const $clonedCell = $commentCells.eq(index).clone();
  4100. $clonedCell.find(".v2p-controls > a:has(.v2p-control-reply)").remove();
  4101. $clonedCell.find(".no").css("pointer-events", "none");
  4102. const firstRefMember = refMemberNames?.at(0);
  4103. if (firstRefMember) {
  4104. const replyMember = commentDataList.findLast(
  4105. (it, idx) => idx < index && it.memberName === firstRefMember
  4106. );
  4107. if (replyMember) {
  4108. $clonedCell.prepend(
  4109. $(`
  4110. <div class="v2p-topic-reply-ref">
  4111. <div class="v2p-topic-reply">
  4112. <div class="v2p-topic-reply-member">
  4113. <a href="${replyMember.memberLink}" target="_blank">
  4114. <img class="v2p-topic-reply-avatar" src="${replyMember.memberAvatar}">
  4115. <span>${replyMember.memberName}</span>
  4116. </a>\uFF1A
  4117. </div>
  4118. <div class="v2p-topic-reply-content">${escapeHTML(replyMember.content)}</div>
  4119. </div>
  4120. </div>
  4121. `)
  4122. );
  4123. }
  4124. }
  4125. $t3.append($clonedCell);
  4126. });
  4127. } else {
  4128. $t3.append($("<div>\u6682\u65E0\u9898\u4E3B\u56DE\u590D</div>").css({ padding: "20px", textAlign: "center" }));
  4129. }
  4130. $content.append($t3);
  4131. }
  4132. const $tabs = $(`
  4133. <div class="v2p-modal-comment-tabs">
  4134. <div data-tab-key="hot" class="v2p-tab-active">\u70ED\u95E8\u56DE\u590D</div>
  4135. <div data-tab-key="recent">\u6700\u8FD1\u56DE\u590D</div>
  4136. <div data-tab-key="op">\u9898\u4E3B\u56DE\u590D</div>
  4137. </div>
  4138. `);
  4139. $title.append($tabs);
  4140. $tabs.find("[data-tab-key]").on("click", (ev) => {
  4141. const $target = $(ev.currentTarget);
  4142. const { tabKey } = $target.data();
  4143. if (typeof tabKey === "string") {
  4144. $target.addClass("v2p-tab-active").siblings().removeClass("v2p-tab-active");
  4145. $(`.v2p-modal-comments[data-tab-key="${tabKey}"]`).addClass("v2p-tab-content-active").siblings().removeClass("v2p-tab-content-active");
  4146. }
  4147. });
  4148. replaceCommentEmojiWithHD($content.find('.cell[id^="r_"]'));
  4149. },
  4150. onOpen: ({ $content }) => {
  4151. const storage = getStorageSync();
  4152. const options = storage["options" /* Options */];
  4153. $content.find('.cell[id^="r_"]').each((_, cellDom) => {
  4154. if (options.replyContent.autoFold) {
  4155. processReplyContent($(cellDom));
  4156. }
  4157. });
  4158. handleLinkPreview();
  4159. }
  4160. });
  4161. $commentsBtn.on("click", () => {
  4162. modal.open();
  4163. });
  4164. {
  4165. const $commentBoxCount = $commentBox.find(".cell:first-of-type > span.gray");
  4166. const countText = $commentBoxCount.text();
  4167. const newCountText = countText.substring(0, countText.indexOf("\u56DE\u590D") + 2);
  4168. const countTextSpan = `<span class="count-text">${newCountText}</span><span class="v2p-dot">\xB7</span>${popularCount} \u6761\u70ED\u95E8\u56DE\u590D`;
  4169. $commentBoxCount.empty().append(countTextSpan);
  4170. }
  4171. }
  4172. function processActions($cellDom, data) {
  4173. const $actions = $cellDom.find("> table > tbody > tr > td:last-of-type > .fr");
  4174. const $controls = $('<span class="v2p-controls">');
  4175. const $thankIcon = $(
  4176. `<span
  4177. class="v2p-control v2p-control-thank"
  4178. data-id="${data.id}"
  4179. data-member-name="${data.memberName}"
  4180. >
  4181. <i data-lucide="heart"></i>
  4182. </span>`
  4183. );
  4184. const thankArea = $actions.find("> .thank_area");
  4185. const thanked = thankArea.hasClass("thanked");
  4186. if (thanked) {
  4187. $thankIcon.addClass("v2p-thanked");
  4188. $controls.append($("<a>").append($thankIcon));
  4189. } else {
  4190. const thankEle = thankArea.find("> .thank");
  4191. const $hide = thankEle.eq(0).removeClass("thank");
  4192. const $thank = thankEle.eq(1).removeClass("thank");
  4193. $hide.html(
  4194. `<span class="v2p-control v2p-hover-btn v2p-control-hide"><i data-lucide="eye-off"></i></span>`
  4195. );
  4196. $thankIcon.addClass("v2p-hover-btn").replaceAll($thank);
  4197. $controls.append($hide).append($thankIcon);
  4198. }
  4199. const $reply = $actions.find("a:last-of-type");
  4200. $reply.find('> img[alt="Reply"]').replaceWith(
  4201. `<span class="v2p-control v2p-hover-btn v2p-control-reply"><i data-lucide="message-square"></i></span>`
  4202. );
  4203. $controls.append($reply);
  4204. thankArea.remove();
  4205. const floorNum = $actions.find(".no").clone();
  4206. $reply.on("click", () => {
  4207. const replyVal = $replyTextArea.val();
  4208. if (typeof replyVal === "string" && replyVal.length > 0) {
  4209. const floor = floorNum.text();
  4210. const replyComment = commentDataList.find((it) => it.floor === floor);
  4211. if (replyComment) {
  4212. const replyMemberName = replyComment.memberName;
  4213. const moreThanOneReply = commentDataList.findIndex(
  4214. (it) => it.memberName === replyMemberName && it.floor !== floor
  4215. ) !== -1;
  4216. if (moreThanOneReply) {
  4217. insertTextToReplyInput(`#${floor} `);
  4218. } else {
  4219. const $page = $(".v2p-paging").eq(0).find(".page_normal, .page_current");
  4220. if ($page.length > 1) {
  4221. const onLastPage = $page.last().hasClass("page_current");
  4222. if (!onLastPage) {
  4223. insertTextToReplyInput(`#${floor} `);
  4224. }
  4225. }
  4226. }
  4227. }
  4228. }
  4229. });
  4230. $actions.empty().append($controls, floorNum);
  4231. }
  4232. async function handleComments() {
  4233. const storage = getStorageSync();
  4234. const tagData = storage["member-tag" /* MemberTag */];
  4235. const options = storage["options" /* Options */];
  4236. if (options.reply.preload !== "off") {
  4237. const $paging = $(".v2p-paging");
  4238. if ($paging.length > 0) {
  4239. const $pagingTop = $paging.eq(0);
  4240. const $pagingBottom = $paging.eq(1);
  4241. const $pageNormal = $paging.find(".page_normal");
  4242. const $pagingTopNormal = $pagingTop.find(".page_normal");
  4243. const toastControl = createToast({ message: "\u6B63\u5728\u9884\u52A0\u8F7D\u56DE\u590D\uFF0C\u8BF7\u7A0D\u5019...", duration: 0 });
  4244. try {
  4245. const $pagingBottomNormal = $pagingBottom.find(".page_normal");
  4246. const $pageCurrent = $pagingTop.find(".page_current");
  4247. const currentPage = $pageCurrent.text();
  4248. if (currentPage === "1") {
  4249. const pages = [];
  4250. $pagingTopNormal.each((i, ele) => {
  4251. if (i <= 1) {
  4252. if (ele.textContent) {
  4253. ele.classList.add("page_current");
  4254. ele.classList.remove("page_normal");
  4255. $pagingBottomNormal.eq(i).addClass("page_current").removeClass("page_normal");
  4256. pages.push(ele.textContent);
  4257. }
  4258. }
  4259. });
  4260. if (pages.length > 0) {
  4261. const pagesText = await Promise.all(
  4262. pages.map((p) => crawlTopicPage(window.location.pathname, p))
  4263. );
  4264. pagesText.map((pageText) => {
  4265. $pagingBottom.before($(pageText).find('.cell[id^="r_"]'));
  4266. });
  4267. }
  4268. updateCommentCells();
  4269. }
  4270. toastControl.clear();
  4271. } catch (err) {
  4272. if (err instanceof Error) {
  4273. console.error(`\u52A0\u8F7D\u591A\u9875\u56DE\u590D\u51FA\u9519\uFF1A${err.message}`);
  4274. }
  4275. createToast({ message: "\u274C \u52A0\u8F7D\u591A\u9875\u56DE\u590D\u5931\u8D25" });
  4276. $pageNormal.removeClass("page_current").addClass("page_normal");
  4277. }
  4278. }
  4279. }
  4280. if (options.replyContent.hideReplyTime) {
  4281. $(".cell .ago").addClass("v2p-auto-hide");
  4282. }
  4283. const canHideRefName = options.nestedReply.display === "indent" && !!options.replyContent.hideRefName;
  4284. commentDataList = getCommentDataList({ options, $commentTableRows, $commentCells });
  4285. {
  4286. const memberNames = /* @__PURE__ */ new Set([topicOwnerName]);
  4287. $commentCells.each((i, cellDom) => {
  4288. const currentComment = commentDataList.at(i);
  4289. if (currentComment?.id !== cellDom.id) {
  4290. return;
  4291. }
  4292. const $cellDom = $(cellDom);
  4293. const { memberName, thanked } = currentComment;
  4294. memberNames.add(memberName);
  4295. processAvatar({
  4296. $trigger: $cellDom.find(".avatar, .dark"),
  4297. popupControl,
  4298. commentData: currentComment,
  4299. openInNewTab: options.openInNewTab
  4300. });
  4301. if (memberName === loginName && !options.hideAccount) {
  4302. $cellDom.find(".badges").append(`<div class="badge ${memberName === topicOwnerName ? "mod" : "you"}">YOU</div>`);
  4303. }
  4304. const $likesBox = $cellDom.find(".small.fade").addClass("v2p-likes-box");
  4305. $likesBox.find('img[alt="\u2764\uFE0F"]').replaceWith('<span class="v2p-icon-heart"><i data-lucide="heart"></i></span>');
  4306. if (thanked) {
  4307. $likesBox.addClass("v2p-thanked");
  4308. }
  4309. processActions($cellDom, currentComment);
  4310. if (canHideRefName) {
  4311. if (currentComment.contentHtml) {
  4312. $cellDom.find(".reply_content").html(currentComment.contentHtml);
  4313. }
  4314. }
  4315. });
  4316. if (tagData) {
  4317. Object.entries(tagData).forEach(([memberName, { tags, avatar }]) => {
  4318. if (memberNames.has(memberName)) {
  4319. updateMemberTag({ memberName, memberAvatar: avatar, tags, options });
  4320. }
  4321. });
  4322. }
  4323. updateCommentCells();
  4324. handleFilteredComments();
  4325. $(".v2p-control-thank").on("click", (ev) => {
  4326. ev.stopPropagation();
  4327. const id = ev.currentTarget.dataset.id;
  4328. const memberName = ev.currentTarget.dataset.memberName;
  4329. if (typeof id === "string" && typeof memberName === "string") {
  4330. if (confirm(`\u786E\u8BA4\u82B1\u8D39 10 \u4E2A\u94DC\u5E01\u5411 @${memberName} \u7684\u8FD9\u6761\u56DE\u590D\u53D1\u9001\u611F\u8C22\uFF1F`)) {
  4331. const replyId = id.split("_").at(1);
  4332. if (replyId) {
  4333. void thankReply({
  4334. replyId,
  4335. onSuccess: () => {
  4336. const $cell = $(`.cell[id="r_${replyId}"]`);
  4337. const $tableInCell = $cell.find("> table");
  4338. const $likesBox = $tableInCell.find(".v2p-likes-box");
  4339. const $firstLikesBox = $likesBox.eq(0);
  4340. const likes = Number($firstLikesBox.text());
  4341. const $clonedIconHeart = $firstLikesBox.find(".v2p-icon-heart").clone();
  4342. if (likes > 0) {
  4343. $likesBox.addClass("v2p-thanked").empty().append($clonedIconHeart, ` ${likes + 1}`);
  4344. } else {
  4345. $(`
  4346. <span class="small v2p-likes-box v2p-thanked" style="position:relative;top:-1px;">
  4347. &nbsp;&nbsp;<span class="v2p-icon-heart"><i data-lucide="heart"></i></span>1
  4348. </span>
  4349. `).insertAfter($tableInCell.find(".ago"));
  4350. loadIcons();
  4351. }
  4352. const $thankAction = $tableInCell.find(".v2p-control-thank");
  4353. $thankAction.addClass("v2p-thanked").off("click");
  4354. $thankAction.siblings().has(".v2p-control-hide").hide();
  4355. },
  4356. onFail: () => {
  4357. createToast({ message: "\u274C \u611F\u8C22\u56DE\u590D\u5931\u8D25" });
  4358. }
  4359. });
  4360. }
  4361. }
  4362. }
  4363. });
  4364. }
  4365. handleNestedComment({ options, $commentCells, commentDataList });
  4366. {
  4367. const $opAvatar = $topicHeader.find(".avatar");
  4368. const $opName = $topicHeader.find('.gray a[href^="/member"]');
  4369. const memberName = $opAvatar.prop("alt");
  4370. const memberAvatar = $opAvatar.prop("src");
  4371. const memberLink = $topicHeader.find(".fr > a").prop("href");
  4372. if (typeof memberName === "string" && typeof memberAvatar === "string" && typeof memberLink === "string") {
  4373. processAvatar({
  4374. $trigger: $opAvatar,
  4375. popupControl,
  4376. commentData: { memberName, memberAvatar, memberLink },
  4377. openInNewTab: options.openInNewTab
  4378. });
  4379. processAvatar({
  4380. $trigger: $opName,
  4381. popupControl,
  4382. commentData: { memberName, memberAvatar, memberLink },
  4383. shouldWrap: false,
  4384. openInNewTab: options.openInNewTab
  4385. });
  4386. fetchUserInfo(memberName).then((memberData) => {
  4387. const registerDays = getRegisterDays(memberData.created);
  4388. memberDataCache.set(memberName, { ...memberData, registerDays });
  4389. if (registerDays <= 30) {
  4390. $opName.append(
  4391. `<span class="v2p-register-days">${registerDays <= 15 ? "15" : "30"} \u5929\u5185\u6CE8\u518C</span>`
  4392. );
  4393. }
  4394. });
  4395. }
  4396. }
  4397. if (options.replyContent.showImgInPage) {
  4398. window.requestIdleCallback(() => {
  4399. handleTopicImg();
  4400. });
  4401. }
  4402. window.requestIdleCallback(() => {
  4403. replaceCommentEmojiWithHD();
  4404. handleLinkPreview();
  4405. });
  4406. }
  4407. var commentDataList, popupControl;
  4408. var init_comment = __esm({
  4409. "src/contents/topic/comment.ts"() {
  4410. "use strict";
  4411. init_lucide();
  4412. init_modal();
  4413. init_popup();
  4414. init_toast();
  4415. init_constants();
  4416. init_services();
  4417. init_utils();
  4418. init_dom();
  4419. init_globals();
  4420. init_helpers();
  4421. init_avatar();
  4422. init_content();
  4423. commentDataList = [];
  4424. popupControl = createPopup({
  4425. root: $wrapper,
  4426. triggerType: "hover",
  4427. placement: "right-start",
  4428. offsetOptions: { mainAxis: 8, crossAxis: -4 }
  4429. });
  4430. }
  4431. });
  4432.  
  4433. // src/contents/topic/layout.ts
  4434. function toggleTopicLayout() {
  4435. if ($wrapperContent.hasClass("v2p-content-layout")) {
  4436. switchToVerticalLayout();
  4437. } else {
  4438. switchToHorizontalLayout();
  4439. }
  4440. }
  4441. function handleLayout() {
  4442. const storage = getStorageSync();
  4443. const options = storage["options" /* Options */];
  4444. if (options.reply.layout === "auto") {
  4445. const contentHeight = $topicContentBox.height();
  4446. if (typeof contentHeight === "number" && contentHeight >= 600) {
  4447. switchToHorizontalLayout();
  4448. } else {
  4449. switchToVerticalLayout();
  4450. }
  4451. } else {
  4452. if (options.reply.layout === "horizontal") {
  4453. switchToHorizontalLayout();
  4454. } else {
  4455. switchToVerticalLayout();
  4456. }
  4457. }
  4458. $layoutToggle.on("click", () => {
  4459. toggleTopicLayout();
  4460. });
  4461. $(".tools").prepend($layoutToggle);
  4462. }
  4463. var $layoutToggle, iconLayoutV, iconLayoutH, switchToHorizontalLayout, switchToVerticalLayout;
  4464. var init_layout = __esm({
  4465. "src/contents/topic/layout.ts"() {
  4466. "use strict";
  4467. init_lucide();
  4468. init_constants();
  4469. init_utils();
  4470. init_globals();
  4471. $layoutToggle = $('<span class="v2p-layout-toggle v2p-hover-btn">');
  4472. iconLayoutV = createElement$1(PanelTop);
  4473. iconLayoutV.setAttribute("width", "100%");
  4474. iconLayoutV.setAttribute("height", "100%");
  4475. iconLayoutH = createElement$1(PanelRight);
  4476. iconLayoutH.setAttribute("width", "100%");
  4477. iconLayoutH.setAttribute("height", "100%");
  4478. switchToHorizontalLayout = () => {
  4479. if (!$wrapperContent.hasClass("v2p-content-layout")) {
  4480. const $divider1 = $main.find("> .sep20:first-of-type");
  4481. const $leftGroup = $divider1.add($divider1.next(".box"));
  4482. const $leftSide = $('<div class="v2p-left-side">');
  4483. $leftGroup.wrapAll($leftSide);
  4484. const $content = $leftGroup.find("> .cell");
  4485. $content.add($content.nextAll(".subtle")).wrapAll('<div class="v2p-left-side-content">');
  4486. const $divider2 = $main.find(".sep20:nth-of-type(2)");
  4487. const $rightGroup = $divider2.add($divider2.nextAll());
  4488. $rightGroup.wrapAll('<div class="v2p-right-side">');
  4489. $wrapperContent.addClass("v2p-content-layout");
  4490. $main.addClass("v2p-horizontal-layout");
  4491. }
  4492. $layoutToggle.html(iconLayoutV);
  4493. $layoutToggle.attr("title", "\u5207\u6362\u4E3A\u5782\u76F4\u5E03\u5C40");
  4494. $(".v2p-reply-tool-layout").text("\u5207\u6362\u4E3A\u5782\u76F4\u5E03\u5C40");
  4495. };
  4496. switchToVerticalLayout = () => {
  4497. if ($wrapperContent.hasClass("v2p-content-layout")) {
  4498. $wrapperContent.removeClass("v2p-content-layout");
  4499. $main.removeClass("v2p-horizontal-layout");
  4500. $(".v2p-left-side-content").children().unwrap();
  4501. $(".v2p-left-side").children().unwrap();
  4502. $(".v2p-right-side").children().unwrap();
  4503. }
  4504. $layoutToggle.html(iconLayoutH);
  4505. $layoutToggle.attr("title", "\u5207\u6362\u4E3A\u6C34\u5E73\u5E03\u5C40");
  4506. $(".v2p-reply-tool-layout").text("\u5207\u6362\u4E3A\u6C34\u5E73\u5E03\u5C40");
  4507. };
  4508. }
  4509. });
  4510.  
  4511. // src/contents/topic/paging.ts
  4512. function handlePaging() {
  4513. const $notCommentCells = $commentBox.find('> .cell:not([id^="r_"])');
  4514. if ($notCommentCells.length <= 1) {
  4515. return;
  4516. }
  4517. const pagingCells = $notCommentCells.slice(1).addClass("v2p-paging");
  4518. const pageBtns = pagingCells.find(".super.button");
  4519. pageBtns.eq(0).addClass("v2p-prev-btn");
  4520. pageBtns.eq(1).addClass("v2p-next-btn");
  4521. }
  4522. var init_paging = __esm({
  4523. "src/contents/topic/paging.ts"() {
  4524. "use strict";
  4525. init_globals();
  4526. }
  4527. });
  4528.  
  4529. // src/components/image-upload.ts
  4530. function bindImageUpload(props) {
  4531. const { $wrapper: $wrapper2, $input, insertText, replaceText } = props;
  4532. const $uploadBar = $(`<div class="v2p-reply-upload-bar">${uploadTip}</div>`);
  4533. const handleUploadImage = (file) => {
  4534. const placeholder = "[\u4E0A\u4F20\u56FE\u7247\u4E2D...]";
  4535. insertText(` ${placeholder} `);
  4536. $uploadBar.addClass("v2p-reply-upload-bar-disabled").text("\u6B63\u5728\u4E0A\u4F20\u56FE\u7247...");
  4537. uploadImage(file).then((imgLink) => {
  4538. replaceText(placeholder, imgLink);
  4539. }).catch(() => {
  4540. replaceText(placeholder, "");
  4541. window.alert("\u274C \u4E0A\u4F20\u56FE\u7247\u5931\u8D25\uFF0C\u8BF7\u6253\u5F00\u63A7\u5236\u53F0\u67E5\u770B\u539F\u56E0");
  4542. }).finally(() => {
  4543. $uploadBar.removeClass("v2p-reply-upload-bar-disabled").text(uploadTip);
  4544. });
  4545. };
  4546. const handleClickUploadImage = () => {
  4547. const imgInput = document.createElement("input");
  4548. imgInput.style.display = "none";
  4549. imgInput.type = "file";
  4550. imgInput.accept = "image/*";
  4551. imgInput.addEventListener("change", () => {
  4552. const selectedFile = imgInput.files?.[0];
  4553. if (selectedFile) {
  4554. handleUploadImage(selectedFile);
  4555. }
  4556. });
  4557. imgInput.click();
  4558. };
  4559. document.addEventListener("paste", (ev) => {
  4560. if (!(ev instanceof ClipboardEvent)) {
  4561. return;
  4562. }
  4563. if ($input && !$input.get(0)?.matches(":focus")) {
  4564. return;
  4565. }
  4566. const items = ev.clipboardData?.items;
  4567. if (!items) {
  4568. return;
  4569. }
  4570. const imageItem = Array.from(items).find((item) => item.type.includes("image"));
  4571. if (imageItem) {
  4572. const file = imageItem.getAsFile();
  4573. if (file) {
  4574. handleUploadImage(file);
  4575. }
  4576. }
  4577. });
  4578. $wrapper2.get(0)?.addEventListener("drop", (ev) => {
  4579. ev.preventDefault();
  4580. if (!(ev instanceof DragEvent)) {
  4581. return;
  4582. }
  4583. const file = ev.dataTransfer?.files[0];
  4584. if (file) {
  4585. handleUploadImage(file);
  4586. }
  4587. });
  4588. $(".flex-one-row:last-of-type > .gray").text("");
  4589. $uploadBar.on("click", () => {
  4590. if (!$uploadBar.hasClass("v2p-reply-upload-bar-disabled")) {
  4591. handleClickUploadImage();
  4592. }
  4593. });
  4594. $wrapper2.append($uploadBar);
  4595. return {
  4596. uploadBar: $uploadBar
  4597. };
  4598. }
  4599. var uploadTip;
  4600. var init_image_upload = __esm({
  4601. "src/components/image-upload.ts"() {
  4602. "use strict";
  4603. init_services();
  4604. uploadTip = "\u9009\u62E9\u3001\u7C98\u8D34\u3001\u62D6\u653E\u4E0A\u4F20\u56FE\u7247\u3002";
  4605. }
  4606. });
  4607.  
  4608. // src/contents/topic/reply.ts
  4609. function handleReplyActions() {
  4610. const os = getOS();
  4611. const replyBtnText = `\u56DE\u590D<kbd>${os === "macos" ? "Cmd" : "Ctrl"}+Enter</kbd>`;
  4612. const $replyBtn = createButton({
  4613. children: replyBtnText,
  4614. type: "submit"
  4615. }).replaceAll($replyBox.find('input[type="submit"]'));
  4616. $replyForm.on("submit", () => {
  4617. const replyVal = $replyTextArea.val();
  4618. if (typeof replyVal === "string") {
  4619. $replyTextArea.val(transformEmoji(replyVal));
  4620. }
  4621. $replyBtn.text("\u63D0\u4EA4\u56DE\u590D\u4E2D...").prop("disabled", true);
  4622. setTimeout(() => {
  4623. $replyBtn.html(replyBtnText).prop("disabled", false);
  4624. }, 5e3);
  4625. });
  4626. document.addEventListener("keydown", (ev) => {
  4627. if (ev.key === "Enter" && (ev.ctrlKey || ev.metaKey)) {
  4628. ev.preventDefault();
  4629. $replyForm.trigger("submit");
  4630. }
  4631. });
  4632. {
  4633. const emoticonGroup = $('<div class="v2p-emoji-group">');
  4634. const emoticonList = $('<div class="v2p-emoji-list">');
  4635. const emoticonSpan = $('<span class="v2p-emoji">');
  4636. const groups = emoticons.map((emojiGroup) => {
  4637. const group = emoticonGroup.clone();
  4638. const list = emoticonList.clone();
  4639. group.append(`<div class="v2p-emoji-title">${emojiGroup.title}</div>`);
  4640. list.append(
  4641. emojiGroup.list.map((emoji) => {
  4642. const emoticon = emoticonSpan.clone();
  4643. if (emojiGroup.title === "\u6D41\u884C") {
  4644. const emojiLink = emojiLinks[emoji].hd;
  4645. emoticon.html(`<img src="${emojiLink}" />`).prop("title", emoji);
  4646. } else {
  4647. emoticon.text(emoji);
  4648. }
  4649. emoticon.on("click", () => {
  4650. insertTextToReplyInput(emoji);
  4651. });
  4652. return emoticon;
  4653. })
  4654. );
  4655. group.append(list);
  4656. return group;
  4657. });
  4658. const emoticonsBox = $('<div class="v2p-emoticons-box">').append(groups);
  4659. const $emojiBtn = createButton({
  4660. children: '<span style="width:18px; height:18px;"><i data-lucide="smile"></i></span>'
  4661. }).insertAfter($replyBtn);
  4662. const $emojiContent = $('<div class="v2p-emoji-container">').append(emoticonsBox).appendTo($replyBox).on("click", () => {
  4663. focusReplyInput();
  4664. });
  4665. const keyupHandler = (ev) => {
  4666. if (ev.key === "Escape") {
  4667. ev.preventDefault();
  4668. emojiPopup.close();
  4669. }
  4670. };
  4671. $emojiBtn.on("click", () => {
  4672. focusReplyInput();
  4673. });
  4674. const emojiPopup = createPopup({
  4675. root: $replyBox,
  4676. trigger: $emojiBtn,
  4677. content: $emojiContent,
  4678. options: { placement: "right-end" },
  4679. onOpen: () => {
  4680. $body.on("keydown", keyupHandler);
  4681. },
  4682. onClose: () => {
  4683. $body.off("keydown", keyupHandler);
  4684. }
  4685. });
  4686. }
  4687. {
  4688. $replyBox.find("#undock-button, #undock-button + a").addClass("v2p-hover-btn").css("padding", "5px 4px");
  4689. }
  4690. }
  4691. function handleReply() {
  4692. $replyTextArea.attr("placeholder", "\u7559\u4E0B\u5BF9\u4ED6\u4EBA\u6709\u5E2E\u52A9\u7684\u56DE\u590D").wrap('<div class="v2p-reply-wrap">');
  4693. const $replyWrap = $(".v2p-reply-wrap");
  4694. const $replyPreview = $('<div class="v2p-reply-preview">');
  4695. $replyPreview.hide().insertAfter($replyWrap);
  4696. bindImageUpload({
  4697. $wrapper: $replyWrap,
  4698. $input: $replyTextArea,
  4699. insertText: (text) => {
  4700. insertTextToReplyInput(text);
  4701. },
  4702. replaceText: (find, replace) => {
  4703. const val = $replyTextArea.val();
  4704. if (typeof val === "string") {
  4705. const newVal = val.replace(find, replace);
  4706. $replyTextArea.val(newVal).trigger("focus");
  4707. }
  4708. }
  4709. });
  4710. {
  4711. const $replyTabs = $('<div class="v2p-reply-tabs">');
  4712. const $replyTabEdit = $('<div class="v2p-reply-tab active">\u7F16\u8F91</div>');
  4713. const $replyTabPreview = $('<div class="v2p-reply-tab">\u9884\u89C8</div>');
  4714. $replyTabEdit.on("click", () => {
  4715. $replyTabEdit.addClass("active");
  4716. $replyTabPreview.removeClass("active");
  4717. $replyWrap.show();
  4718. $replyPreview.hide();
  4719. }).appendTo($replyTabs);
  4720. let lastPreviewText = null;
  4721. $replyTabPreview.on("click", () => {
  4722. if (!$replyTabPreview.hasClass("active")) {
  4723. $replyTabPreview.addClass("active");
  4724. $replyTabEdit.removeClass("active");
  4725. const replyText = $replyTextArea.val();
  4726. if (typeof replyText === "string") {
  4727. $replyWrap.hide();
  4728. $replyPreview.show();
  4729. if (replyText.trim() === "") {
  4730. $replyPreview.html("\u6CA1\u6709\u53EF\u9884\u89C8\u7684\u5185\u5BB9");
  4731. } else {
  4732. const textToPreview = transformEmoji(replyText);
  4733. const handlePreview = async () => {
  4734. $replyPreview.html("\u6B63\u5728\u52A0\u8F7D\u9884\u89C8...");
  4735. try {
  4736. const renderedContent = await getCommentPreview({
  4737. text: textToPreview,
  4738. syntax: "default"
  4739. });
  4740. $replyPreview.html(renderedContent);
  4741. replaceEmojiWithHD($replyPreview.find(".embedded_image"));
  4742. lastPreviewText = textToPreview;
  4743. } catch {
  4744. $replyPreview.html('\u9884\u89C8\u5931\u8D25\uFF0C<a class="v2p-preview-retry">\u70B9\u51FB\u91CD\u8BD5</a>\u3002');
  4745. $replyPreview.find(".v2p-preview-retry").on("click", () => {
  4746. void handlePreview();
  4747. });
  4748. }
  4749. };
  4750. if (replyText !== lastPreviewText) {
  4751. void handlePreview();
  4752. }
  4753. }
  4754. }
  4755. }
  4756. }).appendTo($replyTabs);
  4757. $replyBox.find("> .cell:first-of-type > div:first-of-type").replaceWith($replyTabs);
  4758. }
  4759. $(".flex-one-row:last-of-type > .gray").text("");
  4760. handleReplyActions();
  4761. }
  4762. var init_reply = __esm({
  4763. "src/contents/topic/reply.ts"() {
  4764. "use strict";
  4765. init_button();
  4766. init_image_upload();
  4767. init_popup();
  4768. init_constants();
  4769. init_services();
  4770. init_utils();
  4771. init_globals();
  4772. init_helpers();
  4773. }
  4774. });
  4775.  
  4776. // src/contents/topic/tool.ts
  4777. function handleTools() {
  4778. const storage = getStorageSync();
  4779. const options = storage["options" /* Options */];
  4780. const $tools = $(`
  4781. <div class="cell v2p-tools">
  4782. <span class="v2p-tool v2p-hover-btn v2p-tool-reply">
  4783. <span class="v2p-tool-icon"><i data-lucide="message-square-plus"></i></span>\u56DE\u590D\u4E3B\u9898
  4784. </span>
  4785. <span class="v2p-tool v2p-hover-btn v2p-tool-reading">
  4786. <span class="v2p-tool-icon"><i data-lucide="book-open-check"></i></span>\u7A0D\u540E\u9605\u8BFB
  4787. </span>
  4788. <span class="v2p-tool v2p-hover-btn v2p-tool-scroll-top">
  4789. <span class="v2p-tool-icon"><i data-lucide="chevrons-up"></i></span>\u56DE\u5230\u9876\u90E8
  4790. </span>
  4791. <span class="v2p-tool v2p-hover-btn v2p-tool-more">
  4792. <span class="v2p-tool-icon"><i data-lucide="package-plus"></i></span>\u66F4\u591A\u529F\u80FD
  4793. </span>
  4794. </div>
  4795. `);
  4796. $tools.find(".v2p-tool-reply").on("click", () => {
  4797. $replyTextArea.trigger("focus");
  4798. });
  4799. $tools.find(".v2p-tool-reading").on("click", () => {
  4800. void addToReadingList({
  4801. url: window.location.href,
  4802. title: document.title.replace(" - V2EX", ""),
  4803. content: String($('head meta[property="og:description"]').prop("content"))
  4804. });
  4805. });
  4806. $tools.find(".v2p-tool-scroll-top").on("click", () => {
  4807. window.scrollTo({ top: 0, behavior: "smooth" });
  4808. });
  4809. {
  4810. const $moreTool = $tools.find(".v2p-tool-more");
  4811. const $toolContent = $(`
  4812. <div class="v2p-select-dropdown">
  4813. <div class="v2p-select-item v2p-reply-tool-decode">\u89E3\u6790\u672C\u9875 Base64</div>
  4814. <div class="v2p-select-item v2p-reply-tool-encode">\u6587\u672C\u8F6C Base64</div>
  4815. <div class="v2p-select-item v2p-reply-tool-share">\u751F\u6210\u5206\u4EAB\u56FE\u7247</div>
  4816. </div>
  4817. `);
  4818. const toolsPopup = createPopup({
  4819. root: $replyBox,
  4820. trigger: $moreTool,
  4821. content: $toolContent,
  4822. offsetOptions: { mainAxis: 5, crossAxis: -5 }
  4823. });
  4824. $toolContent.find(".v2p-reply-tool-decode").on("click", () => {
  4825. decodeBase64TopicPage();
  4826. });
  4827. $toolContent.find(".v2p-reply-tool-encode").on("click", () => {
  4828. focusReplyInput();
  4829. setTimeout(() => {
  4830. const inputText = window.prompt("\u8F93\u5165\u8981\u52A0\u5BC6\u7684\u5B57\u7B26\u4E32\uFF0C\u5B8C\u6210\u540E\u5C06\u586B\u5199\u5230\u56DE\u590D\u6846\u4E2D\uFF1A");
  4831. if (inputText) {
  4832. let encodedText;
  4833. try {
  4834. encodedText = window.btoa(encodeURIComponent(inputText));
  4835. } catch (err) {
  4836. const errorTip = "\u8BE5\u6587\u672C\u65E0\u6CD5\u7F16\u7801\u4E3A Base64";
  4837. console.error(err, `${errorTip}\uFF0C\u53EF\u80FD\u7684\u9519\u8BEF\u539F\u56E0\uFF1A\u6587\u672C\u5305\u542B\u4E2D\u6587\u3002`);
  4838. createToast({ message: errorTip });
  4839. }
  4840. if (encodedText) {
  4841. insertTextToReplyInput(encodedText);
  4842. }
  4843. }
  4844. });
  4845. });
  4846. $toolContent.find(".v2p-reply-tool-share").on("click", () => {
  4847. if (pathTopicId) {
  4848. window.open(`${"https://v2p.app" /* Home */}/share/${pathTopicId}`, "_blank");
  4849. }
  4850. });
  4851. const canHideRefName = options.nestedReply.display === "indent" && !!options.replyContent.hideRefName;
  4852. if (canHideRefName) {
  4853. let isHidden = options.replyContent.hideRefName;
  4854. const $toolToggleDisplay = $('<div class="v2p-select-item">\u663E\u793A @ \u7528\u6237\u540D</div>');
  4855. $toolToggleDisplay.on("click", () => {
  4856. if (isHidden) {
  4857. isHidden = false;
  4858. $toolToggleDisplay.text("\u9690\u85CF @ \u7528\u6237\u540D");
  4859. $(".v2p-member-ref").addClass("v2p-member-ref-show");
  4860. } else {
  4861. isHidden = true;
  4862. $toolToggleDisplay.text("\u663E\u793A @ \u7528\u6237\u540D");
  4863. $(".v2p-member-ref").removeClass("v2p-member-ref-show");
  4864. }
  4865. });
  4866. $toolContent.prepend($toolToggleDisplay);
  4867. }
  4868. const $toolToggleLayout = $(
  4869. `
  4870. <div class="v2p-select-item v2p-reply-tool-layout">
  4871. ${options.reply.layout === "horizontal" ? "\u5207\u6362\u4E3A\u5782\u76F4\u5E03\u5C40" : "\u5207\u6362\u4E3A\u6C34\u5E73\u5E03\u5C40"}
  4872. </div>
  4873. `
  4874. );
  4875. $toolToggleLayout.on("click", () => {
  4876. toggleTopicLayout();
  4877. });
  4878. $toolContent.prepend($toolToggleLayout);
  4879. $toolContent.find(".v2p-select-item").on("click", () => {
  4880. toolsPopup.close();
  4881. });
  4882. }
  4883. $infoCard.addClass("v2p-tool-box").append($tools);
  4884. loadIcons();
  4885. }
  4886. var init_tool = __esm({
  4887. "src/contents/topic/tool.ts"() {
  4888. "use strict";
  4889. init_popup();
  4890. init_toast();
  4891. init_constants();
  4892. init_utils();
  4893. init_globals();
  4894. init_helpers();
  4895. init_layout();
  4896. }
  4897. });
  4898.  
  4899. // src/contents/topic/index.ts
  4900. var topic_exports = {};
  4901. var init_topic = __esm({
  4902. "src/contents/topic/index.ts"() {
  4903. "use strict";
  4904. init_constants();
  4905. init_utils();
  4906. init_globals();
  4907. init_helpers();
  4908. init_comment();
  4909. init_content();
  4910. init_layout();
  4911. init_paging();
  4912. init_reply();
  4913. init_tool();
  4914. void (async () => {
  4915. const storage = await getStorage();
  4916. const options = storage["options" /* Options */];
  4917. handleLayout();
  4918. if (options.openInNewTab) {
  4919. $topicHeader.find('a[href^="/member/"]').prop("target", "_blank");
  4920. $commentTableRows.find("> td:nth-child(3) > strong > a").prop("target", "_blank");
  4921. }
  4922. handleTools();
  4923. {
  4924. $(document).on("keydown", (ev) => {
  4925. if (!ev.isDefaultPrevented()) {
  4926. if (ev.key === "Escape") {
  4927. const $replyContent = $("#reply_content");
  4928. if ($replyBox.hasClass("reply-box-sticky")) {
  4929. $replyBox.removeClass("reply-box-sticky");
  4930. $("#undock-button").css("display", "none");
  4931. }
  4932. $replyContent.trigger("blur");
  4933. }
  4934. }
  4935. });
  4936. }
  4937. handleContent();
  4938. if (document.referrer !== "") {
  4939. if (document.referrer.includes(document.location.pathname)) {
  4940. const url = new URL(document.location.href);
  4941. const page = url.searchParams.get("p");
  4942. if (page && page !== "1") {
  4943. document.querySelector(".topic_buttons")?.scrollIntoView({ behavior: "smooth" });
  4944. }
  4945. }
  4946. }
  4947. handlePaging();
  4948. await handleComments();
  4949. handleReply();
  4950. loadIcons();
  4951. {
  4952. const isFlamewarNode = $topicHeader.find('a[href^="/go"]').attr("href")?.endsWith("/flamewar");
  4953. if (isFlamewarNode) {
  4954. $("#node_sidebar").addClass("v2p-node-sidebar-flamewar");
  4955. }
  4956. }
  4957. })();
  4958. }
  4959. });
  4960.  
  4961. // src/contents/write/write.ts
  4962. function handleWrite() {
  4963. bindImageUpload({
  4964. $wrapper: $("#workspace"),
  4965. insertText: (text) => {
  4966. postTask(`editor.getDoc().replaceRange("${text}", editor.getCursor())`);
  4967. },
  4968. replaceText: (find, replace) => {
  4969. if (replace) {
  4970. const mode = $("input[name=syntax]:checked").val();
  4971. if (mode === "markdown") {
  4972. replace = `![](${replace})`;
  4973. }
  4974. }
  4975. postTask(`
  4976. editor.setValue(editor.getValue().replace("${find}", "${replace}"));
  4977. const doc = editor.getDoc();
  4978. const lastLine = doc.lastLine();
  4979. const lastChar = doc.getLine(lastLine).length;
  4980. doc.setCursor({ line: doc.lastLine(), ch: lastChar });
  4981. `);
  4982. }
  4983. });
  4984. }
  4985. var init_write = __esm({
  4986. "src/contents/write/write.ts"() {
  4987. "use strict";
  4988. init_image_upload();
  4989. init_helpers();
  4990. }
  4991. });
  4992.  
  4993. // src/contents/write/index.ts
  4994. var write_exports = {};
  4995. var init_write2 = __esm({
  4996. "src/contents/write/index.ts"() {
  4997. "use strict";
  4998. init_helpers();
  4999. init_write();
  5000. handleWrite();
  5001. loadIcons();
  5002. }
  5003. });
  5004.  
  5005. // node_modules/.pnpm/webext-patterns@1.5.0/node_modules/webext-patterns/index.js
  5006. var patternValidationRegex = /^(https?|wss?|file|ftp|\*):\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^file:\/\/\/.*$|^resource:\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^about:/;
  5007. var isFirefox = globalThis.navigator?.userAgent.includes("Firefox/");
  5008. var allStarsRegex = isFirefox ? /^(https?|wss?):[/][/][^/]+([/].*)?$/ : /^https?:[/][/][^/]+([/].*)?$/;
  5009. var allUrlsRegex = /^(https?|file|ftp):[/]+/;
  5010. function assertValidPattern(matchPattern) {
  5011. if (!isValidPattern(matchPattern)) {
  5012. throw new Error(matchPattern + " is an invalid pattern. See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns for more info.");
  5013. }
  5014. }
  5015. function isValidPattern(matchPattern) {
  5016. return matchPattern === "<all_urls>" || patternValidationRegex.test(matchPattern);
  5017. }
  5018. function getRawPatternRegex(matchPattern) {
  5019. assertValidPattern(matchPattern);
  5020. let [, protocol, host = "", pathname] = matchPattern.split(/(^[^:]+:[/][/])([^/]+)?/);
  5021. protocol = protocol.replace("*", isFirefox ? "(https?|wss?)" : "https?").replaceAll(/[/]/g, "[/]");
  5022. if (host === "*") {
  5023. host = "[^/]+";
  5024. }
  5025. host &&= host.replace(/^[*][.]/, "([^/]+.)*").replaceAll(/[.]/g, "[.]").replace(/[*]$/, "[^.]+");
  5026. pathname = pathname.replaceAll(/[/]/g, "[/]").replaceAll(/[.]/g, "[.]").replaceAll(/[*]/g, ".*");
  5027. return "^" + protocol + host + "(" + pathname + ")?$";
  5028. }
  5029. function patternToRegex(...matchPatterns) {
  5030. if (matchPatterns.length === 0) {
  5031. return /$./;
  5032. }
  5033. if (matchPatterns.includes("<all_urls>")) {
  5034. return allUrlsRegex;
  5035. }
  5036. if (matchPatterns.includes("*://*/*")) {
  5037. return allStarsRegex;
  5038. }
  5039. return new RegExp(matchPatterns.map((x) => getRawPatternRegex(x)).join("|"));
  5040. }
  5041.  
  5042. // src/user-scripts/style.ts
  5043. var style = `:root{--zidx-serach: 100;--zidx-tabs: 10;--zidx-tools-card: 10;--zidx-reply-box: 99;--zidx-modal-header: 50;--zidx-modal-mask: 888;--zidx-toast: 999;--zidx-tip: 99;--zidx-popup: 99;--zidx-expand-mask: 10;--zidx-expand-btn: 20;--v2p-underline-offset: 0.5ex;--v2p-layout-column-gap: 25px;--v2p-layout-row-gap: 20px;--v2p-nav-height: 55px;--v2p-tp-item-x: 20px;--v2p-tp-item-y: 10px;--v2p-tp-tabs-pd: 10px;--v2p-tp-nested-pd: 15px;--v2p-tp-preview-pd: 20px;--v2p-emoji-size: 21px;--v2p-box-radius: 10px}:root body{--v2p-color-main-50: #f7f9fb;--v2p-color-main-100: #f1f5f9;--v2p-color-main-200: #e2e8f0;--v2p-color-main-300: #cbd5e1;--v2p-color-main-350: #94a3b8cc;--v2p-color-main-400: #94a3b8;--v2p-color-main-500: #64748b;--v2p-color-main-600: #475569;--v2p-color-main-700: #334155;--v2p-color-main-800: #1e293b;--v2p-color-accent-50: #ecfdf5;--v2p-color-accent-100: #d1fae5;--v2p-color-accent-200: #a7f3d0;--v2p-color-accent-300: #6ee7b7;--v2p-color-accent-400: #34d399;--v2p-color-accent-500: #10b981;--v2p-color-accent-600: #059669;--v2p-color-orange-50: #fff7ed;--v2p-color-orange-100: #ffedd5;--v2p-color-orange-400: #fb923c;--v2p-color-background: #f2f3f5;--v2p-color-foreground: var(--v2p-color-main-800);--v2p-color-selection-foreground: var(--v2p-color-main-100);--v2p-color-selection-background: var(--v2p-color-main-700);--v2p-color-selection-background-img: var(--v2p-color-main-500);--v2p-color-font-secondary: var(--v2p-color-main-600);--v2p-color-font-tertiary: var(--v2p-color-main-500);--v2p-color-font-quaternary: var(--v2p-color-main-400);--v2p-color-button-background: var(--v2p-color-main-100);--v2p-color-button-foreground: var(--v2p-color-main-500);--v2p-color-button-background-hover: var(--v2p-color-main-200);--v2p-color-button-foreground-hover: var(--v2p-color-main-600);--v2p-color-bg-content: #fff;--v2p-color-bg-footer: var(--v2p-color-bg-content);--v2p-color-bg-hover-btn: rgb(226 232 240 / 70%);--v2p-color-bg-subtle: rgb(236 253 245 / 90%);--v2p-color-bg-input: var(--v2p-color-main-50);--v2p-color-bg-search: var(--v2p-color-main-100);--v2p-color-bg-search-active: var(--v2p-color-main-200);--v2p-color-bg-widget: rgb(255 255 255 / 70%);--v2p-color-bg-reply: var(--v2p-color-main-100);--v2p-color-bg-tooltip: var(--v2p-color-bg-content);--v2p-color-bg-tabs: var(--v2p-color-main-100);--v2p-color-bg-tpr: var(--v2p-color-main-100);--v2p-color-bg-avatar: var(--v2p-color-main-300);--v2p-color-bg-block: var(--v2p-color-main-100);--v2p-color-bg-block-darker: var(--v2p-color-main-300);--v2p-color-bg-link: var(--v2p-color-main-100);--v2p-color-bg-link-hover: var(--v2p-color-main-200);--v2p-color-tabs: var(--v2p-color-foreground);--v2p-color-heart: #ef4444;--v2p-color-heart-fill: #fee2e2;--v2p-color-mask: rgb(0 0 0 / 25%);--v2p-color-divider: var(--v2p-color-main-200);--v2p-color-border: var(--v2p-color-main-200);--v2p-color-input-border: var(--v2p-color-main-300);--v2p-color-border-darker: var(--v2p-color-main-300);--v2p-color-link-visited: var(--v2p-color-main-400);--v2p-color-error: #ef4444;--v2p-color-bg-error: #fee2e2;--v2p-color-cell-num: var(--v2p-color-main-350);--v2p-box-shadow: 0 3px 5px 0 rgb(0 0 0 / 4%);--v2p-widget-shadow: 0 9px 24px -3px rgb(0 0 0 / 6%), 0 4px 8px -1px rgb(0 0 0 /12%);--v2p-toast-shadow: 0 6px 16px 0 rgb(0 0 0 / 8%), 0 3px 6px -4px rgb(0 0 0 / 12%), 0 9px 28px 8px rgb(0 0 0 / 5%);--color-fade: var(--v2p-color-font-secondary);--color-gray: var(--v2p-color-font-secondary);--link-color: var(--v2p-color-foreground);--link-darker-color: var(--v2p-color-main-600);--link-hover-color: var(--v2p-color-foreground);--link-caution-color: var(--v2p-color-orange-400);--box-border-color: var(--v2p-color-border);--box-foreground-color: var(--v2p-color-foreground);--box-background-color: var(--v2p-color-bg-content);--box-background-alt-color: var(--v2p-color-main-100);--box-background-hover-color: var(--v2p-color-main-200);--box-border-focus-color: var(--v2p-color-main-200);--box-border-radius: var(--v2p-box-radius);--button-background-color: var(--v2p-color-button-background);--button-foreground-color: var(--v2p-color-button-foreground);--button-hover-color: var(--v2p-color-button-background-hover);--button-background-hover-color: var(--v2p-color-button-background-hover);--button-foreground-hover-color: var(--v2p-color-button-foreground-hover);--button-border-color: var(--v2p-color-main-300);--button-border-hover-color: var(--v2p-color-main-400);font-family:system-ui,sans-serif;color:var(--v2p-color-foreground);background-color:var(--v2p-color-background)}:root body.v2p-theme-light-default,:root body .v2p-theme-light-default{--v2p-color-main-50: #f7f9fb;--v2p-color-main-100: #f1f5f9;--v2p-color-main-200: #e2e8f0;--v2p-color-main-300: #cbd5e1;--v2p-color-main-350: #94a3b8cc;--v2p-color-main-400: #94a3b8;--v2p-color-main-500: #64748b;--v2p-color-main-600: #475569;--v2p-color-main-700: #334155;--v2p-color-main-800: #1e293b;--v2p-color-accent-50: #ecfdf5;--v2p-color-accent-100: #d1fae5;--v2p-color-accent-200: #a7f3d0;--v2p-color-accent-300: #6ee7b7;--v2p-color-accent-400: #34d399;--v2p-color-accent-500: #10b981;--v2p-color-accent-600: #059669;--v2p-color-orange-50: #fff7ed;--v2p-color-orange-100: #ffedd5;--v2p-color-orange-400: #fb923c;--v2p-color-background: #f2f3f5;--v2p-color-foreground: var(--v2p-color-main-800);--v2p-color-selection-foreground: var(--v2p-color-main-100);--v2p-color-selection-background: var(--v2p-color-main-700);--v2p-color-selection-background-img: var(--v2p-color-main-500);--v2p-color-font-secondary: var(--v2p-color-main-600);--v2p-color-font-tertiary: var(--v2p-color-main-500);--v2p-color-font-quaternary: var(--v2p-color-main-400);--v2p-color-button-background: var(--v2p-color-main-100);--v2p-color-button-foreground: var(--v2p-color-main-500);--v2p-color-button-background-hover: var(--v2p-color-main-200);--v2p-color-button-foreground-hover: var(--v2p-color-main-600);--v2p-color-bg-content: #fff;--v2p-color-bg-footer: var(--v2p-color-bg-content);--v2p-color-bg-hover-btn: rgb(226 232 240 / 70%);--v2p-color-bg-subtle: rgb(236 253 245 / 90%);--v2p-color-bg-input: var(--v2p-color-main-50);--v2p-color-bg-search: var(--v2p-color-main-100);--v2p-color-bg-search-active: var(--v2p-color-main-200);--v2p-color-bg-widget: rgb(255 255 255 / 70%);--v2p-color-bg-reply: var(--v2p-color-main-100);--v2p-color-bg-tooltip: var(--v2p-color-bg-content);--v2p-color-bg-tabs: var(--v2p-color-main-100);--v2p-color-bg-tpr: var(--v2p-color-main-100);--v2p-color-bg-avatar: var(--v2p-color-main-300);--v2p-color-bg-block: var(--v2p-color-main-100);--v2p-color-bg-block-darker: var(--v2p-color-main-300);--v2p-color-bg-link: var(--v2p-color-main-100);--v2p-color-bg-link-hover: var(--v2p-color-main-200);--v2p-color-tabs: var(--v2p-color-foreground);--v2p-color-heart: #ef4444;--v2p-color-heart-fill: #fee2e2;--v2p-color-mask: rgb(0 0 0 / 25%);--v2p-color-divider: var(--v2p-color-main-200);--v2p-color-border: var(--v2p-color-main-200);--v2p-color-input-border: var(--v2p-color-main-300);--v2p-color-border-darker: var(--v2p-color-main-300);--v2p-color-link-visited: var(--v2p-color-main-400);--v2p-color-error: #ef4444;--v2p-color-bg-error: #fee2e2;--v2p-color-cell-num: var(--v2p-color-main-350);--v2p-box-shadow: 0 3px 5px 0 rgb(0 0 0 / 4%);--v2p-widget-shadow: 0 9px 24px -3px rgb(0 0 0 / 6%), 0 4px 8px -1px rgb(0 0 0 /12%);--v2p-toast-shadow: 0 6px 16px 0 rgb(0 0 0 / 8%), 0 3px 6px -4px rgb(0 0 0 / 12%), 0 9px 28px 8px rgb(0 0 0 / 5%);--color-fade: var(--v2p-color-font-secondary);--color-gray: var(--v2p-color-font-secondary);--link-color: var(--v2p-color-foreground);--link-darker-color: var(--v2p-color-main-600);--link-hover-color: var(--v2p-color-foreground);--link-caution-color: var(--v2p-color-orange-400);--box-border-color: var(--v2p-color-border);--box-foreground-color: var(--v2p-color-foreground);--box-background-color: var(--v2p-color-bg-content);--box-background-alt-color: var(--v2p-color-main-100);--box-background-hover-color: var(--v2p-color-main-200);--box-border-focus-color: var(--v2p-color-main-200);--box-border-radius: var(--v2p-box-radius);--button-background-color: var(--v2p-color-button-background);--button-foreground-color: var(--v2p-color-button-foreground);--button-hover-color: var(--v2p-color-button-background-hover);--button-background-hover-color: var(--v2p-color-button-background-hover);--button-foreground-hover-color: var(--v2p-color-button-foreground-hover);--button-border-color: var(--v2p-color-main-300);--button-border-hover-color: var(--v2p-color-main-400)}:root body #Logo{background-image:url("https://www.v2ex.com/static/img/v2ex@2x.png")}:root body ::selection{color:var(--v2p-color-selection-foreground);background-color:var(--v2p-color-selection-background)}:root body img::selection{background-color:var(--v2p-color-selection-background-img)}
  5044. :root{color-scheme:light}:root html,:root body{min-height:100vh}body{scrollbar-gutter:stable;overflow:overlay;visibility:hidden}body.v2p-theme-light-default{visibility:visible}body h1{font-weight:bold}body a{cursor:default;text-decoration:none}body a[href]{cursor:pointer}body a:hover{text-decoration:underline 1px;text-underline-offset:var(--v2p-underline-offset)}body pre{max-width:calc(830px - 2*var(--v2p-layout-column-gap) - 24px)}body #Top{height:var(--v2p-nav-height);background-color:var(--v2p-color-bg-content);border:none}body #Bottom{color:var(--v2p-color-font-secondary);background-color:var(--v2p-color-bg-footer);border:none}body #Bottom .content{flex-direction:column}body #Wrapper{background-color:inherit;background-image:none}body #Wrapper.Night{background-color:inherit;background-image:none}body #Wrapper .content{display:flex;gap:var(--v2p-layout-column-gap)}#Navcol .nav_item:hover{background-color:var(--box-background-hover-color)}#Navcol .nav_item:hover:active{box-shadow:none}#Navcol .nav_item_current:hover{color:var(--box-foreground-color);background-color:var(--v2p-color-bg-content)}#Navcol .nav_item_current:active{box-shadow:none}#Rightcol #page-outline-title{background-color:var(--v2p-color-button-background-hover)}#Rightcol .page-outline-item:hover{background-color:var(--box-background-hover-color)}body #Wrapper:has(#Singleton){padding:20px 0 0}body #Wrapper:has(#Singleton) #Singleton{overflow:hidden;box-shadow:var(--v2p-box-shadow)}body #Leftbar{float:none;order:1}body #Main{flex:1;order:2;max-width:85vw;margin:0}body #Rightbar{float:none;order:3;margin-right:var(--v2p-layout-column-gap)}body #search-container{height:30px;margin:0 30px;background-color:var(--v2p-color-bg-search);border:none;border-radius:6px}body #search-container::before{top:0;left:4px;opacity:.6;background-size:14px 14px;filter:none}body #search-container.active{background-color:var(--v2p-color-bg-search-active)}body #search-container #search-result{z-index:var(--zidx-serach);top:42px;font-size:14px;color:var(--v2p-color-font-secondary);background:var(--v2p-color-bg-widget);backdrop-filter:blur(16px);border:1px solid var(--box-border-color);box-shadow:var(--v2p-widget-shadow)}body #search-container #search-result .fade{color:var(--v2p-color-font-secondary)}body #search-container #search-result .search-item{font-weight:bold;color:var(--v2p-color-foreground);border-radius:5px}body #search-container #search-result .search-item.active{color:var(--v2p-color-foreground)}body #search-container #search-result .search-item.active.v2p-no-active{background-color:rgba(0,0,0,0)}body .box{color:var(--box-foreground-color);background-color:var(--v2p-color-bg-content);border:none;border-radius:var(--box-border-radius);box-shadow:var(--v2p-box-shadow)}body .box .header>h1{font-size:22px;font-weight:bold}body .box .header .gray{color:var(--color-gray)}body .button{--button-hover-shadow: 0 1.8px 0 var(--button-border-color), 0 1.8px 0 var(--button-background-color)}body .button.normal,body .button.super{cursor:pointer;user-select:none;position:relative;display:inline-flex;gap:5px;align-items:center;height:28px;padding:0 12px;font-family:inherit;font-size:14px;font-weight:500;line-height:28px;color:var(--v2p-color-button-foreground);text-shadow:none;white-space:nowrap;background:var(--v2p-color-button-background);border:none;border-radius:6px;outline:none;box-shadow:0 1.8px 0 var(--box-background-hover-color),0 1.8px 0 var(--button-background-color);transition:color .25s,background-color .25s,box-shadow .25s}body .button.normal:is(:hover:enabled,:active:enabled),body .button.super:is(:hover:enabled,:active:enabled){font-weight:500;color:var(--v2p-color-button-foreground-hover);text-shadow:none;background:var(--v2p-color-button-background-hover);border:none;box-shadow:var(--button-hover-shadow)}body .button.normal:is(.hover_now,.disable_now),body .button.super:is(.hover_now,.disable_now){color:var(--v2p-color-button-foreground) !important;text-shadow:none !important;background:var(--button-background-color) !important;border:none !important;box-shadow:0 1.8px 0 var(--box-background-hover-color) !important,0 1.8px 0 var(--button-background-color) !important}body .button.normal:is(.disable_now,:disabled),body .button.super:is(.disable_now,:disabled){pointer-events:none;cursor:default;font-weight:500;color:var(--v2p-color-button-foreground);text-shadow:none;opacity:.8;background:var(--button-background-color);box-shadow:0 1.8px 0 var(--box-background-hover-color),0 1.8px 0 var(--button-background-color)}body .button.normal kbd,body .button.super kbd{position:relative;right:-4px;padding:0 3px;font-family:inherit;font-size:90%;line-height:initial;border:1px solid var(--button-border-color);border-radius:4px}body .button.special{--button-hover-shadow: 0 1.8px 0 var(--v2p-color-accent-200), 0 1.8px 0 var(--v2p-color-accent-100);color:var(--v2p-color-accent-500);background:var(--v2p-color-accent-100);box-shadow:var(--button-hover-shadow)}body .button.special:hover,body .button.special:hover:enabled{color:var(--v2p-color-accent-600);background:var(--v2p-color-accent-100);border:none;box-shadow:var(--button-hover-shadow)}body .button a{color:inherit;text-decoration:none}body .badge{user-select:none;padding:2px 5px;font-weight:bold;border:1px solid var(--v2p-color-accent-400)}body .badge:first-child{border:1px solid var(--v2p-color-accent-400);border-top-left-radius:4px;border-bottom-left-radius:4px}body .badge:last-child{border:1px solid var(--v2p-color-accent-400);border-top-right-radius:4px;border-bottom-right-radius:4px}body .badge.op{color:var(--v2p-color-accent-500);background-color:var(--v2p-color-accent-50)}body .badge.mod{color:var(--v2p-color-bg-content);background-color:var(--v2p-color-accent-400)}body .badge.you{color:var(--v2p-color-orange-400);background-color:var(--v2p-color-orange-50);border:1px solid var(--v2p-color-orange-400)}body .badge.mini{height:1.2em;padding:0 3px;font-size:12px;font-weight:normal;line-height:1}body a.node:is(:active,:link,:visited){padding:5px 6px;font-size:13px;color:var(--v2p-color-font-secondary);background-color:var(--v2p-color-bg-block);border-radius:4px}body a.node:is(:active,:link,:visited):hover{color:var(--v2p-color-font-tertiary);background-color:var(--v2p-color-button-background-hover)}body .outdated{font-size:12px;border-color:var(--v2p-color-border);border-bottom:none}body :is(.page_normal,.page_current):is(:link,:visited){user-select:none;padding:6px 9px;font-size:14px;border:none;border-radius:4px}body .page_normal:is(:link,:visited){font-weight:500;background-color:var(--v2p-color-bg-content);box-shadow:0 2px 2px var(--box-background-hover-color);transition:transform .25s}body .page_normal:is(:link,:visited):hover{transform:scale(1.1) translateY(-2px)}body .page_current:is(:link,:visited){pointer-events:none;font-weight:bold;background-color:var(--box-background-hover-color);box-shadow:none}body .page_input{display:none}body .dock_area{margin:12px 0;background:var(--v2p-color-bg-block)}body .member-activity-bar{background-color:var(--v2p-color-divider)}body .member-activity-bar .member-activity-start{background-color:var(--v2p-color-accent-200)}body .member-activity-bar .member-activity-fourth{background-color:var(--v2p-color-accent-400)}body .member-activity-bar .member-activity-half{background-color:var(--v2p-color-accent-500)}body .member-activity-bar .member-activity-almost{background-color:var(--v2p-color-accent-600)}body .member-activity-bar .member-activity-done{background-color:var(--v2p-color-orange-400)}body .online{user-select:none;padding:6px 8px;font-size:13px;color:var(--v2p-color-bg-content);background:var(--v2p-color-accent-400);border-radius:5px}body #topic_supplement{resize:none;overflow:hidden;height:unset;min-height:550px !important;max-height:800px !important;font-size:15px;color:currentColor;background-color:var(--v2p-color-bg-input);border:1px solid var(--button-border-color);border-radius:8px;transition:opacity .25s;overflow-y:auto}body #topic_supplement::placeholder{font-size:15px;color:var(--v2p-color-font-tertiary)}body #topic_supplement:is(:focus,:focus-within){background-color:rgba(0,0,0,0);outline:none;box-shadow:0 0 0 1px var(--button-border-color)}body .item_hot_topic_title{--offset: 2.4px;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-height:1.4;position:relative;padding:var(--offset) 0;text-shadow:none}body .item_hot_topic_title>a:hover{text-underline-offset:var(--offset)}body form textarea#topic_title{resize:none;overflow:hidden;height:unset;min-height:75px !important;max-height:800px !important;font-size:15px;color:currentColor;background-color:var(--v2p-color-bg-input);border:1px solid var(--button-border-color);border-radius:8px;transition:opacity .25s}body form textarea#topic_title::placeholder{font-size:15px;color:var(--v2p-color-font-tertiary)}body form textarea#topic_title:is(:focus,:focus-within){background-color:rgba(0,0,0,0);outline:none;box-shadow:0 0 0 1px var(--button-border-color)}body form #topic_title{resize:none;overflow:hidden;height:unset;min-height:30px !important;max-height:800px !important;font-size:15px;color:currentColor;background-color:var(--v2p-color-bg-input);border:1px solid var(--button-border-color);border-radius:8px;transition:opacity .25s}body form #topic_title::placeholder{font-size:15px;color:var(--v2p-color-font-tertiary)}body form #topic_title:is(:focus,:focus-within){background-color:rgba(0,0,0,0);outline:none;box-shadow:0 0 0 1px var(--button-border-color)}body form #topic_content{resize:none;overflow:hidden;height:unset;min-height:120px !important;max-height:800px !important;font-size:15px;color:currentColor;background-color:var(--v2p-color-bg-input);border:1px solid var(--button-border-color);border-radius:8px;transition:opacity .25s}body form #topic_content::placeholder{font-size:15px;color:var(--v2p-color-font-tertiary)}body form #topic_content:is(:focus,:focus-within){background-color:rgba(0,0,0,0);outline:none;box-shadow:0 0 0 1px var(--button-border-color)}body form[action^="/notes"] .cell{background-color:rgba(0,0,0,0) !important}body #syntax-selector .radio-group{padding:3px;background-color:var(--v2p-color-background)}body #syntax-selector .radio-group>input[type=radio]:checked+label{background-color:var(--v2p-color-accent-100)}body #syntax-selector .radio-group>input[type=radio]+label{cursor:pointer;font-size:13px}body #syntax-selector label{color:var(--v2p-color-foreground)}body .snow{color:var(--v2p-color-font-quaternary)}body .orange-dot{background:var(--v2p-color-orange-400)}body .alt{background-color:var(--v2p-color-bg-input);border:1px solid var(--button-border-color)}body a.btn_hero{border-color:var(--v2p-color-foreground)}body a.btn_hero:hover{background-color:var(--v2p-color-foreground)}body .cell_ops{background-color:rgba(0,0,0,0)}body :is(.topic_content,.reply_content,.v2p-topic-preview-content,.v2p-topic-preview-addons,.v2p-reply-preview,.markdown_body) a[href*="v2ex.com/t"],body :is(.topic_content,.reply_content,.v2p-topic-preview-content,.v2p-topic-preview-addons,.v2p-reply-preview,.markdown_body) a[href*="v2ex.com/t"][href^=http],body :is(.topic_content,.reply_content,.v2p-topic-preview-content,.v2p-topic-preview-addons,.v2p-reply-preview,.markdown_body) a[href^="/"]{text-decoration:underline 1.5px;text-underline-offset:.46ex;color:var(--v2p-color-accent-500);background-color:var(--v2p-color-accent-50)}body :is(.topic_content,.reply_content,.v2p-topic-preview-content,.v2p-topic-preview-addons,.v2p-reply-preview,.markdown_body) a[href*="v2ex.com/t"]:hover,body :is(.topic_content,.reply_content,.v2p-topic-preview-content,.v2p-topic-preview-addons,.v2p-reply-preview,.markdown_body) a[href*="v2ex.com/t"][href^=http]:hover,body :is(.topic_content,.reply_content,.v2p-topic-preview-content,.v2p-topic-preview-addons,.v2p-reply-preview,.markdown_body) a[href^="/"]:hover{color:var(--v2p-color-accent-500);background-color:var(--v2p-color-accent-50)}body :is(.topic_content,.reply_content,.v2p-topic-preview-content,.v2p-topic-preview-addons,.v2p-reply-preview,.markdown_body) a[href^=http],body :is(.topic_content,.reply_content,.v2p-topic-preview-content,.v2p-topic-preview-addons,.v2p-reply-preview,.markdown_body) a[href*="/email-protection"],body :is(.topic_content,.reply_content,.v2p-topic-preview-content,.v2p-topic-preview-addons,.v2p-reply-preview,.markdown_body) a[href^="/member/"]{text-decoration:underline 1.5px;text-underline-offset:.46ex;color:currentColor;background-color:var(--v2p-color-bg-link)}body :is(.topic_content,.reply_content,.v2p-topic-preview-content,.v2p-topic-preview-addons,.v2p-reply-preview,.markdown_body) a[href^=http]:hover,body :is(.topic_content,.reply_content,.v2p-topic-preview-content,.v2p-topic-preview-addons,.v2p-reply-preview,.markdown_body) a[href*="/email-protection"]:hover,body :is(.topic_content,.reply_content,.v2p-topic-preview-content,.v2p-topic-preview-addons,.v2p-reply-preview,.markdown_body) a[href^="/member/"]:hover{color:currentColor;background-color:var(--v2p-color-bg-link-hover)}body :is(.topic_content,.reply_content,.v2p-topic-preview-content,.v2p-topic-preview-addons,.v2p-reply-preview,.markdown_body) a[href^=http]:has(>.embedded_image),body :is(.topic_content,.reply_content,.v2p-topic-preview-content,.v2p-topic-preview-addons,.v2p-reply-preview,.markdown_body) a[href*="/email-protection"]:has(>.embedded_image),body :is(.topic_content,.reply_content,.v2p-topic-preview-content,.v2p-topic-preview-addons,.v2p-reply-preview,.markdown_body) a[href^="/member/"]:has(>.embedded_image){background-color:rgba(0,0,0,0)}body .CodeMirror{color:currentColor;background-color:var(--v2p-color-bg-input)}body .CodeMirror.CodeMirror-focused{background-color:var(--v2p-color-bg-content)}body .CodeMirror .CodeMirror-gutters{padding-right:5px;background-color:var(--v2p-color-bg-input);border-right:1px solid var(--v2p-color-border-darker)}body .CodeMirror .CodeMirror-linenumber{color:var(--v2p-color-font-quaternary)}body .CodeMirror .CodeMirror-cursors{background-color:var(--v2p-color-foreground)}body .cm-s-one-dark{background-color:var(--v2p-color-bg-input)}body #workspace{overflow:hidden;border:1px solid var(--button-border-color);border-radius:8px}body .select2-container{width:200px !important}body .select2-container .select2-selection{background-color:var(--v2p-color-background);border:1px solid var(--v2p-color-border)}body .select2-container .select2-selection .select2-selection__rendered,body .select2-container .select2-selection .select2-selection__placeholder{color:var(--v2p-color-foreground)}body .select2-container .select2-dropdown{font-size:14px;background:var(--v2p-color-bg-widget);backdrop-filter:blur(16px);border:1px solid var(--box-border-color);border-radius:8px;box-shadow:var(--v2p-widget-shadow);transform:translateY(5px)}body .select2-container .select2-dropdown .select2-search{padding:5px}body .select2-container .select2-dropdown .select2-search .select2-search__field{padding:6px 4px;background-color:rgba(0,0,0,0);border:1px solid var(--v2p-color-border);border-radius:4px}body .select2-container .select2-dropdown .select2-search .select2-search__field:focus-visible{border-color:var(--v2p-color-font-quaternary);outline:none}body .select2-container .select2-dropdown .select2-results>.select2-results__options{padding:5px}body .select2-container .select2-dropdown .select2-container--default .select2-results__option--selected{color:currentColor;background-color:var(--v2p-color-accent-100)}body .select2-container .select2-results__option{border-radius:4px}body .select2-container .select2-results__option--highlighted.select2-results__option--selectable{color:currentColor;background-color:var(--v2p-color-main-200)}body .select2-container .select2-results__option--selected{color:currentColor !important;background-color:var(--v2p-color-accent-100) !important}body .problem{color:currentColor;color:var(--v2p-color-orange-400);background-color:var(--v2p-color-orange-50);border-color:var(--v2p-color-orange-400);border-bottom:none}body .markdown_body table{border-top:1px solid var(--v2p-color-border-darker);box-shadow:none}body .markdown_body table tr th,body .markdown_body table tr td{border:1px solid var(--v2p-color-border-darker)}body .markdown_body table tr:nth-child(2n){background-color:var(--box-background-alt-color)}body .markdown_body blockquote{margin-inline:0;padding:0 1em;color:var(--v2p-color-font-tertiary);border-left:3px solid var(--v2p-color-border)}body .markdown_body pre{line-height:1.5}body .social_label:is(:link,:visited,:active){background-color:var(--v2p-color-button-background);border-radius:var(--box-border-radius);box-shadow:none}body .social_label:is(:link,:visited,:active):hover{background-color:var(--v2p-color-button-background-hover)}body .green{color:var(--v2p-color-accent-500)}body .message{color:var(--v2p-color-orange-400);background-color:var(--v2p-color-orange-50);border:none}body .balance_area,body a.balance_area:is(:link,:visited){display:inline-flex;gap:3px;align-items:center;font-weight:600;color:var(--v2p-color-foreground);text-shadow:none;background:var(--v2p-color-button-background)}body .balance_area:hover,body a.balance_area:is(:link,:visited):hover{background:var(--v2p-color-button-background-hover)}body :is(.subtle){background-color:var(--v2p-color-bg-subtle);border-left:3px solid var(--v2p-color-accent-200)}body :is(.subtle) .topic_content{font-size:15px}body .onoffswitch label .frame::before{color:#fff;background-color:var(--v2p-color-accent-400)}body .onoffswitch label .frame::after{color:var(--v2p-color-font-secondary);background-color:var(--v2p-color-bg-search)}body select{color:var(--v2p-color-foreground);background-color:var(--v2p-color-background);border:1px solid var(--v2p-color-border);border-radius:4px;padding:4px 6px}body .ml,body .mle,body .mll,body .sl,body .sll,body .sls{color:var(--v2p-color-foreground);background-color:var(--v2p-color-bg-content);border-color:var(--v2p-color-input-border)}body .ml:focus,body .mle:focus,body .mll:focus,body .sl:focus,body .sll:focus,body .sls:focus{border-color:var(--v2p-color-input-border)}body input,body select,body textarea{color:var(--v2p-color-foreground)}body .onoffswitch{width:90px;min-width:90px;max-width:90px}.box .tag::before{color:var(--v2p-color-font-secondary)}.box .tag:link,.box .tag:visited{font-size:12px;color:var(--v2p-color-font-secondary);background-color:var(--v2p-color-main-100);border-radius:5px}.box .tag>li{opacity:.6}#Top .content{height:100%}#Top .site-nav{height:100%;padding:0}#Top .tools{display:flex;gap:8px 14px;align-items:center;justify-content:flex-end;font-size:14px;font-weight:400}#Top .tools .top{height:26px;padding:0 6px;line-height:26px;color:var(--v2p-color-button-foreground);white-space:nowrap;border-radius:4px}#Top .tools .top:hover{color:var(--v2p-color-foreground)}#Top .tools .top:not(.v2p-hover-btn):hover{background-color:var(--v2p-color-button-background-hover)}#Top .tools *{margin-left:0}#Main .box{overflow:auto;padding:0 12px}#Main .box.node-header>.cell{margin:0 -12px}#Main .box .cell{padding:var(--v2p-tp-item-x) var(--v2p-tp-item-y);background-image:none !important}#Main .box .cell_ops{padding:15px 5px}#Main .box:has(form[action="/write"]) .cell:nth-child(1),#Main .box:has(form[action="/write"]) .cell:nth-child(2){border:none}#Main .box:has(form[action="/write"]) .cell:has(#syntax-selector){padding:8px 0 !important}#Main .box:has(form[action="/write"]) .cell:has(#syntax-selector) .tab-alt-container{gap:0 8px}#Main .box:has(form[action="/write"]) .cell:has(#syntax-selector) .tab-alt{padding:4px 2px;border-bottom-width:2px;transition:none}#Main .box:has(form[action="/write"]) .cell:has(#syntax-selector) .tab-alt:not(.active):hover{border-color:rgba(0,0,0,0)}#Main .box:has(form[action="/write"]) .cell#preview{padding:2px 5px}#Main .topic_buttons{display:flex;flex-wrap:wrap;column-gap:5px;align-items:center;padding:8px 0;background:none}#Main .topic_buttons .topic_stats{float:none;flex:1;order:99;margin-left:10px;padding:0 !important;font-size:12px;text-shadow:none;white-space:nowrap}#Main .topic_buttons .topic_thanked{font-size:12px}#Main .topic_buttons a.tb:link{display:flex;flex-direction:row-reverse;column-gap:5px;align-items:center;padding:5px;text-shadow:none;white-space:nowrap;background:none;border-radius:4px}#Main .topic_buttons a.tb:link:not(.v2p-hover-btn){color:var(--v2p-color-font-secondary)}#Main .topic_buttons a.tb:link:hover:not(.v2p-hover-btn){color:currentColor;background:var(--v2p-color-bg-block)}#Main .vote:link{color:var(--v2p-color-font-tertiary);border-color:var(--v2p-color-border-darker);border-radius:5px}#Main .vote:link:hover{box-shadow:0 2px 2px var(--v2p-color-main-200)}#Main .cell .topic-link{color:var(--v2p-color-foreground);text-decoration:none}#Main .cell .topic-link:visited{color:var(--v2p-color-link-visited)}#Main .cell .topic_info{pointer-events:none;user-select:none;position:relative;display:flex;flex-wrap:wrap;align-items:center}#Main .cell .topic_info::after{content:"";position:absolute;z-index:1;inset:0 0 -6px;background-color:var(--v2p-color-bg-content)}#Main .cell .topic_info .votes,#Main .cell .topic_info .node,#Main .cell .topic_info strong:first-of-type,#Main .cell .topic_info span:first-of-type,#Main .cell .topic_info .v2p-topic-actions{pointer-events:auto;position:relative;z-index:2}#Main .cell .topic_info a[href^="/member"]{font-weight:500;color:var(--v2p-color-font-tertiary);background-color:rgba(0,0,0,0)}#Main .cell .count_livid{user-select:none;display:inline-block;padding:5px 10px;font-size:12px;font-weight:400;white-space:nowrap;border-radius:5px;margin-right:0;color:var(--v2p-color-button-foreground);background-color:var(--v2p-color-bg-block)}#Main .cell .count_orange{user-select:none;display:inline-block;padding:5px 10px;font-size:12px;font-weight:400;white-space:nowrap;border-radius:5px;font-weight:bold;color:var(--v2p-color-bg-block);background-color:var(--v2p-color-orange-400)}#Main .cell .item_title .topic-link{font-size:15px;font-weight:500}#Main .cell.item tr>td:nth-child(2){width:30px}#Main .box>.cell[id^=r]:not(:has(.cell[id^=r])) .reply_content,#Main .v2p-modal-content>.cell[id^=r]:not(:has(.cell[id^=r])) .reply_content{padding-bottom:0}#Main .cell[id^=r]{--bg-reply: var(--v2p-color-bg-content);background-color:var(--bg-reply)}#Main .cell[id^=r]:not(:has(+.cell[id^=r])){border-bottom:none}#Main .cell[id^=r]:hover>table td:last-of-type .fr a{opacity:1}#Main .cell[id^=r] .ago{font-size:12px;color:var(--v2p-color-font-quaternary);white-space:nowrap}#Main .cell[id^=r] .reply_content{padding-bottom:10px;line-height:1.5}#Main .cell[id^=r]>table:first-of-type td:first-of-type{width:40px}#Main .cell[id^=r]>table:first-of-type td:first-of-type .avatar{display:inline-block;aspect-ratio:1;width:40px !important;height:40px !important;background-color:var(--v2p-color-bg-avatar);border-radius:5px}#Main .cell[id^=r]>table~.cell[id^=r]{--bg-reply: var(--v2p-color-bg-reply);position:relative;z-index:var(--zidx-expand-btn);padding:var(--v2p-tp-nested-pd) 0 0 var(--v2p-tp-nested-pd);border:none;border-radius:0;box-shadow:-2.4px 0 var(--v2p-color-border-darker)}#Main .cell[id^=r]>table~.cell[id^=r] .cell[id^=r]{padding:0;box-shadow:none}#Main .cell[id^=r]>table~.cell[id^=r] .cell[id^=r].v2p-indent{padding-left:var(--v2p-tp-nested-pd);border-left:1px solid var(--v2p-color-border-darker)}#Main .cell[id^=r]>table~.cell[id^=r] tr td:first-of-type{width:25px}#Main .cell[id^=r]>table~.cell[id^=r] tr td:first-of-type .avatar{width:25px !important;height:25px !important;border-radius:4px}#Main .cell[id^=r]>table~.cell[id^=r] tr td:nth-child(3) strong a{font-size:13px}#Main .cell[id^=r]>table~.cell[id^=r] .reply_content{padding-right:5px}#Main .cell[id^=r]>table td:nth-of-type(2){width:15px}#Main .cell[id^=r]>table td:last-of-type a.dark{color:var(--v2p-color-font-secondary);text-decoration:none}#Main .cell[id^=r]>table td:last-of-type a.dark:hover{text-decoration:none}#Main .cell[id^=r]>table td:last-of-type .fr{user-select:none;position:relative;top:-3px}#Main .cell[id^=r]>table td:last-of-type .fr a{opacity:0}#Main .cell[id^=r]>table td:last-of-type .fr+.sep3{height:0}#Main .cell[id^=r]:last-of-type{border:none}#Main .cell[id^=r] .no{user-select:none;position:relative;top:-4px;padding:5px 10px;font-size:12px;color:var(--v2p-color-cell-num);background-color:rgba(0,0,0,0);border-radius:5px}#Main #Tabs{user-select:none;position:sticky;z-index:var(--zidx-tabs);top:0;display:flex;flex-wrap:wrap;gap:5px 3px;align-items:center;padding:var(--v2p-tp-tabs-pd);background-color:var(--v2p-color-bg-content);border-bottom:1px solid var(--box-border-color)}#Main #Tabs .tab_current{margin-right:0}#Main #Tabs .tab{margin:0}#Main #Tabs .tab.v2p-hover-btn::before{inset:0}#Main #SecondaryTabs{padding:10px;background-color:var(--v2p-color-bg-tabs);border-radius:5px}#Main #SecondaryTabs a{color:var(--v2p-color-tabs)}#Main .topic_content,#Main .reply_content{font-size:15px;line-height:1.6;color:currentColor}#Main .topic_content a[href^="/member"],#Main .reply_content a[href^="/member"]{position:relative;bottom:1px;font-size:13px;color:var(--v2p-color-font-tertiary);text-decoration:underline;text-underline-offset:.4ex;background-color:rgba(0,0,0,0)}#Main .thank_area{font-size:12px}#Main .tab{user-select:none;color:var(--v2p-color-foreground);background-color:rgba(0,0,0,0);border-radius:3px}#Main .tab:not(.v2p-hover-btn):hover{background-color:var(--v2p-color-bg-block)}#Main .tab_current{user-select:none;color:var(--v2p-color-bg-content);background-color:var(--v2p-color-foreground);border-radius:3px}#Main #reply-box.reply-box-sticky{z-index:var(--zidx-reply-box);bottom:20px;overflow:visible;margin:0 -10px;padding:0 22px;border:none;border-radius:var(--box-border-radius);outline:2px solid var(--v2p-color-border)}#Main #reply-box .flex-one-row:last-of-type{flex-direction:row-reverse;gap:10px;justify-content:flex-start}#Main #reply-box .flex-one-row:last-of-type .gray{margin-right:auto}#Main #reply-box>.cell{font-size:12px}#Main #reply-box>.cell.flex-one-row{min-height:45px;padding:0 10px;border:none}#Main #reply-box>.cell.flex-row-end{padding:12px 10px;border:none}#Main #reply-box>.cell:has(form){padding-top:0}#Main #no-comments-yet{color:var(--color-gray);border-color:var(--color-gray)}#Main #notifications .cell[id^=n]:hover .node{opacity:1}#Main #notifications .cell[id^=n] .node{opacity:0}#Main #notifications .cell[id^=n] .payload{color:var(--v2p-color-foreground);background-color:var(--v2p-color-bg-block)}#Main #notifications .cell[id^=n] .payload:has(.embedded_video_wrapper){min-width:50%}#Main #notifications .cell[id^=n] .topic-link:visited{color:var(--v2p-color-font-quaternary)}#Main .cell_tabs .cell_tab_current{font-weight:bold;border-color:var(--v2p-color-foreground)}#Main .cell_tabs .cell_tab{color:var(--v2p-color-foreground)}#Main .cell_tabs .cell_tab:hover{border-color:var(--v2p-color-border-darker)}#Rightbar .cell:has(.light-toggle){font-size:13px}#Rightbar .box .item_node{font-size:12px;color:var(--v2p-color-font-secondary);border-color:var(--v2p-color-border);border-radius:5px}#Rightbar .box .item_node:hover{box-shadow:var(--v2p-box-shadow)}#Rightbar a.dark:is(:link,:active,:visited,:hover){color:var(--v2p-color-font-tertiary)}#Rightbar a.dark:is(:link,:active,:visited,:hover):hover{color:var(--v2p-color-font-secondary)}#Bottom{position:sticky;top:100%}#Bottom a.dark{font-size:13px;font-weight:400}#Bottom a.dark:is(:link,:active,:visited,:hover){color:var(--v2p-color-font-tertiary)}.wwads-cn{border:none !important;box-shadow:none !important}.box:has(a[href^="/advertise"]){overflow:hidden;border:none !important;box-shadow:none !important}.box:has(a[href^="/advertise"]) .sidebar_compliance{background-color:var(--v2p-color-bg-block)}
  5045. :root body.v2p-theme-dark-default,:root .v2p-theme-dark-default,:root[data-darkreader-scheme=dark] body,:root body:has(#Wrapper.Night){color-scheme:dark;--v2p-color-main-50: unset;--v2p-color-main-100: #2d333b;--v2p-color-main-200: #374151;--v2p-color-main-300: #374151;--v2p-color-main-350: #6b7280cc;--v2p-color-main-400: #6b7280;--v2p-color-main-500: #9ca3af;--v2p-color-main-600: #9ca3af;--v2p-color-main-700: #d1d5db;--v2p-color-main-800: #e5e7eb;--v2p-color-main-900: #111827;--v2p-color-main-950: #030712;--v2p-color-accent-50: #064e3b;--v2p-color-accent-100: #065f46;--v2p-color-accent-200: #047857;--v2p-color-accent-300: #059669;--v2p-color-accent-400: #10b981;--v2p-color-accent-500: #34d399;--v2p-color-accent-600: #6ee7b7;--v2p-color-orange-50: #593600;--v2p-color-orange-100: #9a3412;--v2p-color-orange-400: #fbe090;--v2p-color-background: #1c2128;--v2p-color-foreground: #adbac7;--v2p-color-font-secondary: var(--v2p-color-main-600);--v2p-color-button-background: #373e47;--v2p-color-button-foreground: var(--v2p-color-foreground);--v2p-color-button-background-hover: #444c56;--v2p-color-button-foreground-hover: var(--v2p-color-foreground);--v2p-color-bg-content: #22272e;--v2p-color-bg-hover-btn: var(--v2p-color-button-background-hover);--v2p-color-bg-subtle: rgb(6 78 59 / 30%);--v2p-color-bg-input: var(--v2p-color-background);--v2p-color-bg-search: var(--v2p-color-main-100);--v2p-color-bg-search-active: var(--v2p-color-main-200);--v2p-color-bg-widget: var(--v2p-color-bg-content);--v2p-color-bg-reply: var(--v2p-color-main-100);--v2p-color-bg-tooltip: var(--v2p-color-main-100);--v2p-color-bg-avatar: var(--v2p-color-main-300);--v2p-color-bg-block: #373e47;--v2p-color-heart: #ef4444;--v2p-color-heart-fill: #fca5a5;--v2p-color-mask: rgb(99 110 123 / 40%);--v2p-color-border: #444c56;--v2p-color-input-border: #444c56;--v2p-color-border-darker: #444c56;--v2p-box-shadow: 0 0 0 1px var(--v2p-color-border);--v2p-toast-shadow: none;--link-color: var(--v2p-color-foreground);--box-background-alt-color: var(--v2p-color-main-100);--box-background-hover-color: var(--v2p-color-main-300);--button-hover-color: var(--button-background-hover-color);--button-border-color: var(--v2p-color-border);--button-border-hover-color: #768390;visibility:visible}:root body.v2p-theme-dark-default #Logo,:root .v2p-theme-dark-default #Logo,:root[data-darkreader-scheme=dark] body #Logo,:root body:has(#Wrapper.Night) #Logo{background-image:url("https://www.v2ex.com/static/img/v2ex-alt@2x.png")}:root body.v2p-theme-dark-default ::selection,:root .v2p-theme-dark-default ::selection,:root[data-darkreader-scheme=dark] body ::selection,:root body:has(#Wrapper.Night) ::selection{color:var(--v2p-color-background, #1c2128);background-color:var(--v2p-color-foreground, #adbac7)}:root body.v2p-theme-dark-default img::selection,:root .v2p-theme-dark-default img::selection,:root[data-darkreader-scheme=dark] body img::selection,:root body:has(#Wrapper.Night) img::selection{background-color:var(--v2p-color-foreground, #adbac7)}:root body.v2p-theme-dark-default #Top,:root .v2p-theme-dark-default #Top,:root[data-darkreader-scheme=dark] body #Top,:root body:has(#Wrapper.Night) #Top{background-color:rgba(0,0,0,0)}:root body.v2p-theme-dark-default #Main .cell .item_title .topic-link,:root .v2p-theme-dark-default #Main .cell .item_title .topic-link,:root[data-darkreader-scheme=dark] body #Main .cell .item_title .topic-link,:root body:has(#Wrapper.Night) #Main .cell .item_title .topic-link{font-weight:normal}:root body.v2p-theme-dark-default #search-container::before,:root .v2p-theme-dark-default #search-container::before,:root[data-darkreader-scheme=dark] body #search-container::before,:root body:has(#Wrapper.Night) #search-container::before{background-image:url("/static/img/search_icon_light.png")}@supports not selector(:has(*)){:root #Wrapper.Night{--v2p-color-main-50: unset;--v2p-color-main-100: #2d333b;--v2p-color-main-200: #374151;--v2p-color-main-300: #374151;--v2p-color-main-350: #6b7280cc;--v2p-color-main-400: #6b7280;--v2p-color-main-500: #9ca3af;--v2p-color-main-600: #9ca3af;--v2p-color-main-700: #d1d5db;--v2p-color-main-800: #e5e7eb;--v2p-color-main-900: #111827;--v2p-color-main-950: #030712;--v2p-color-accent-50: #064e3b;--v2p-color-accent-100: #065f46;--v2p-color-accent-200: #047857;--v2p-color-accent-300: #059669;--v2p-color-accent-400: #10b981;--v2p-color-accent-500: #34d399;--v2p-color-accent-600: #6ee7b7;--v2p-color-orange-50: #593600;--v2p-color-orange-100: #9a3412;--v2p-color-orange-400: #fbe090;--v2p-color-background: #1c2128;--v2p-color-foreground: #adbac7;--v2p-color-font-secondary: var(--v2p-color-main-600);--v2p-color-button-background: #373e47;--v2p-color-button-foreground: var(--v2p-color-foreground);--v2p-color-button-background-hover: #444c56;--v2p-color-button-foreground-hover: var(--v2p-color-foreground);--v2p-color-bg-content: #22272e;--v2p-color-bg-hover-btn: var(--v2p-color-button-background-hover);--v2p-color-bg-subtle: rgb(6 78 59 / 30%);--v2p-color-bg-input: var(--v2p-color-background);--v2p-color-bg-search: var(--v2p-color-main-100);--v2p-color-bg-search-active: var(--v2p-color-main-200);--v2p-color-bg-widget: var(--v2p-color-bg-content);--v2p-color-bg-reply: var(--v2p-color-main-100);--v2p-color-bg-tooltip: var(--v2p-color-main-100);--v2p-color-bg-avatar: var(--v2p-color-main-300);--v2p-color-bg-block: #373e47;--v2p-color-heart: #ef4444;--v2p-color-heart-fill: #fca5a5;--v2p-color-mask: rgb(99 110 123 / 40%);--v2p-color-border: #444c56;--v2p-color-input-border: #444c56;--v2p-color-border-darker: #444c56;--v2p-box-shadow: 0 0 0 1px var(--v2p-color-border);--v2p-toast-shadow: none;--link-color: var(--v2p-color-foreground);--box-background-alt-color: var(--v2p-color-main-100);--box-background-hover-color: var(--v2p-color-main-300);--button-hover-color: var(--button-background-hover-color);--button-border-color: var(--v2p-color-border);--button-border-hover-color: #768390;visibility:visible}:root #Wrapper.Night #Logo{background-image:url("https://www.v2ex.com/static/img/v2ex-alt@2x.png")}:root #Wrapper.Night ::selection{color:var(--v2p-color-background, #1c2128);background-color:var(--v2p-color-foreground, #adbac7)}:root #Wrapper.Night img::selection{background-color:var(--v2p-color-foreground, #adbac7)}}
  5046. body.v2p-theme-dawn,.v2p-theme-dawn{--v2p-color-base: 32deg 57% 95%;--v2p-color-surface: 35deg 100% 98%;--v2p-color-overlay: 33deg 43% 91%;--v2p-color-muted: 257deg 9% 61%;--v2p-color-subtle: 248deg 12% 52%;--v2p-color-text: 248deg 19% 40%;--v2p-color-love: 343deg 35% 55%;--v2p-color-gold: 35deg 81% 56%;--v2p-color-rose: 3deg 53% 67%;--v2p-color-pine: 197deg 53% 34%;--v2p-color-foam: 189deg 30% 48%;--v2p-color-iris: 268deg 21% 57%;--v2p-color-accent-50: hsl(var(--v2p-color-foam) / 10%);--v2p-color-accent-100: hsl(var(--v2p-color-foam) / 20%);--v2p-color-accent-200: hsl(var(--v2p-color-foam) / 30%);--v2p-color-accent-300: hsl(var(--v2p-color-foam) / 40%);--v2p-color-accent-400: hsl(var(--v2p-color-foam) / 50%);--v2p-color-accent-500: hsl(var(--v2p-color-foam) / 65%);--v2p-color-accent-600: hsl(var(--v2p-color-foam) / 80%);--v2p-color-orange-50: hsl(var(--v2p-color-gold) / 10%);--v2p-color-orange-100: hsl(var(--v2p-color-gold) / 20%);--v2p-color-orange-400: hsl(var(--v2p-color-gold));--v2p-color-background: hsl(var(--v2p-color-base));--v2p-color-foreground: hsl(var(--v2p-color-text));--v2p-color-selection-foreground: var(--v2p-color-foreground);--v2p-color-selection-background: hsl(var(--v2p-color-muted) / 20%);--v2p-color-selection-background-img: hsl(var(--v2p-color-muted) / 60%);--v2p-color-font-secondary: hsl(var(--v2p-color-subtle));--v2p-color-font-tertiary: hsl(var(--v2p-color-subtle));--v2p-color-font-quaternary: hsl(var(--v2p-color-subtle));--v2p-color-button-background: hsl(var(--v2p-color-text) / 10%);--v2p-color-button-foreground: var(--v2p-color-foreground);--v2p-color-button-background-hover: hsl(var(--v2p-color-text) / 15%);--v2p-color-button-foreground-hover: var(--v2p-color-foreground);--v2p-color-bg-content: hsl(var(--v2p-color-surface));--v2p-color-bg-footer: var(--v2p-color-bg-content);--v2p-color-bg-hover-btn: rgb(152 147 165 / 10%);--v2p-color-bg-subtle: hsl(var(--v2p-color-pine) / 10%);--v2p-color-bg-input: hsl(var(--v2p-color-overlay) / 40%);--v2p-color-bg-search: hsl(var(--v2p-color-overlay) / 60%);--v2p-color-bg-search-active: hsl(var(--v2p-color-overlay) / 90%);--v2p-color-bg-widget: rgb(255 255 255 / 70%);--v2p-color-bg-reply: #faf4ed;--v2p-color-bg-tooltip: var(--v2p-color-bg-content);--v2p-color-bg-tabs: hsl(var(--v2p-color-pine) / 10%);--v2p-color-bg-tpr: hsl(var(--v2p-color-text) / 10%);--v2p-color-bg-avatar: hsl(var(--v2p-color-overlay));--v2p-color-bg-block: hsl(var(--v2p-color-text) / 10%);--v2p-color-bg-block-darker: hsl(var(--v2p-color-text) / 25%);--v2p-color-bg-link: hsl(var(--v2p-color-text) / 10%);--v2p-color-bg-link-hover: hsl(var(--v2p-color-text) / 15%);--v2p-color-tabs: hsl(var(--v2p-color-pine));--v2p-color-heart: #eb6f92;--v2p-color-heart-fill: rgb(235 111 146 / 50%);--v2p-color-mask: rgb(0 0 0 / 25%);--v2p-color-divider: hsl(var(--v2p-color-muted) / 20%);--v2p-color-border: hsl(var(--v2p-color-muted) / 20%);--v2p-color-input-border: rgb(152 147 165 / 20%);--v2p-color-border-darker: hsl(var(--v2p-color-muted) / 40%);--v2p-color-link-visited: hsl(var(--v2p-color-subtle));--v2p-color-error: #eb6f92;--v2p-color-bg-error: #fee2e2;--v2p-color-cell-num: hsl(var(--v2p-color-subtle));--v2p-box-shadow: 0 3px 5px 0 rgb(0 0 0 / 4%);--v2p-widget-shadow: 0 9px 24px -3px rgb(0 0 0 / 6%), 0 4px 8px -1px rgb(0 0 0 /12%);--v2p-toast-shadow: 0 6px 16px 0 rgb(0 0 0 / 8%), 0 3px 6px -4px rgb(0 0 0 / 12%), 0 9px 28px 8px rgb(0 0 0 / 5%);--link-color: var(--v2p-color-foreground);--box-background-alt-color: var(--v2p-color-bg-block);--box-background-hover-color: var(--v2p-color-bg-link-hover);--button-hover-color: var(--v2p-color-button-background-hover);--button-border-color: var(--v2p-color-border);--button-border-hover-color: var(--v2p-color-border-darker);visibility:visible}body.v2p-theme-dawn .button.special:hover,body.v2p-theme-dawn .button.special:hover:enabled,.v2p-theme-dawn .button.special:hover,.v2p-theme-dawn .button.special:hover:enabled{text-shadow:none}
  5047. \uFEFFbody{position:relative}body.v2p-modal-open{overflow:hidden}body .button.v2p-prev-btn,body .button.v2p-next-btn{padding:0 15px}body #Top a:has(#Logo.v2p-logo){display:inline-flex;align-items:center}body #Logo.v2p-logo{width:unset;height:25px;padding:0 8px;background-image:none !important}.v2p-hover-btn{cursor:pointer;user-select:none;position:relative;z-index:1;margin:0;text-decoration:none;white-space:nowrap;background:none;background-color:rgba(0,0,0,0);transition:color .2s}.v2p-hover-btn::before{content:"";position:absolute;z-index:-1;inset:0 -5px;transform:scale(0.65);opacity:0;background-color:var(--v2p-color-bg-hover-btn);border-radius:5px;transition:background-color .15s,color .15s,transform .15s,opacity .15s}.v2p-hover-btn:hover{text-decoration:none}.v2p-hover-btn:hover::before{transform:scale(1);opacity:1}.v2p-hover-btn-disabled{pointer-events:none;opacity:.8}.v2p-icon-heart{display:inline-flex;width:16px;height:16px;color:var(--v2p-color-heart)}.v2p-icon-heart svg{fill:var(--v2p-color-heart-fill)}#Main .cell[id^=r] .v2p-auto-hide{overflow:hidden;display:inline-flex;width:0}#Main #reply-box .v2p-reply-preview{font-size:15px;line-height:1.6}#Main .cell:hover .v2p-topic-preview-btn,#Main .cell:hover .v2p-topic-ignore-btn,#Rightbar .cell:hover .v2p-topic-preview-btn,#Rightbar .cell:hover .v2p-topic-ignore-btn{visibility:visible}#Rightbar .v2p-info-row{display:block;font-size:12px;color:var(--v2p-color-accent-500);text-align:center}#Rightbar .v2p-info-row:hover{text-decoration:none;background-color:var(--v2p-color-accent-50)}#Rightbar .v2p-topic-preview-btn{position:absolute;top:0;right:0;bottom:0;height:20px;font-size:12px;backdrop-filter:blur(8px);box-shadow:0 0 0 3px var(--v2p-color-bg-content)}.v2p-tool-box{position:sticky;z-index:var(--zidx-tools-card);top:var(--v2p-layout-row-gap)}.v2p-tool-box .v2p-tools{display:grid;grid-auto-rows:auto;grid-template-columns:repeat(3, 1fr);gap:8px 15px;align-items:center;justify-content:center;font-size:12px;color:var(--v2p-color-font-secondary)}.v2p-tool{display:inline-flex;gap:0 5px;align-items:center;padding:3px 0}.v2p-tool:hover{color:var(--v2p-color-button-foreground-hover)}.v2p-tool .v2p-tool-icon{width:16px;height:16px}.v2p-topic-actions{display:inline-flex;gap:2px 10px}.v2p-topic-preview-btn{cursor:pointer;font-size:13px;color:var(--v2p-color-button-foreground);visibility:hidden;background-color:var(--v2p-color-button-background-hover);border:none;border-radius:3px;outline:none}.v2p-topic-ignore-btn{cursor:pointer;margin-left:8px;visibility:hidden}.v2p-topic-preview{font-size:15px;line-height:1.6;padding:var(--v2p-tp-preview-pd)}.v2p-tpr-loading{--tpr-h: 30px;--tpr-h-p: 22px;display:flex;flex-direction:column;gap:10px;padding:25px}.v2p-tpr-loading .v2p-tpr{background-color:var(--v2p-color-bg-tpr);border-radius:5px}.v2p-tpr-loading .v2p-tpr-info{display:flex;gap:15px;align-items:center}.v2p-tpr-info-avatar{width:var(--tpr-h);height:var(--tpr-h)}.v2p-tpr-info-text{width:50%;height:var(--tpr-h)}.v2p-tpr-loading .v2p-tpr-content{display:flex;flex-direction:column;gap:12px;padding:15px 0 20px}.v2p-tpr-loading .v2p-tpr-content .v2p-tpr-content-p{height:var(--tpr-h-p)}.v2p-tpr-loading .v2p-tpr-cmt{display:flex;gap:15px;padding-top:15px;border-top:1px solid var(--v2p-color-border)}.v2p-tpr-loading .v2p-tpr-cmt .v2p-tpr-cmt-avatar{width:50px;height:50px}.v2p-tpr-loading .v2p-tpr-cmt .v2p-tpr-cmt-right{display:flex;flex:1;flex-direction:column;gap:10px}.v2p-tpr-loading .v2p-tpr-cmt .v2p-tpr-cmt-right .v2p-tpr-cmt-header{width:50%;height:var(--tpr-h)}.v2p-tpr-loading .v2p-tpr-cmt .v2p-tpr-cmt-right .v2p-tpr-cmt-p{height:var(--tpr-h-p)}.v2p-tp-info-bar{display:flex;gap:10px;align-items:center;margin-bottom:10px}.v2p-tp-info,.v2p-tp-read{overflow:hidden;display:inline-flex;gap:20px;align-items:center;padding:5px 10px;font-size:13px;background-color:var(--v2p-color-button-background);border-radius:5px}.v2p-tp-read{cursor:pointer;user-select:none;gap:4px}.v2p-tp-read:hover{background-color:var(--v2p-color-button-background-hover)}.v2p-tp-read-icon{width:16px;height:16px}.v2p-tp-member{display:inline-flex;gap:5px;align-items:center;font-weight:bold}.v2p-tp-avatar{width:20px;height:20px;background-color:var(--v2p-color-bg-avatar);border-radius:3px}.v2p-topic-preview-addons{margin-top:30px}#Main.v2p-topic-preview>.box{border:1px solid var(--v2p-color-border)}a.v2p-topic-preview-title-link:hover{text-decoration:underline 1.5px;text-underline-offset:.46ex}.v2p-dot{margin:0 8px;font-size:15px;font-weight:800}.v2p-paging{background:none !important}.v2p-paging.cell{border-bottom:none}.v2p-modal-mask{position:fixed;z-index:var(--zidx-modal-mask);inset:0;overflow:hidden;overflow-y:auto;padding:min(2vh,60px);background-color:var(--v2p-color-mask)}.v2p-popup{font-size:14px;background:var(--v2p-color-bg-widget);backdrop-filter:blur(16px);border:1px solid var(--box-border-color);border-radius:8px;box-shadow:var(--v2p-widget-shadow);position:absolute;z-index:var(--zidx-popup);top:0;left:0}.v2p-popup-content{overflow-y:auto;width:max-content}.v2p-toast{position:fixed;z-index:var(--zidx-toast);top:50px;left:50%;transform:translateX(-50%);padding:10px 15px;font-size:14px;color:var(--v2p-color-background);background:var(--v2p-color-foreground);border-radius:8px;box-shadow:var(--v2p-toast-shadow)}.v2p-modal-main{position:relative;overflow:hidden;display:flex;flex-direction:column;box-sizing:border-box;width:800px;height:100%;margin:0 auto;background-color:var(--v2p-color-bg-content);border-radius:var(--box-border-radius)}.v2p-modal-header{display:flex;gap:0 20px;align-items:center;padding:15px 20px 20px;background-color:var(--v2p-color-bg-content);border-bottom:1px solid var(--box-border-color)}.v2p-modal-title{overflow:hidden;padding:2px 0;font-size:16px;font-weight:bold;text-overflow:ellipsis;white-space:nowrap}.v2p-modal-actions{display:flex;gap:0 10px;align-items:center;margin-left:auto}.v2p-modal-content{position:relative;overflow-y:auto;overscroll-behavior-y:contain;flex:1;outline:none}.v2p-modal-content #Main.v2p-topic-preview>.box{border:none;box-shadow:none}.v2p-modal-comments{position:absolute;inset:0;overflow-y:auto;padding:0 20px;visibility:hidden}.v2p-modal-comments.v2p-tab-content-active{z-index:20;visibility:visible}.v2p-modal-comment-tabs{display:flex;gap:4px;align-items:center;padding:4px 5px;font-size:14px;font-weight:normal;background-color:var(--button-background-color);border-radius:4px}.v2p-modal-comment-tabs>[data-tab-key]{cursor:pointer;padding:4px 5px;border-radius:4px}.v2p-modal-comment-tabs>[data-tab-key]:hover{background-color:var(--v2p-color-button-background-hover)}.v2p-modal-comment-tabs>[data-tab-key].v2p-tab-active{color:var(--v2p-color-foreground);background-color:var(--v2p-color-accent-100)}.v2p-no-pat{padding:30px 20px;font-size:15px;text-align:center}.v2p-no-pat .v2p-no-pat-title{font-size:16px;font-weight:bold}.v2p-no-pat .v2p-no-pat-desc{display:flex;align-items:center;justify-content:center;margin-top:15px}.v2p-no-pat .v2p-no-pat-block{display:inline-flex;align-items:center;margin:0 5px;padding:2px 10px;background-color:var(--v2p-color-bg-block);border-radius:2px}.v2p-no-pat .v2p-no-pat-steps{display:flex;flex-wrap:wrap;gap:20px;max-width:800px;margin-top:20px;padding:20px;background-color:var(--v2p-color-bg-block);border-radius:10px}.v2p-no-pat .v2p-no-pat-step{flex:1}.v2p-no-pat .v2p-no-pat-img{width:100%;border-radius:8px;box-shadow:var(--v2p-widget-shadow)}.v2p-no-pat .v2p-icon-logo{width:15px;height:15px}.v2p-likes-box{user-select:none;position:relative;top:3px;display:inline-flex;column-gap:5px;align-items:center}.v2p-likes-box.v2p-thanked{font-weight:bold;color:var(--v2p-color-heart);opacity:.8}.v2p-likes-box.v2p-thanked .v2p-icon-heart svg{fill:var(--v2p-color-heart)}@supports not selector(:has(*)){#Main .cell[id^=r]>table:hover .v2p-controls{opacity:1}}@supports selector(:has(*)){#Main .cell[id^=r]:not(:has(.cell:hover))>table:hover .v2p-auto-hide{width:auto}#Main .cell[id^=r]:not(:has(.cell:hover))>table:hover .v2p-controls{opacity:1}}.v2p-controls{display:inline-flex;column-gap:15px;align-items:center;margin-right:15px;font-size:12px;opacity:0}.v2p-controls>a{text-decoration:none}.v2p-control{position:relative;display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;padding:4px 0;color:var(--v2p-color-font-tertiary)}.v2p-control::after{pointer-events:none;z-index:var(--zidx-tip);overflow:hidden;width:max-content;min-width:30px;padding:2px 5px;font-size:12px;color:var(--v2p-color-foreground);text-align:center;white-space:nowrap;background-color:var(--v2p-color-bg-tooltip);border-radius:4px;box-shadow:var(--v2p-widget-shadow);position:absolute;top:-8px;transform:translateY(-100%);opacity:0}.v2p-control:hover{color:var(--v2p-color-font-secondary)}.v2p-control.v2p-thanked{cursor:default;color:var(--v2p-color-heart)}.v2p-control:hover::after{opacity:1}.v2p-control.v2p-control-hide::after{content:"\u9690\u85CF\u56DE\u590D"}.v2p-control.v2p-control-thank::after{content:"\u611F\u8C22\u56DE\u590D"}.v2p-control.v2p-control-thank.v2p-thanked::after{content:"\u5DF2\u611F\u8C22"}.v2p-control.v2p-control-reply::after{content:"\u56DE\u590D"}.topic_buttons .v2p-tb.v2p-hover-btn{color:var(--v2p-color-font-secondary)}.topic_buttons .v2p-tb.v2p-hover-btn::after{display:none}.topic_buttons .v2p-tb.v2p-hover-btn:hover{color:currentColor}.v2p-tb-icon{width:15px;height:15px}.v2p-emoji-container{overflow-y:auto;max-height:285px;padding:15px 18px;color:var(--v2p-color-font-secondary)}.v2p-member-card{max-width:300px;max-height:285px;padding:12px;font-size:13px;text-align:left}.v2p-member-card .v2p-info{display:flex;gap:15px}.v2p-member-card .v2p-info-right{padding:2px 0}.v2p-member-card .v2p-avatar-box{overflow:hidden;display:inline-block;width:73px;height:73px;background-color:var(--button-background-hover-color);border-radius:5px}.v2p-member-card .v2p-avatar{width:100%;height:100%}.v2p-member-card .v2p-username{font-size:16px;font-weight:bold}.v2p-member-card .v2p-no{margin:5px 0}.v2p-member-card .v2p-no,.v2p-member-card .v2p-created-date{width:160px;height:16px}.v2p-member-card .v2p-loading{background-color:var(--button-background-hover-color);border-radius:4px}.v2p-member-card .v2p-bio{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;line-height:1.4;margin-top:10px}.v2p-member-card-actions{padding:10px 0 0}.v2p-reply-tags{cursor:pointer;display:inline-flex;margin:0 0 2px;padding:0 3px;font-size:12px;background-color:var(--button-background-hover-color);border-radius:3px}.v2p-reply-tags-inline{overflow:hidden;max-width:300px;margin:0 5px 0 0;text-overflow:ellipsis;white-space:nowrap}.v2p-emoticons-box{font-size:15px}.v2p-emoji-group~.v2p-emoji-group{margin-top:10px}.v2p-emoji-title{margin:0 0 10px;font-size:14px;text-align:left}.v2p-emoji-list{display:grid;grid-template-columns:repeat(8, 1fr);gap:5px;font-size:20px}.v2p-emoji{cursor:pointer;height:20px;padding:3px;line-height:20px;border-radius:4px}.v2p-emoji:hover{background-color:var(--box-background-hover-color)}.v2p-emoji>img{height:100%}.v2p-decode{cursor:copy;position:relative;padding:2px 4px;font-size:13px;color:var(--v2p-color-orange-400);text-decoration:none;background-color:var(--v2p-color-orange-50)}.v2p-decode::after{pointer-events:none;z-index:var(--zidx-tip);overflow:hidden;width:max-content;min-width:30px;padding:2px 5px;font-size:12px;color:var(--v2p-color-foreground);text-align:center;white-space:nowrap;background-color:var(--v2p-color-bg-tooltip);border-radius:4px;box-shadow:var(--v2p-widget-shadow);content:attr(data-title);position:absolute;top:-8px;left:50%;transform:translate(-50%, -100%);opacity:0}.v2p-decode:hover{color:var(--v2p-color-orange-400)}.v2p-decode:hover::after{opacity:1}.v2p-reply-content{position:relative}.v2p-reply-content .v2p-expand-btn.normal.button{position:absolute;z-index:var(--zidx-expand-btn);bottom:5px;left:50%;transform:translateX(-50%);font-size:12px;font-weight:400}.v2p-reply-content.v2p-collapsed::before{pointer-events:none;content:"";position:absolute;z-index:var(--zidx-expand-mask);right:0;bottom:0;left:0;height:130px;background:linear-gradient(to top, var(--bg-reply) 10px, transparent)}.v2p-reply-content.v2p-collapsed .reply_content a,.v2p-reply-content.v2p-collapsed .reply_content .embedded_video{pointer-events:none}.v2p-reply-content.v2p-collapsed .v2p-expand-btn.normal.button{bottom:10px;transform:translateX(-50%)}.cell[id^=r] .cell[id^=r] .v2p-reply-content .v2p-expand-btn.normal.button{color:var(--v2p-color-button-foreground);background:var(--v2p-color-button-background-hover);box-shadow:var(--button-hover-shadow)}.v2p-empty-content{display:flex;flex-direction:column;align-items:center;padding-top:20px;font-size:14px;color:var(--v2p-color-font-secondary)}.v2p-empty-content .v2p-text-emoji{font-size:20px}.v2p-topic-reply-ref{margin:0 -10px 15px;padding:5px 10px;font-size:13px;color:var(--v2p-color-font-tertiary);background-color:var(--v2p-color-bg-block);border-radius:5px}.v2p-topic-reply-box{margin-top:50px;padding:30px 0;font-size:14px;line-height:1.55;color:var(--v2p-color-font-secondary);border-top:1px solid var(--v2p-color-divider)}.v2p-topic-reply~.v2p-topic-reply{margin-top:15px}.v2p-topic-reply-member{display:inline;font-weight:bold;color:var(--v2p-color-main-700)}.v2p-topic-reply-avatar{position:relative;top:2px;overflow:hidden;width:15px;height:15px;margin-right:5px;object-fit:cover;background-color:var(--v2p-color-bg-avatar);border-radius:2px}.v2p-topic-reply-content{display:inline}.v2p-topic-reply-tip{margin-top:20px;font-size:13px;color:var(--v2p-color-font-quaternary);text-align:center}.v2p-reply-wrap{resize:none;overflow:hidden;height:unset;min-height:140px !important;max-height:800px !important;font-size:15px;color:currentColor;background-color:var(--v2p-color-bg-input);border:1px solid var(--button-border-color);border-radius:8px;transition:opacity .25s}.v2p-reply-wrap::placeholder{font-size:15px;color:var(--v2p-color-font-tertiary)}.v2p-reply-wrap:is(:focus,:focus-within){background-color:rgba(0,0,0,0);outline:none;box-shadow:0 0 0 1px var(--button-border-color)}.v2p-reply-wrap #reply_content{background-color:rgba(0,0,0,0);border:none}.v2p-reply-wrap #reply_content::placeholder{font-size:14px;color:var(--v2p-color-font-tertiary)}.v2p-reply-wrap #reply_content:focus{background-color:var(--v2p-color-bg-content);outline:none}.v2p-reply-upload-bar{cursor:pointer;padding:6px 10px;font-size:12px;color:var(--v2p-color-font-tertiary);background-color:var(--v2p-color-bg-input);border-top:1px dashed var(--v2p-color-border-darker)}.v2p-reply-upload-bar-disabled{pointer-events:none}.v2p-footer{position:relative;display:flex;align-items:center;justify-content:space-between;padding:35px 10px 20px;font-size:12px;color:var(--v2p-color-font-tertiary);border-top:1px solid var(--v2p-color-divider)}.v2p-footer a:hover{text-decoration:none}.v2p-footer-logo{--logo-size: 16px;position:absolute;top:calc(-1*(var(--logo-size) + 5px)/2);left:50%;transform:translateX(-50%);display:inline-flex;box-sizing:border-box;padding:3px 25px;background-color:var(--v2p-color-bg-footer)}.v2p-footer-logo svg{width:var(--logo-size)}.v2p-footer-text{display:inline-flex;align-items:center;justify-content:flex-start;width:240px;color:var(--v2p-color-font-secondary)}.v2p-footer-links{display:inline-flex;gap:0 8px;align-items:center}.v2p-footer-link{padding:4px 5px;color:currentColor}.v2p-footer-brand{display:inline-flex;gap:0 15px;align-items:center;justify-content:flex-end;width:240px}.v2p-footer-brand .v2p-github-ref{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:2px 0}.v2p-color-mode-toggle{width:22px;height:22px;opacity:.8}.v2p-color-mode-toggle:hover{opacity:1}.v2p-reply-tabs{display:flex;gap:0 6px;align-items:center;font-size:14px}.v2p-reply-tabs .v2p-reply-tab{cursor:pointer;padding:2px 3px}.v2p-reply-tabs .v2p-reply-tab.active{text-decoration:underline;text-decoration-color:var(--v2p-color-font-tertiary);text-decoration-thickness:2px;text-underline-offset:4px}.v2p-select-dropdown{padding:5px;font-size:12px;border-radius:5px}.v2p-select-item{cursor:pointer;padding:5px 10px;white-space:nowrap;border-radius:4px}.v2p-select-item:hover{background-color:var(--v2p-color-button-background-hover)}.v2p-select-item.v2p-select-item-active{background-color:var(--v2p-color-accent-50)}.v2p-preview-retry,.v2p-topic-preview-retry,a.v2p-preview-retry,a.v2p-topic-preview-retry,a:link.v2p-preview-retry,a:link.v2p-topic-preview-retry{text-decoration:underline 1px;text-underline-offset:var(--v2p-underline-offset);cursor:pointer}.v2p-indent .v2p-member-ref{display:none}.v2p-member-ref+br{display:none}.v2p-member-ref+br+b{display:none}.v2p-member-ref.v2p-member-ref-show{display:inline}.v2p-layout-toggle{display:inline-block;width:18px;height:18px;padding:4px 2px;color:var(--v2p-color-font-tertiary)}.v2p-content-layout.v2p-content-layout{max-width:2000px}.v2p-content-layout.v2p-content-layout .v2p-horizontal-layout{display:flex;flex-wrap:wrap;gap:var(--v2p-layout-column-gap)}.v2p-left-side{flex:1}.v2p-left-side>.box{position:sticky;top:var(--v2p-layout-row-gap);display:flex;flex-direction:column;max-height:calc(100vh - 2*var(--v2p-layout-row-gap))}.v2p-left-side>.box>.header{flex-shrink:0}.v2p-left-side .v2p-left-side-content{overflow:auto;flex:1;border-bottom:1px solid var(--box-border-color)}.v2p-right-side{flex:1}.v2p-register-days{display:inline-flex;align-items:center;margin-left:2px;padding:0 2px;color:var(--v2p-color-orange-400);background-color:var(--v2p-color-orange-100);border-radius:2px}.v2p-register-days.v2p-register-days-large{display:inline-flex;margin-left:10px;padding:2px 5px;font-size:16px;line-height:1.4;border-radius:4px}.v2p-topics-hot-loading{display:flex;align-items:center;justify-content:center;padding:50px 0;color:currentColor}.v2p-topics-hot-loading .v2p-icon-loading{width:40px}.v2p-topics-hot-header{display:flex;align-items:center}.v2p-topics-hot-picker{cursor:pointer;display:inline-flex;gap:4px;align-items:center;margin-left:auto;padding:1px 6px;font-size:13px;background-color:var(--v2p-color-button-background);border-radius:4px}.v2p-topics-hot-picker:hover{background-color:var(--v2p-color-button-background-hover)}.v2p-topics-hot-icon{position:relative;top:-2px;width:1em;height:1em}.v2p-tag-block{margin-bottom:10px}@supports(filter: blur(6px)){.v2p-hide-account{opacity:.5;filter:blur(6px)}}@supports not (filter: blur(6px)){.v2p-hide-account{opacity:0}}@supports(filter: blur(6px)){.v2p-hide-balance{opacity:.5;filter:blur(6px)}.v2p-hide-balance:hover{opacity:1;filter:none}}@supports not (filter: blur(6px)){.v2p-hide-balance{opacity:0}.v2p-hide-balance:hover{opacity:1}}.v2p-member-name{display:flex;align-items:center}.v2p-settings-header.page-content-header{gap:10px}.v2p-settings-header.page-content-header .v2p-settings-icon{width:40px;height:40px;margin:0}.v2p-settings-header.page-content-header .v2p-settings-title{padding:0;white-space:nowrap}.v2p-settings-header.page-content-header .v2p-settings-actions{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end}.v2p-link-preview-btn{cursor:pointer;user-select:none;display:inline-flex;height:100%;margin-left:5px;padding:0 5px;font-size:12px;color:var(--v2p-color-bg-content);background-color:var(--v2p-color-accent-500);border-radius:3px}.v2p-link-preview-btn:hover{background-color:var(--v2p-color-accent-600)}#node_sidebar.v2p-node-sidebar-flamewar{background-color:var(--v2p-color-orange-50);border:1px solid var(--v2p-color-orange-400);border-radius:var(--v2p-box-radius)}
  5048. `;
  5049.  
  5050. // src/user-scripts/index.ts
  5051. function runAfterLoaded(fn) {
  5052. if (document.readyState !== "loading") {
  5053. fn();
  5054. } else {
  5055. document.addEventListener("DOMContentLoaded", () => {
  5056. fn();
  5057. });
  5058. }
  5059. }
  5060. if (typeof window.GM_addStyle !== "undefined") {
  5061. window.GM_addStyle(style);
  5062. } else {
  5063. runAfterLoaded(() => {
  5064. $(`<style type='text/css'>${style}</style>`).appendTo("head");
  5065. });
  5066. }
  5067. var allowedHosts = [
  5068. "https://v2ex.com",
  5069. "https://www.v2ex.com",
  5070. "https://cn.v2ex.com",
  5071. "https://jp.v2ex.com",
  5072. "https://de.v2ex.com",
  5073. "https://us.v2ex.com",
  5074. "https://hk.v2ex.com",
  5075. "https://global.v2ex.com",
  5076. "https://fast.v2ex.com",
  5077. "https://s.v2ex.com",
  5078. "https://origin.v2ex.com",
  5079. "https://staging.v2ex.com"
  5080. ];
  5081. var commonRegex = patternToRegex(...allowedHosts.map((host) => `${host}/*`));
  5082. var topicRegex = patternToRegex(...allowedHosts.map((host) => `${host}/t/*`));
  5083. var writeRegex = patternToRegex(...allowedHosts.map((host) => `${host}/write/*`));
  5084. runAfterLoaded(() => {
  5085. const url = window.location.href;
  5086. void (async () => {
  5087. if (commonRegex.test(url)) {
  5088. await Promise.resolve().then(() => (init_common(), common_exports));
  5089. await Promise.resolve().then(() => (init_home(), home_exports));
  5090. }
  5091. if (topicRegex.test(url)) {
  5092. await Promise.resolve().then(() => (init_topic(), topic_exports));
  5093. }
  5094. if (writeRegex.test(url)) {
  5095. await Promise.resolve().then(() => (init_write2(), write_exports));
  5096. }
  5097. })();
  5098. });
  5099. /*! Bundled license information:
  5100.  
  5101. lucide/dist/esm/createElement.js:
  5102. (**
  5103. * @license lucide v0.445.0 - ISC
  5104. *
  5105. * This source code is licensed under the ISC license.
  5106. * See the LICENSE file in the root directory of this source tree.
  5107. *)
  5108.  
  5109. lucide/dist/esm/replaceElement.js:
  5110. (**
  5111. * @license lucide v0.445.0 - ISC
  5112. *
  5113. * This source code is licensed under the ISC license.
  5114. * See the LICENSE file in the root directory of this source tree.
  5115. *)
  5116.  
  5117. lucide/dist/esm/defaultAttributes.js:
  5118. (**
  5119. * @license lucide v0.445.0 - ISC
  5120. *
  5121. * This source code is licensed under the ISC license.
  5122. * See the LICENSE file in the root directory of this source tree.
  5123. *)
  5124.  
  5125. lucide/dist/esm/icons/book-open-check.js:
  5126. (**
  5127. * @license lucide v0.445.0 - ISC
  5128. *
  5129. * This source code is licensed under the ISC license.
  5130. * See the LICENSE file in the root directory of this source tree.
  5131. *)
  5132.  
  5133. lucide/dist/esm/icons/chevron-down.js:
  5134. (**
  5135. * @license lucide v0.445.0 - ISC
  5136. *
  5137. * This source code is licensed under the ISC license.
  5138. * See the LICENSE file in the root directory of this source tree.
  5139. *)
  5140.  
  5141. lucide/dist/esm/icons/chevrons-up.js:
  5142. (**
  5143. * @license lucide v0.445.0 - ISC
  5144. *
  5145. * This source code is licensed under the ISC license.
  5146. * See the LICENSE file in the root directory of this source tree.
  5147. *)
  5148.  
  5149. lucide/dist/esm/icons/eye-off.js:
  5150. (**
  5151. * @license lucide v0.445.0 - ISC
  5152. *
  5153. * This source code is licensed under the ISC license.
  5154. * See the LICENSE file in the root directory of this source tree.
  5155. *)
  5156.  
  5157. lucide/dist/esm/icons/heart.js:
  5158. (**
  5159. * @license lucide v0.445.0 - ISC
  5160. *
  5161. * This source code is licensed under the ISC license.
  5162. * See the LICENSE file in the root directory of this source tree.
  5163. *)
  5164.  
  5165. lucide/dist/esm/icons/house.js:
  5166. (**
  5167. * @license lucide v0.445.0 - ISC
  5168. *
  5169. * This source code is licensed under the ISC license.
  5170. * See the LICENSE file in the root directory of this source tree.
  5171. *)
  5172.  
  5173. lucide/dist/esm/icons/message-square-plus.js:
  5174. (**
  5175. * @license lucide v0.445.0 - ISC
  5176. *
  5177. * This source code is licensed under the ISC license.
  5178. * See the LICENSE file in the root directory of this source tree.
  5179. *)
  5180.  
  5181. lucide/dist/esm/icons/message-square.js:
  5182. (**
  5183. * @license lucide v0.445.0 - ISC
  5184. *
  5185. * This source code is licensed under the ISC license.
  5186. * See the LICENSE file in the root directory of this source tree.
  5187. *)
  5188.  
  5189. lucide/dist/esm/icons/moon.js:
  5190. (**
  5191. * @license lucide v0.445.0 - ISC
  5192. *
  5193. * This source code is licensed under the ISC license.
  5194. * See the LICENSE file in the root directory of this source tree.
  5195. *)
  5196.  
  5197. lucide/dist/esm/icons/package-plus.js:
  5198. (**
  5199. * @license lucide v0.445.0 - ISC
  5200. *
  5201. * This source code is licensed under the ISC license.
  5202. * See the LICENSE file in the root directory of this source tree.
  5203. *)
  5204.  
  5205. lucide/dist/esm/icons/panel-right.js:
  5206. (**
  5207. * @license lucide v0.445.0 - ISC
  5208. *
  5209. * This source code is licensed under the ISC license.
  5210. * See the LICENSE file in the root directory of this source tree.
  5211. *)
  5212.  
  5213. lucide/dist/esm/icons/panel-top.js:
  5214. (**
  5215. * @license lucide v0.445.0 - ISC
  5216. *
  5217. * This source code is licensed under the ISC license.
  5218. * See the LICENSE file in the root directory of this source tree.
  5219. *)
  5220.  
  5221. lucide/dist/esm/icons/smile.js:
  5222. (**
  5223. * @license lucide v0.445.0 - ISC
  5224. *
  5225. * This source code is licensed under the ISC license.
  5226. * See the LICENSE file in the root directory of this source tree.
  5227. *)
  5228.  
  5229. lucide/dist/esm/icons/square-arrow-up-right.js:
  5230. (**
  5231. * @license lucide v0.445.0 - ISC
  5232. *
  5233. * This source code is licensed under the ISC license.
  5234. * See the LICENSE file in the root directory of this source tree.
  5235. *)
  5236.  
  5237. lucide/dist/esm/icons/star.js:
  5238. (**
  5239. * @license lucide v0.445.0 - ISC
  5240. *
  5241. * This source code is licensed under the ISC license.
  5242. * See the LICENSE file in the root directory of this source tree.
  5243. *)
  5244.  
  5245. lucide/dist/esm/icons/sun.js:
  5246. (**
  5247. * @license lucide v0.445.0 - ISC
  5248. *
  5249. * This source code is licensed under the ISC license.
  5250. * See the LICENSE file in the root directory of this source tree.
  5251. *)
  5252.  
  5253. lucide/dist/esm/icons/twitter.js:
  5254. (**
  5255. * @license lucide v0.445.0 - ISC
  5256. *
  5257. * This source code is licensed under the ISC license.
  5258. * See the LICENSE file in the root directory of this source tree.
  5259. *)
  5260.  
  5261. lucide/dist/esm/lucide.js:
  5262. (**
  5263. * @license lucide v0.445.0 - ISC
  5264. *
  5265. * This source code is licensed under the ISC license.
  5266. * See the LICENSE file in the root directory of this source tree.
  5267. *)
  5268. */
  5269.