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

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

目前为 2023-05-19 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name ▲V2EX Polish - 体验更现代化的 V2EX 🟢
  3. // @namespace LeoKu(https://leoku.top)
  4. // @version 1.4.3.1
  5. // @description 一款专为 V2EX 用户设计的浏览器插件,提供了丰富的扩展功能,让原生页面焕然一新!✨
  6. // @author LeoKu
  7. // @match https://*.v2ex.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=v2ex.com
  9. // @run-at document-start
  10. // @grant GM_addStyle
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. "use strict";
  15. var __getOwnPropNames = Object.getOwnPropertyNames;
  16. var __esm = (fn, res) => function __init() {
  17. return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
  18. };
  19.  
  20. // src/constants.ts
  21. var EXTENSION_NAME, emoticons, READABLE_CONTENT_HEIGHT, MAX_CONTENT_HEIGHT, dataExpiryTime, imgurClientIdPool, defaultOptions;
  22. var init_constants = __esm({
  23. "src/constants.ts"() {
  24. "use strict";
  25. EXTENSION_NAME = "V2EX_Polish";
  26. emoticons = [
  27. {
  28. title: "\u5C0F\u9EC4\u8138",
  29. list: [
  30. "\u{1F600}",
  31. "\u{1F601}",
  32. "\u{1F602}",
  33. "\u{1F923}",
  34. "\u{1F605}",
  35. "\u{1F60A}",
  36. "\u{1F60B}",
  37. "\u{1F618}",
  38. "\u{1F970}",
  39. "\u{1F617}",
  40. "\u{1F929}",
  41. "\u{1F914}",
  42. "\u{1F928}",
  43. "\u{1F610}",
  44. "\u{1F611}",
  45. "\u{1F644}",
  46. "\u{1F60F}",
  47. "\u{1F62A}",
  48. "\u{1F62B}",
  49. "\u{1F971}",
  50. "\u{1F61C}",
  51. "\u{1F612}",
  52. "\u{1F614}",
  53. "\u{1F628}",
  54. "\u{1F630}",
  55. "\u{1F631}",
  56. "\u{1F975}",
  57. "\u{1F621}",
  58. "\u{1F973}",
  59. "\u{1F97A}",
  60. "\u{1F92D}",
  61. "\u{1F9D0}",
  62. "\u{1F60E}",
  63. "\u{1F913}",
  64. "\u{1F62D}",
  65. "\u{1F911}",
  66. "\u{1F92E}"
  67. ]
  68. },
  69. {
  70. title: "\u624B\u52BF",
  71. list: [
  72. "\u{1F64B}",
  73. "\u{1F64E}",
  74. "\u{1F645}",
  75. "\u{1F647}",
  76. "\u{1F937}",
  77. "\u{1F90F}",
  78. "\u{1F449}",
  79. "\u270C\uFE0F",
  80. "\u{1F918}",
  81. "\u{1F919}",
  82. "\u{1F44C}",
  83. "\u{1F90C}",
  84. "\u{1F44D}",
  85. "\u{1F44E}",
  86. "\u{1F44B}",
  87. "\u{1F91D}",
  88. "\u{1F64F}",
  89. "\u{1F44F}"
  90. ]
  91. },
  92. {
  93. title: "\u5E86\u795D",
  94. list: ["\u2728", "\u{1F389}", "\u{1F38A}"]
  95. },
  96. {
  97. title: "\u5176\u4ED6",
  98. 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}"]
  99. }
  100. ];
  101. READABLE_CONTENT_HEIGHT = 250;
  102. MAX_CONTENT_HEIGHT = 550;
  103. dataExpiryTime = 60 * 60 * 1e3;
  104. imgurClientIdPool = [
  105. "3107b9ef8b316f3",
  106. // 以下 Client ID 来自「V2EX Plus」
  107. "442b04f26eefc8a",
  108. "59cfebe717c09e4",
  109. "60605aad4a62882",
  110. "6c65ab1d3f5452a",
  111. "83e123737849aa9",
  112. "9311f6be1c10160",
  113. "c4a4a563f698595",
  114. "81be04b9e4a08ce"
  115. ];
  116. defaultOptions = {
  117. openInNewTab: false,
  118. autoCheckIn: {
  119. enabled: true
  120. },
  121. theme: {
  122. autoSwitch: false
  123. },
  124. reply: {
  125. preload: "off"
  126. },
  127. replyContent: {
  128. autoFold: true
  129. },
  130. nestedReply: {
  131. display: "indent"
  132. }
  133. };
  134. }
  135. });
  136.  
  137. // src/deep-merge.ts
  138. function isObject(value) {
  139. return typeof value === "object" && value !== null && !Array.isArray(value);
  140. }
  141. function deepMerge(target, source) {
  142. const result = {};
  143. for (const key in target) {
  144. const targetProp = target[key];
  145. const sourceProp = source[key];
  146. if (isObject(targetProp) && isObject(sourceProp)) {
  147. result[key] = deepMerge(targetProp, sourceProp);
  148. } else if (Reflect.has(source, key)) {
  149. result[key] = sourceProp;
  150. } else {
  151. result[key] = targetProp;
  152. }
  153. }
  154. for (const key in source) {
  155. if (!Reflect.has(target, key)) {
  156. result[key] = source[key];
  157. }
  158. }
  159. return result;
  160. }
  161. var init_deep_merge = __esm({
  162. "src/deep-merge.ts"() {
  163. "use strict";
  164. }
  165. });
  166.  
  167. // src/icons.ts
  168. var iconEmoji, iconHeart, iconHide, iconReply, iconStar, iconTwitter, iconIgnore, iconLove, iconLoading, iconLogo, iconChromeWebStore, iconGitHub, iconScrollTop, iconTool, iconLight, iconDark, iconBookMark;
  169. var init_icons = __esm({
  170. "src/icons.ts"() {
  171. "use strict";
  172. iconEmoji = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width:18px; height:18px;" ><path stroke-linecap="round" stroke-linejoin="round" d="M15.182 15.182a4.5 4.5 0 01-6.364 0M21 12a9 9 0 11-18 0 9 9 0 0118 0zM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75zm-.375 0h.008v.015h-.008V9.75zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75zm-.375 0h.008v.015h-.008V9.75z" /></svg>`;
  173. iconHeart = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" ><path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" /></svg>`;
  174. iconHide = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" ><path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" /></svg>`;
  175. iconReply = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" ><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" /></svg>`;
  176. iconStar = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><polygon 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" ></polygon></svg>`;
  177. iconTwitter = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path 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" ></path></svg>`;
  178. iconIgnore = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><circle cx="12" cy="12" r="10"></circle><path d="M8 15h8"></path><path d="M8 9h2"></path><path d="M14 9h2"></path></svg>`;
  179. iconLove = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><path d="M20.42 4.58a5.4 5.4 0 0 0-7.65 0l-.77.78-.77-.78a5.4 5.4 0 0 0-7.65 0C1.46 6.7 1.33 10.28 4 13l8 8 8-8c2.67-2.72 2.54-6.3.42-8.42z" ></path></svg>`;
  180. iconLoading = `<svg version="1.1" id="L4" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 0 0" xml:space="preserve"><circle fill="currentcolor" stroke="none" cx="6" cy="50" r="6"><animate attributeName="opacity" dur="1s" values="0;1;0" repeatCount="indefinite" begin="0.1"/></circle><circle fill="currentcolor" stroke="none" cx="26" cy="50" r="6"><animate attributeName="opacity" dur="1s" values="0;1;0" repeatCount="indefinite" begin="0.2"/></circle><circle fill="currentcolor" stroke="none" cx="46" cy="50" r="6"><animate attributeName="opacity" dur="1s" values="0;1;0" repeatCount="indefinite" begin="0.3"/></circle></svg>`;
  181. iconLogo = `<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>`;
  182. iconChromeWebStore = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 152.01 132"><defs><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="39.16" y1="775.015" x2="152.84" y2="775.015" gradientTransform="translate(0 -650)"><stop offset="0" style="stop-color:#d93025"/><stop offset="1" style="stop-color:#ea4335"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="-1169.827" y1="59.741" x2="-1056.123" y2="59.741" gradientTransform="rotate(-120 -489.637 -232.003)"><stop offset="0" style="stop-color:#1e8e3e"/><stop offset="1" style="stop-color:#34a853"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="56.707" y1="-664.775" x2="170.407" y2="-664.775" gradientTransform="rotate(120 -125.508 -249.005)"><stop offset="0" style="stop-color:#fbbc04"/><stop offset="1" style="stop-color:#fcc934"/></linearGradient><linearGradient xlink:href="#a" id="d" gradientUnits="userSpaceOnUse" gradientTransform="translate(-20 -678)" x1="39.16" y1="775.015" x2="152.84" y2="775.015"/><linearGradient xlink:href="#b" id="e" gradientUnits="userSpaceOnUse" gradientTransform="rotate(-120 -507.72 -240.23)" x1="-1169.827" y1="59.741" x2="-1056.123" y2="59.741"/><linearGradient xlink:href="#c" id="f" gradientUnits="userSpaceOnUse" gradientTransform="rotate(120 -127.425 -268.779)" x1="56.707" y1="-664.775" x2="170.407" y2="-664.775"/></defs><path d="M0 0v121.63C0 127.35 4.64 132 10.37 132h131.27c5.72 0 10.37-4.64 10.37-10.37L152 0Zm58.73 14.52h34.54c3.82 0 6.91 3.09 6.91 6.91 0 3.81-3.09 6.91-6.91 6.91H58.73c-3.82 0-6.91-3.09-6.91-6.91 0-3.82 3.09-6.91 6.91-6.91z" fill="#f1f3f4"/><path d="M0 0v121.63C0 127.35 4.64 132 10.37 132h131.27c5.72 0 10.37-4.64 10.37-10.37L152 0Zm58.73 14.52h34.54c3.82 0 6.91 3.09 6.91 6.91 0 3.81-3.09 6.91-6.91 6.91H58.73c-3.82 0-6.91-3.09-6.91-6.91 0-3.82 3.09-6.91 6.91-6.91z" fill="#f1f3f4"/><path d="M0 0v66.35h152V0Zm58.73 14.52h34.54c3.82 0 6.91 3.09 6.91 6.91 0 3.81-3.09 6.91-6.91 6.91H58.73c-3.82 0-6.91-3.09-6.91-6.91 0-3.82 3.09-6.91 6.91-6.91z" fill="#e8eaed"/><path style="fill:url(#d)" d="M76 55.99c-24.29 0-45.49 13.19-56.84 32.81l9.05 27.61L42.18 132h8.894L76 88.81l56.84-.01C121.49 69.18 100.29 55.99 76 55.99Z"/><path style="fill:url(#e)" d="M19.16 88.8C11.694 101.767 8.8 117.054 11.186 132h32.908z"/><path style="fill:url(#f)" d="M76 88.81 100.934 132h39.88c2.387-14.943-.508-30.225-7.974-43.19z"/><path style="fill:#f1f3f4" d="M76 88.81c-18.12 0-32.81 14.695-32.81 32.82 0 3.626.598 7.11 1.683 10.37h62.254a32.771 32.771 0 0 0 1.684-10.37c0-18.125-14.69-32.82-32.811-32.82Z"/><path d="M76 94.96a26.66 26.67 0 0 0-26.66 26.67A26.66 26.67 0 0 0 51.486 132h49.028a26.66 26.67 0 0 0 2.146-10.37A26.66 26.67 0 0 0 76 94.96Z" fill="#1a73e8"/><path opacity=".1" fill="#bdc1c6" d="M0 66.35h152v.86H0zM0 65.48h152v.86H0z"/></svg>`;
  183. iconGitHub = `<svg viewBox="0 0 24 24" aria-hidden="true"><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></svg>`;
  184. iconScrollTop = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l7.5-7.5 7.5 7.5m-15 6l7.5-7.5 7.5 7.5" /></svg>`;
  185. iconTool = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 16.875h3.375m0 0h3.375m-3.375 0V13.5m0 3.375v3.375M6 10.5h2.25a2.25 2.25 0 002.25-2.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v2.25A2.25 2.25 0 006 10.5zm0 9.75h2.25A2.25 2.25 0 0010.5 18v-2.25a2.25 2.25 0 00-2.25-2.25H6a2.25 2.25 0 00-2.25 2.25V18A2.25 2.25 0 006 20.25zm9.75-9.75H18a2.25 2.25 0 002.25-2.25V6A2.25 2.25 0 0018 3.75h-2.25A2.25 2.25 0 0013.5 6v2.25a2.25 2.25 0 002.25 2.25z" /></svg>`;
  186. iconLight = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" /></svg>`;
  187. iconDark = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" /></svg>`;
  188. iconBookMark = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"></path><line x1="12" x2="12" y1="7" y2="13"></line><line x1="15" x2="9" y1="10" y2="10"></line></svg>`;
  189. }
  190. });
  191.  
  192. // src/utils.ts
  193. function getOS() {
  194. const userAgent = window.navigator.userAgent.toLowerCase();
  195. const macosPlatforms = /(macintosh|macintel|macppc|mac68k|macos)/i;
  196. const windowsPlatforms = /(win32|win64|windows|wince)/i;
  197. const iosPlatforms = /(iphone|ipad|ipod)/i;
  198. let os = null;
  199. if (macosPlatforms.test(userAgent)) {
  200. os = "macos";
  201. } else if (iosPlatforms.test(userAgent)) {
  202. os = "ios";
  203. } else if (windowsPlatforms.test(userAgent)) {
  204. os = "windows";
  205. } else if (userAgent.includes("android")) {
  206. os = "android";
  207. } else if (userAgent.includes("linux")) {
  208. os = "linux";
  209. }
  210. return os;
  211. }
  212. function formatTimestamp(timestamp, { format = "YMD" } = {}) {
  213. const date = new Date(timestamp.toString().length === 10 ? timestamp * 1e3 : timestamp);
  214. const year = date.getFullYear().toString();
  215. const month = (date.getMonth() + 1).toString().padStart(2, "0");
  216. const day = date.getDate().toString().padStart(2, "0");
  217. const YMD = `${year}-${month}-${day}`;
  218. if (format === "YMDHMS") {
  219. const hour = date.getHours().toString().padStart(2, "0");
  220. const minute = date.getMinutes().toString().padStart(2, "0");
  221. const second = date.getSeconds().toString().padStart(2, "0");
  222. return `${YMD} ${hour}:${minute}:${second}`;
  223. }
  224. return YMD;
  225. }
  226. function isSameDay(timestamp1, timestamp2) {
  227. const date1 = new Date(timestamp1);
  228. const date2 = new Date(timestamp2);
  229. return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate();
  230. }
  231. function getRunEnv() {
  232. if (typeof chrome === "object" && typeof chrome.extension !== "undefined") {
  233. return "chrome";
  234. }
  235. if (typeof browser === "object" && typeof browser.extension !== "undefined") {
  236. return "web-ext";
  237. }
  238. return null;
  239. }
  240. function getStorage(useCache = true) {
  241. return new Promise((resolve, reject) => {
  242. if (useCache) {
  243. if (typeof window !== "undefined" && window.__V2P_StorageCache) {
  244. resolve(window.__V2P_StorageCache);
  245. }
  246. }
  247. const runEnv = getRunEnv();
  248. if (!(runEnv === "chrome" || runEnv === "web-ext")) {
  249. const data = { ["options" /* Options */]: defaultOptions };
  250. if (typeof window !== "undefined") {
  251. window.__V2P_StorageCache = data;
  252. }
  253. resolve(data);
  254. } else {
  255. chrome.storage.sync.get().then((items) => {
  256. let data;
  257. const options = items["options" /* Options */];
  258. if (options) {
  259. data = { ...items, ["options" /* Options */]: deepMerge(defaultOptions, options) };
  260. } else {
  261. data = { ...items, ["options" /* Options */]: defaultOptions };
  262. }
  263. if (typeof window !== "undefined") {
  264. window.__V2P_StorageCache = data;
  265. }
  266. resolve(data);
  267. }).catch((err) => {
  268. reject(err);
  269. });
  270. }
  271. });
  272. }
  273. function getStorageSync() {
  274. const storage = window.__V2P_StorageCache;
  275. if (!storage) {
  276. throw new Error(`${EXTENSION_NAME}: \u65E0\u53EF\u7528\u7684 Storage \u7F13\u5B58\u6570\u636E`);
  277. }
  278. return storage;
  279. }
  280. async function setStorage(storageKey, storageItem) {
  281. switch (storageKey) {
  282. case "options" /* Options */:
  283. case "api" /* API */:
  284. case "daily" /* Daily */:
  285. case "member-tag" /* MemberTag */:
  286. case "settings-sync" /* SyncInfo */:
  287. case "reading-list" /* ReadingList */:
  288. await chrome.storage.sync.set({ [storageKey]: storageItem });
  289. break;
  290. default:
  291. throw new Error(`\u672A\u77E5\u7684 storageKey\uFF1A ${storageKey}`);
  292. }
  293. }
  294. function escapeHTML(htmlString) {
  295. return htmlString.replace(/[<>&"'']/g, (match) => {
  296. switch (match) {
  297. case "<":
  298. return "&lt;";
  299. case ">":
  300. return "&gt;";
  301. case "&":
  302. return "&amp;";
  303. case '"':
  304. return "&quot;";
  305. case "'":
  306. return "&#39;";
  307. default:
  308. return match;
  309. }
  310. });
  311. }
  312. var init_utils = __esm({
  313. "src/utils.ts"() {
  314. "use strict";
  315. init_constants();
  316. init_deep_merge();
  317. }
  318. });
  319.  
  320. // src/contents/common.ts
  321. var common_exports = {};
  322. var init_common = __esm({
  323. "src/contents/common.ts"() {
  324. "use strict";
  325. init_constants();
  326. init_deep_merge();
  327. init_icons();
  328. init_utils();
  329. void (async () => {
  330. const storage = await getStorage();
  331. const options = storage["options" /* Options */];
  332. {
  333. const $toggle = $("#Rightbar .light-toggle").addClass("v2p-color-mode-toggle");
  334. const $toggleImg = $toggle.find("> img");
  335. const alt = $toggleImg.prop("alt");
  336. if (alt === "Light") {
  337. $toggle.prop("title", "\u4F7F\u7528\u6DF1\u8272\u4E3B\u9898");
  338. $toggleImg.replaceWith(iconDark);
  339. } else if (alt === "Dark") {
  340. $toggle.prop("title", "\u4F7F\u7528\u6D45\u8272\u4E3B\u9898");
  341. $toggleImg.replaceWith(iconLight);
  342. }
  343. if (options.theme.autoSwitch) {
  344. const perfersDark = window.matchMedia("(prefers-color-scheme: dark)");
  345. if (perfersDark.matches) {
  346. $("#Wrapper").addClass("Night");
  347. }
  348. perfersDark.addEventListener("change", ({ matches }) => {
  349. if (matches) {
  350. $("#Wrapper").addClass("Night");
  351. } else {
  352. $("#Wrapper").removeClass("Night");
  353. }
  354. });
  355. $toggle.on("click", () => {
  356. void setStorage("options" /* Options */, deepMerge(options, { theme: { autoSwitch: false } }));
  357. });
  358. }
  359. }
  360. {
  361. $("#Top .site-nav .tools > .top").addClass("v2p-hover-btn");
  362. }
  363. {
  364. const $searchItem = $('<a class="search-item cell" target="_blank">');
  365. $searchItem.on("mouseover", () => {
  366. $("#search-result .search-item.active").addClass("v2p-no-active");
  367. $searchItem.addClass("active");
  368. }).on("mouseleave", () => {
  369. $("#search-result .search-item.active").removeClass("v2p-no-active");
  370. $searchItem.removeClass("active");
  371. });
  372. const $search = $("#search");
  373. $search.on("input", (ev) => {
  374. const value = ev.target.value;
  375. const $searchGroup = $("#search-result .search-item-group:last-of-type");
  376. $searchItem.text(`SOV2EX ${value}`).prop("href", `https://www.sov2ex.com/?q=${value}`);
  377. $searchGroup.append($searchItem);
  378. });
  379. }
  380. {
  381. const $extraFooter = $(`<div class="v2p-footer"><div class="v2p-footer-text">\u6269\u5C55\u81EA V2EX Polish </div><div class="v2p-footer-links"><a class="v2p-footer-link v2p-hover-btn" href="${"https://v2p.app" /* Home */}" target="_blank">\u63D2\u4EF6\u4E3B\u9875</a><a class="v2p-footer-link v2p-hover-btn" href="${"https://github.com/coolpace/V2EX_Polish/discussions/1" /* Feedback */}" target="_blank">\u95EE\u9898\u53CD\u9988</a></div><div class="v2p-footer-brand"><span><a href="https://chrome.google.com/webstore/detail/v2ex-polish/onnepejgdiojhiflfoemillegpgpabdm" target="_blank" title="Chrome \u5E94\u7528\u5546\u5E97" >${iconChromeWebStore}</a></span><span><a href="https://github.com/coolpace/V2EX_Polish" target="_blank" title="GitHub \u4ED3\u5E93" >${iconGitHub}</a></span></div></div>`);
  382. $(`<div class="v2p-footer-logo">${iconLogo}</div>`).prependTo($extraFooter);
  383. $("#Bottom .content").append($extraFooter);
  384. }
  385. })();
  386. }
  387. });
  388.  
  389. // src/components/button.ts
  390. function createButton(props) {
  391. const { children, className = "", type = "button", tag = "button" } = props;
  392. const $button = $(`<${tag} class="normal button ${className}">${children}</${tag}>`);
  393. if (tag === "button") {
  394. $button.prop("type", type);
  395. }
  396. return $button;
  397. }
  398. var init_button = __esm({
  399. "src/components/button.ts"() {
  400. "use strict";
  401. }
  402. });
  403.  
  404. // src/components/model.ts
  405. function createModel(props) {
  406. const { root, title, onOpen, onClose, onMount } = props;
  407. const $mask = $('<div class="v2p-model-mask">');
  408. const $content = $('<div class="v2p-model-content">');
  409. const $closeBtn = createButton({
  410. children: "\u5173\u95ED<kbd>Esc</kbd>",
  411. className: "v2p-model-close-btn"
  412. });
  413. const $title = $(`<div class="v2p-model-title">${title ?? ""}</div>`);
  414. const $actions = $('<div class="v2p-model-actions">').append($closeBtn);
  415. const $header = $('<div class="v2p-model-header">').append($title, $actions);
  416. const $main = $('<div class="v2p-model-main">').append($header, $content).on("click", (ev) => ev.stopPropagation());
  417. const $container = $mask.append($main).hide();
  418. const modelElements = {
  419. $mask,
  420. $main,
  421. $container,
  422. $title,
  423. $actions,
  424. $content
  425. };
  426. let boundEvent = false;
  427. const maskClickHandler = () => {
  428. handleModalClose();
  429. };
  430. const keyupHandler = (ev) => {
  431. if (ev.key === "Escape") {
  432. handleModalClose();
  433. }
  434. };
  435. const handleModalClose = () => {
  436. $mask.off("click", maskClickHandler);
  437. $(document).off("keydown", keyupHandler);
  438. boundEvent = false;
  439. $container.fadeOut("fast");
  440. document.body.classList.remove("v2p-modal-open");
  441. onClose?.(modelElements);
  442. };
  443. const handleModalOpen = () => {
  444. setTimeout(() => {
  445. if (!boundEvent) {
  446. $mask.on("click", maskClickHandler);
  447. $(document).on("keydown", keyupHandler);
  448. boundEvent = true;
  449. }
  450. });
  451. $container.fadeIn("fast");
  452. document.body.classList.add("v2p-modal-open");
  453. onOpen?.(modelElements);
  454. };
  455. $closeBtn.on("click", handleModalClose);
  456. onMount?.(modelElements);
  457. if (root) {
  458. root.append($container);
  459. }
  460. return { ...modelElements, open: handleModalOpen, close: handleModalClose };
  461. }
  462. var init_model = __esm({
  463. "src/components/model.ts"() {
  464. "use strict";
  465. init_button();
  466. }
  467. });
  468.  
  469. // src/services.ts
  470. async function legacyRequest(url, options) {
  471. const res = await fetch(url, options);
  472. return res.json();
  473. }
  474. function fetchUserInfo(memberName, options) {
  475. return legacyRequest(
  476. `${V2EX_LEGACY_API}/members/show.json?username=${memberName}`,
  477. options
  478. );
  479. }
  480. async function request(url, options) {
  481. const storage = await getStorage();
  482. const PAT = storage["api" /* API */]?.pat;
  483. const res = await fetch(url, {
  484. ...options,
  485. headers: { Authorization: PAT ? `Bearer ${PAT}` : "", ...options?.headers }
  486. });
  487. {
  488. const limit = res.headers.get("X-Rate-Limit-Limit");
  489. const reset = res.headers.get("X-Rate-Limit-Reset");
  490. const remaining = res.headers.get("X-Rate-Limit-Remaining");
  491. const api = {
  492. pat: PAT,
  493. limit: limit ? Number(limit) : void 0,
  494. reset: reset ? Number(reset) : void 0,
  495. remaining: remaining ? Number(remaining) : void 0
  496. };
  497. void setStorage("api" /* API */, api);
  498. }
  499. const resultData = await res.json();
  500. if (typeof resultData.success === "boolean" && !resultData.success) {
  501. throw new Error(resultData.message, { cause: resultData });
  502. }
  503. return resultData;
  504. }
  505. function fetchTopic(topicId, options) {
  506. return request(`${V2EX_API}/topics/${topicId}`, { method: "GET", ...options });
  507. }
  508. function fetchTopicReplies(topicId, options) {
  509. return request(`${V2EX_API}/topics/${topicId}/replies`, {
  510. method: "GET",
  511. ...options
  512. });
  513. }
  514. async function uploadImage(file) {
  515. const formData = new FormData();
  516. formData.append("image", file);
  517. const randomIndex = Math.floor(Math.random() * imgurClientIdPool.length);
  518. const clidenId = imgurClientIdPool[randomIndex];
  519. const res = await fetch("https://api.imgur.com/3/upload", {
  520. method: "POST",
  521. headers: { Authorization: `Client-ID ${clidenId}` },
  522. body: formData
  523. });
  524. if (res.ok) {
  525. const resData = await res.json();
  526. if (resData.success) {
  527. return resData.data.link;
  528. }
  529. }
  530. throw new Error("\u4E0A\u4F20\u5931\u8D25");
  531. }
  532. async function fetchTopicPage(path, page) {
  533. const res = await fetch(`${"https://www.v2ex.com" /* Origin */}${path}?p=${page}`);
  534. const htmlText = await res.text();
  535. return htmlText;
  536. }
  537. var V2EX_ORIGIN, V2EX_LEGACY_API, V2EX_API, mark;
  538. var init_services = __esm({
  539. "src/services.ts"() {
  540. "use strict";
  541. init_constants();
  542. init_utils();
  543. V2EX_ORIGIN = window.location.origin.includes("v2ex.com") ? window.location.origin : "https://www.v2ex.com" /* Origin */;
  544. V2EX_LEGACY_API = `${V2EX_ORIGIN}/api`;
  545. V2EX_API = `${V2EX_ORIGIN}/api/v2`;
  546. mark = `${EXTENSION_NAME}_settings`;
  547. }
  548. });
  549.  
  550. // src/contents/globals.ts
  551. function updateCommentCells() {
  552. $commentCells = $commentBox.find('.cell[id^="r_"]');
  553. $commentTableRows = $commentCells.find("> table > tbody > tr");
  554. }
  555. var loginName, topicOwnerName, $topicList, $topicContentBox, $commentBox, $commentCells, $commentTableRows, $replyBox, $replyForm, colorTheme, $replyTextArea, replyTextArea;
  556. var init_globals = __esm({
  557. "src/contents/globals.ts"() {
  558. "use strict";
  559. loginName = $('#Top .tools > a[href^="/member"]').text();
  560. topicOwnerName = $('#Main > .box > .header > small > a[href^="/member"]').text();
  561. $topicList = $(
  562. "#Main #Tabs ~ .cell.item, #Main #TopicsNode > .cell, #Main .cell.item:has(.item_title > .topic-link)"
  563. );
  564. $topicContentBox = $("#Main .box:has(.topic_content)");
  565. $commentBox = $('#Main .box:has(.cell[id^="r_"])');
  566. $commentCells = $commentBox.find('.cell[id^="r_"]');
  567. $commentTableRows = $commentCells.find("> table > tbody > tr");
  568. $replyBox = $("#reply-box");
  569. $replyForm = $replyBox.find('form[action^="/t"]');
  570. colorTheme = $("#Wrapper").hasClass("Night") ? "dark" : "light";
  571. $replyTextArea = $("#reply_content");
  572. replyTextArea = document.querySelector("#reply_content");
  573. }
  574. });
  575.  
  576. // src/components/toast.ts
  577. function createToast(props) {
  578. const { message, duration = 3e3 } = props;
  579. const $existTosat = $(".v2p-toast");
  580. if ($existTosat.length > 0) {
  581. $existTosat.remove();
  582. }
  583. const $toast = $(`<div class="v2p-toast">${message}</div>`).hide();
  584. $(document.body).append($toast);
  585. $toast.fadeIn("fast");
  586. if (duration !== 0) {
  587. setTimeout(() => {
  588. $toast.fadeOut("fast", () => {
  589. $toast.remove();
  590. });
  591. }, duration);
  592. }
  593. return {
  594. clear() {
  595. $toast.remove();
  596. }
  597. };
  598. }
  599. var init_toast = __esm({
  600. "src/components/toast.ts"() {
  601. "use strict";
  602. }
  603. });
  604.  
  605. // src/contents/helpers.ts
  606. function isV2EX_RequestError(error) {
  607. if ("cause" in error) {
  608. const cause = error["cause"];
  609. if ("success" in cause && "message" in cause) {
  610. return (
  611. // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  612. typeof cause["success"] === "boolean" && // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  613. !cause["success"] && // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  614. typeof cause["message"] === "string"
  615. );
  616. }
  617. }
  618. return false;
  619. }
  620. function focusReplyInput() {
  621. if (replyTextArea instanceof HTMLTextAreaElement) {
  622. replyTextArea.focus();
  623. }
  624. }
  625. function insertTextToReplyInput(text) {
  626. if (replyTextArea instanceof HTMLTextAreaElement) {
  627. const startPos = replyTextArea.selectionStart;
  628. const endPos = replyTextArea.selectionEnd;
  629. const valueToStart = replyTextArea.value.substring(0, startPos);
  630. const valueFromEnd = replyTextArea.value.substring(endPos, replyTextArea.value.length);
  631. replyTextArea.value = `${valueToStart}${text}${valueFromEnd}`;
  632. focusReplyInput();
  633. replyTextArea.selectionStart = replyTextArea.selectionEnd = startPos + text.length;
  634. }
  635. }
  636. async function setMemberTags(memberName, tags) {
  637. const storage = await getStorage(false);
  638. const tagData = storage["member-tag" /* MemberTag */];
  639. const runEnv = getRunEnv();
  640. if (runEnv !== "chrome") {
  641. return;
  642. }
  643. if (tags && tags.length > 0) {
  644. const newTagData = { ...tagData, [memberName]: { tags } };
  645. await setStorage("member-tag" /* MemberTag */, newTagData);
  646. } else {
  647. if (tagData && Reflect.has(tagData, memberName)) {
  648. delete tagData[memberName];
  649. await setStorage("member-tag" /* MemberTag */, tagData);
  650. }
  651. }
  652. }
  653. async function addToReadingList(params) {
  654. const { url, title, content } = params;
  655. if (!(typeof url === "string" || typeof title === "string" || typeof content === "string")) {
  656. const message = "\u65E0\u6CD5\u8BC6\u522B\u5C06\u8BE5\u4E3B\u9898\u7684\u5143\u6570\u636E";
  657. createToast({ message });
  658. throw new Error(message);
  659. }
  660. const storage = await getStorage();
  661. const currentData = storage["reading-list" /* ReadingList */]?.data || [];
  662. const exist = currentData.findIndex((it) => it.url === url) !== -1;
  663. if (exist) {
  664. createToast({ message: "\u8BE5\u4E3B\u9898\u5DF2\u5B58\u5728\u4E8E\u7A0D\u540E\u9605\u8BFB" });
  665. } else {
  666. await setStorage("reading-list" /* ReadingList */, {
  667. data: [
  668. { url, title: title.replace(" - V2EX", ""), content, addedTime: Date.now() },
  669. ...currentData
  670. ]
  671. });
  672. createToast({ message: "\u{1F4D6} \u5DF2\u6DFB\u52A0\u8FDB\u7A0D\u540E\u9605\u8BFB" });
  673. }
  674. }
  675. var init_helpers = __esm({
  676. "src/contents/helpers.ts"() {
  677. "use strict";
  678. init_toast();
  679. init_constants();
  680. init_utils();
  681. init_globals();
  682. }
  683. });
  684.  
  685. // src/contents/home/topic-list.ts
  686. function handlingTopicList() {
  687. const runEnv = getRunEnv();
  688. if (!runEnv) {
  689. return;
  690. }
  691. const storage = getStorageSync();
  692. const options = storage["options" /* Options */];
  693. const PAT = storage["api" /* API */]?.pat;
  694. let abortController = null;
  695. const $detailBtn = createButton({
  696. children: "\u8FDB\u5165\u4E3B\u9898",
  697. className: "special",
  698. tag: "a"
  699. });
  700. if (options.openInNewTab) {
  701. $detailBtn.prop("target", "_blank");
  702. }
  703. const model = createModel({
  704. root: $(document.body),
  705. onMount: ({ $actions }) => {
  706. $actions.prepend($detailBtn);
  707. },
  708. onClose: ({ $title, $content }) => {
  709. $title.empty();
  710. $content.empty();
  711. abortController?.abort();
  712. }
  713. });
  714. const topicDataCache = /* @__PURE__ */ new Map();
  715. $topicList.each((_, topicItem) => {
  716. const $topicItem = $(topicItem);
  717. const $itemTitle = $topicItem.find(".item_title");
  718. $('<button class="v2p-topic-preview-btn">\u9884\u89C8</button>').on("click", () => {
  719. const linkHref = $topicItem.find(".topic-link").attr("href");
  720. const match = linkHref?.match(/\/t\/(\d+)/);
  721. const topicId = match?.at(1);
  722. if (topicId) {
  723. model.open();
  724. $detailBtn.prop("href", linkHref);
  725. const topicTitle = $itemTitle.find(".topic-link").text();
  726. const $titleLink = $(
  727. `<a class="v2p-topic-preview-title-link" title="${topicTitle}">${topicTitle}</a>`
  728. );
  729. model.$title.empty().append($titleLink);
  730. if (PAT) {
  731. void (async () => {
  732. let cacheData = topicDataCache.get(topicId);
  733. if (!cacheData || Date.now() - cacheData.cacheTime > 1e3 * 60 * 10) {
  734. try {
  735. abortController = new AbortController();
  736. model.$content.empty().append(`<div class="v2p-model-loading"><div class="v2p-icon-loading">${iconLoading}</div></div>`);
  737. const promises = [
  738. fetchTopic(topicId, { signal: abortController.signal }),
  739. fetchTopicReplies(topicId)
  740. ];
  741. const [{ result: topic }, { result: topicReplies }] = await Promise.all(promises);
  742. const data = {
  743. topic,
  744. topicReplies,
  745. cacheTime: Date.now()
  746. };
  747. topicDataCache.set(topicId, data);
  748. cacheData = data;
  749. } catch (err) {
  750. if (isV2EX_RequestError(err)) {
  751. const message = err.cause.message;
  752. if (
  753. /* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
  754. message === "Token expired" /* TokenExpired */ || message === "Invalid token" /* InvalidToken */
  755. ) {
  756. model.$content.empty().append(invalidTemplate("\u60A8\u7684 PAT \u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u8BBE\u7F6E\u3002"));
  757. }
  758. }
  759. }
  760. }
  761. if (cacheData) {
  762. const { topic, topicReplies } = cacheData;
  763. const $topicPreview = $('<div class="v2p-topic-preview">');
  764. $titleLink.prop("href", topic.url);
  765. if (options.openInNewTab) {
  766. $titleLink.prop("target", "_blank");
  767. }
  768. const $infoBar = $(`<div class="v2p-tp-info-bar"><div class="v2p-tp-info"><a class="v2p-tp-member" href="${topic.member.url}"><img class="v2p-tp-avatar" src="${topic.member.avatar}"><span>${topic.member.username}</span></a><span>${formatTimestamp(topic.created, { format: "YMDHMS" })}</span><span>${topic.replies} \u6761\u56DE\u590D</span></div></div>`);
  769. $(`<div class="v2p-tp-read"><span class="v2p-tp-read-icon">${iconBookMark}</span>\u7A0D\u540E\u9605\u8BFB</div>`).on("click", () => {
  770. void addToReadingList({
  771. url: topic.url,
  772. title: topic.title,
  773. content: topic.content
  774. });
  775. }).appendTo($infoBar);
  776. $topicPreview.append($infoBar);
  777. if (topic.content_rendered) {
  778. $topicPreview.append(
  779. `<div class="v2p-topic-preview-content markdown_body">${topic.content_rendered}</div>`
  780. );
  781. } else {
  782. $topicPreview.append(`<div class="v2p-empty-content"><div class="v2p-text-emoji">\xAF\\_(\u30C4)_/\xAF</div><p>\u8BE5\u4E3B\u9898\u6CA1\u6709\u6B63\u6587\u5185\u5BB9</p></div>`);
  783. }
  784. if (topicReplies.length > 0) {
  785. const $template = $("<div>");
  786. const op = topic.member.username;
  787. topicReplies.forEach((r) => {
  788. $template.append(`<div class="v2p-topic-reply"><div class="v2p-topic-reply-member"><a href="${r.member.url}"><img class="v2p-topic-reply-avatar" src="${r.member.avatar}"><span>${r.member.username}</span><span style=" display: ${op === r.member.username ? "unset" : "none"}; margin-left:${op === r.member.username ? "3px" : "0"}; "><span class="badge op mini">OP</span></span></a>\uFF1A </div><div class="v2p-topic-reply-content">${escapeHTML(r.content)}</div></div>`);
  789. });
  790. $('<div class="v2p-topic-reply-box">').append($template.html()).append(`<div class="v2p-more-reply-tip">\u5728\u4E3B\u9898\u5185\u67E5\u770B\u5B8C\u6574\u8BC4\u8BBA...</div>`).appendTo($topicPreview);
  791. }
  792. model.$content.empty().append($topicPreview);
  793. }
  794. })();
  795. } else {
  796. model.$content.empty().append(invalidTemplate("\u60A8\u9700\u8981\u5148\u8BBE\u7F6E PAT \u624D\u80FD\u83B7\u53D6\u9884\u89C8\u5185\u5BB9\u3002"));
  797. }
  798. }
  799. }).appendTo($itemTitle);
  800. });
  801. }
  802. var invalidTemplate;
  803. var init_topic_list = __esm({
  804. "src/contents/home/topic-list.ts"() {
  805. "use strict";
  806. init_button();
  807. init_model();
  808. init_constants();
  809. init_icons();
  810. init_services();
  811. init_utils();
  812. init_globals();
  813. init_helpers();
  814. invalidTemplate = (tip) => `<div class="v2p-no-pat"><div class="v2p-no-pat-title">${tip}</div><div class="v2p-no-pat-desc"> \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 </div><div class="v2p-no-pat-steps"><div class="v2p-no-pat-step"><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><img class="v2p-no-pat-img" src="https://i.imgur.com/UfNkuTF.png"></div><div class="v2p-no-pat-step"><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><img class="v2p-no-pat-img" src="https://i.imgur.com/O6hP86A.png"></div></div></div>`;
  815. }
  816. });
  817.  
  818. // src/contents/home/index.ts
  819. var home_exports = {};
  820. var init_home = __esm({
  821. "src/contents/home/index.ts"() {
  822. "use strict";
  823. init_constants();
  824. init_utils();
  825. init_topic_list();
  826. void (async () => {
  827. const storage = await getStorage();
  828. const options = storage["options" /* Options */];
  829. {
  830. $("#Main .tab").addClass("v2p-hover-btn");
  831. if (options.openInNewTab) {
  832. $('#Main .topic-link, .item_hot_topic_title > a, .item_node, a[href="/write"]').prop(
  833. "target",
  834. "_blank"
  835. );
  836. }
  837. }
  838. handlingTopicList();
  839. {
  840. const dailyInfo = storage["daily" /* Daily */];
  841. if (dailyInfo?.lastCheckInTime) {
  842. if (isSameDay(dailyInfo.lastCheckInTime, Date.now())) {
  843. const $info = $(`<a class="cell v2p-info-row" href="/mission/daily"> \u4ECA\u65E5\u5DF2\u81EA\u52A8\u7B7E\u5230 </a>`);
  844. $('#Rightbar > .box:has("#member-activity")').append($info);
  845. }
  846. }
  847. }
  848. })();
  849. }
  850. });
  851.  
  852. // node_modules/.pnpm/@floating-ui+core@1.2.6/node_modules/@floating-ui/core/dist/floating-ui.core.mjs
  853. function getAlignment(placement) {
  854. return placement.split("-")[1];
  855. }
  856. function getLengthFromAxis(axis) {
  857. return axis === "y" ? "height" : "width";
  858. }
  859. function getSide(placement) {
  860. return placement.split("-")[0];
  861. }
  862. function getMainAxisFromPlacement(placement) {
  863. return ["top", "bottom"].includes(getSide(placement)) ? "x" : "y";
  864. }
  865. function computeCoordsFromPlacement(_ref, placement, rtl) {
  866. let {
  867. reference,
  868. floating
  869. } = _ref;
  870. const commonX = reference.x + reference.width / 2 - floating.width / 2;
  871. const commonY = reference.y + reference.height / 2 - floating.height / 2;
  872. const mainAxis = getMainAxisFromPlacement(placement);
  873. const length = getLengthFromAxis(mainAxis);
  874. const commonAlign = reference[length] / 2 - floating[length] / 2;
  875. const side = getSide(placement);
  876. const isVertical = mainAxis === "x";
  877. let coords;
  878. switch (side) {
  879. case "top":
  880. coords = {
  881. x: commonX,
  882. y: reference.y - floating.height
  883. };
  884. break;
  885. case "bottom":
  886. coords = {
  887. x: commonX,
  888. y: reference.y + reference.height
  889. };
  890. break;
  891. case "right":
  892. coords = {
  893. x: reference.x + reference.width,
  894. y: commonY
  895. };
  896. break;
  897. case "left":
  898. coords = {
  899. x: reference.x - floating.width,
  900. y: commonY
  901. };
  902. break;
  903. default:
  904. coords = {
  905. x: reference.x,
  906. y: reference.y
  907. };
  908. }
  909. switch (getAlignment(placement)) {
  910. case "start":
  911. coords[mainAxis] -= commonAlign * (rtl && isVertical ? -1 : 1);
  912. break;
  913. case "end":
  914. coords[mainAxis] += commonAlign * (rtl && isVertical ? -1 : 1);
  915. break;
  916. }
  917. return coords;
  918. }
  919. function expandPaddingObject(padding) {
  920. return {
  921. top: 0,
  922. right: 0,
  923. bottom: 0,
  924. left: 0,
  925. ...padding
  926. };
  927. }
  928. function getSideObjectFromPadding(padding) {
  929. return typeof padding !== "number" ? expandPaddingObject(padding) : {
  930. top: padding,
  931. right: padding,
  932. bottom: padding,
  933. left: padding
  934. };
  935. }
  936. function rectToClientRect(rect) {
  937. return {
  938. ...rect,
  939. top: rect.y,
  940. left: rect.x,
  941. right: rect.x + rect.width,
  942. bottom: rect.y + rect.height
  943. };
  944. }
  945. async function detectOverflow(state, options) {
  946. var _await$platform$isEle;
  947. if (options === void 0) {
  948. options = {};
  949. }
  950. const {
  951. x,
  952. y,
  953. platform: platform2,
  954. rects,
  955. elements,
  956. strategy
  957. } = state;
  958. const {
  959. boundary = "clippingAncestors",
  960. rootBoundary = "viewport",
  961. elementContext = "floating",
  962. altBoundary = false,
  963. padding = 0
  964. } = options;
  965. const paddingObject = getSideObjectFromPadding(padding);
  966. const altContext = elementContext === "floating" ? "reference" : "floating";
  967. const element = elements[altBoundary ? altContext : elementContext];
  968. const clippingClientRect = rectToClientRect(await platform2.getClippingRect({
  969. 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)),
  970. boundary,
  971. rootBoundary,
  972. strategy
  973. }));
  974. const rect = elementContext === "floating" ? {
  975. ...rects.floating,
  976. x,
  977. y
  978. } : rects.reference;
  979. const offsetParent = await (platform2.getOffsetParent == null ? void 0 : platform2.getOffsetParent(elements.floating));
  980. const offsetScale = await (platform2.isElement == null ? void 0 : platform2.isElement(offsetParent)) ? await (platform2.getScale == null ? void 0 : platform2.getScale(offsetParent)) || {
  981. x: 1,
  982. y: 1
  983. } : {
  984. x: 1,
  985. y: 1
  986. };
  987. const elementClientRect = rectToClientRect(platform2.convertOffsetParentRelativeRectToViewportRelativeRect ? await platform2.convertOffsetParentRelativeRectToViewportRelativeRect({
  988. rect,
  989. offsetParent,
  990. strategy
  991. }) : rect);
  992. return {
  993. top: (clippingClientRect.top - elementClientRect.top + paddingObject.top) / offsetScale.y,
  994. bottom: (elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom) / offsetScale.y,
  995. left: (clippingClientRect.left - elementClientRect.left + paddingObject.left) / offsetScale.x,
  996. right: (elementClientRect.right - clippingClientRect.right + paddingObject.right) / offsetScale.x
  997. };
  998. }
  999. function within(min$1, value, max$1) {
  1000. return max(min$1, min(value, max$1));
  1001. }
  1002. function getOppositePlacement(placement) {
  1003. return placement.replace(/left|right|bottom|top/g, (side) => oppositeSideMap[side]);
  1004. }
  1005. function getAlignmentSides(placement, rects, rtl) {
  1006. if (rtl === void 0) {
  1007. rtl = false;
  1008. }
  1009. const alignment = getAlignment(placement);
  1010. const mainAxis = getMainAxisFromPlacement(placement);
  1011. const length = getLengthFromAxis(mainAxis);
  1012. let mainAlignmentSide = mainAxis === "x" ? alignment === (rtl ? "end" : "start") ? "right" : "left" : alignment === "start" ? "bottom" : "top";
  1013. if (rects.reference[length] > rects.floating[length]) {
  1014. mainAlignmentSide = getOppositePlacement(mainAlignmentSide);
  1015. }
  1016. return {
  1017. main: mainAlignmentSide,
  1018. cross: getOppositePlacement(mainAlignmentSide)
  1019. };
  1020. }
  1021. function getOppositeAlignmentPlacement(placement) {
  1022. return placement.replace(/start|end/g, (alignment) => oppositeAlignmentMap[alignment]);
  1023. }
  1024. function getExpandedPlacements(placement) {
  1025. const oppositePlacement = getOppositePlacement(placement);
  1026. return [getOppositeAlignmentPlacement(placement), oppositePlacement, getOppositeAlignmentPlacement(oppositePlacement)];
  1027. }
  1028. function getSideList(side, isStart, rtl) {
  1029. const lr = ["left", "right"];
  1030. const rl = ["right", "left"];
  1031. const tb = ["top", "bottom"];
  1032. const bt = ["bottom", "top"];
  1033. switch (side) {
  1034. case "top":
  1035. case "bottom":
  1036. if (rtl)
  1037. return isStart ? rl : lr;
  1038. return isStart ? lr : rl;
  1039. case "left":
  1040. case "right":
  1041. return isStart ? tb : bt;
  1042. default:
  1043. return [];
  1044. }
  1045. }
  1046. function getOppositeAxisPlacements(placement, flipAlignment, direction, rtl) {
  1047. const alignment = getAlignment(placement);
  1048. let list = getSideList(getSide(placement), direction === "start", rtl);
  1049. if (alignment) {
  1050. list = list.map((side) => side + "-" + alignment);
  1051. if (flipAlignment) {
  1052. list = list.concat(list.map(getOppositeAlignmentPlacement));
  1053. }
  1054. }
  1055. return list;
  1056. }
  1057. async function convertValueToCoords(state, value) {
  1058. const {
  1059. placement,
  1060. platform: platform2,
  1061. elements
  1062. } = state;
  1063. const rtl = await (platform2.isRTL == null ? void 0 : platform2.isRTL(elements.floating));
  1064. const side = getSide(placement);
  1065. const alignment = getAlignment(placement);
  1066. const isVertical = getMainAxisFromPlacement(placement) === "x";
  1067. const mainAxisMulti = ["left", "top"].includes(side) ? -1 : 1;
  1068. const crossAxisMulti = rtl && isVertical ? -1 : 1;
  1069. const rawValue = typeof value === "function" ? value(state) : value;
  1070. let {
  1071. mainAxis,
  1072. crossAxis,
  1073. alignmentAxis
  1074. } = typeof rawValue === "number" ? {
  1075. mainAxis: rawValue,
  1076. crossAxis: 0,
  1077. alignmentAxis: null
  1078. } : {
  1079. mainAxis: 0,
  1080. crossAxis: 0,
  1081. alignmentAxis: null,
  1082. ...rawValue
  1083. };
  1084. if (alignment && typeof alignmentAxis === "number") {
  1085. crossAxis = alignment === "end" ? alignmentAxis * -1 : alignmentAxis;
  1086. }
  1087. return isVertical ? {
  1088. x: crossAxis * crossAxisMulti,
  1089. y: mainAxis * mainAxisMulti
  1090. } : {
  1091. x: mainAxis * mainAxisMulti,
  1092. y: crossAxis * crossAxisMulti
  1093. };
  1094. }
  1095. function getCrossAxis(axis) {
  1096. return axis === "x" ? "y" : "x";
  1097. }
  1098. var computePosition, min, max, oppositeSideMap, oppositeAlignmentMap, flip, offset, shift;
  1099. var init_floating_ui_core = __esm({
  1100. "node_modules/.pnpm/@floating-ui+core@1.2.6/node_modules/@floating-ui/core/dist/floating-ui.core.mjs"() {
  1101. computePosition = async (reference, floating, config) => {
  1102. const {
  1103. placement = "bottom",
  1104. strategy = "absolute",
  1105. middleware = [],
  1106. platform: platform2
  1107. } = config;
  1108. const validMiddleware = middleware.filter(Boolean);
  1109. const rtl = await (platform2.isRTL == null ? void 0 : platform2.isRTL(floating));
  1110. let rects = await platform2.getElementRects({
  1111. reference,
  1112. floating,
  1113. strategy
  1114. });
  1115. let {
  1116. x,
  1117. y
  1118. } = computeCoordsFromPlacement(rects, placement, rtl);
  1119. let statefulPlacement = placement;
  1120. let middlewareData = {};
  1121. let resetCount = 0;
  1122. for (let i = 0; i < validMiddleware.length; i++) {
  1123. const {
  1124. name,
  1125. fn
  1126. } = validMiddleware[i];
  1127. const {
  1128. x: nextX,
  1129. y: nextY,
  1130. data,
  1131. reset
  1132. } = await fn({
  1133. x,
  1134. y,
  1135. initialPlacement: placement,
  1136. placement: statefulPlacement,
  1137. strategy,
  1138. middlewareData,
  1139. rects,
  1140. platform: platform2,
  1141. elements: {
  1142. reference,
  1143. floating
  1144. }
  1145. });
  1146. x = nextX != null ? nextX : x;
  1147. y = nextY != null ? nextY : y;
  1148. middlewareData = {
  1149. ...middlewareData,
  1150. [name]: {
  1151. ...middlewareData[name],
  1152. ...data
  1153. }
  1154. };
  1155. if (reset && resetCount <= 50) {
  1156. resetCount++;
  1157. if (typeof reset === "object") {
  1158. if (reset.placement) {
  1159. statefulPlacement = reset.placement;
  1160. }
  1161. if (reset.rects) {
  1162. rects = reset.rects === true ? await platform2.getElementRects({
  1163. reference,
  1164. floating,
  1165. strategy
  1166. }) : reset.rects;
  1167. }
  1168. ({
  1169. x,
  1170. y
  1171. } = computeCoordsFromPlacement(rects, statefulPlacement, rtl));
  1172. }
  1173. i = -1;
  1174. continue;
  1175. }
  1176. }
  1177. return {
  1178. x,
  1179. y,
  1180. placement: statefulPlacement,
  1181. strategy,
  1182. middlewareData
  1183. };
  1184. };
  1185. min = Math.min;
  1186. max = Math.max;
  1187. oppositeSideMap = {
  1188. left: "right",
  1189. right: "left",
  1190. bottom: "top",
  1191. top: "bottom"
  1192. };
  1193. oppositeAlignmentMap = {
  1194. start: "end",
  1195. end: "start"
  1196. };
  1197. flip = function(options) {
  1198. if (options === void 0) {
  1199. options = {};
  1200. }
  1201. return {
  1202. name: "flip",
  1203. options,
  1204. async fn(state) {
  1205. var _middlewareData$flip;
  1206. const {
  1207. placement,
  1208. middlewareData,
  1209. rects,
  1210. initialPlacement,
  1211. platform: platform2,
  1212. elements
  1213. } = state;
  1214. const {
  1215. mainAxis: checkMainAxis = true,
  1216. crossAxis: checkCrossAxis = true,
  1217. fallbackPlacements: specifiedFallbackPlacements,
  1218. fallbackStrategy = "bestFit",
  1219. fallbackAxisSideDirection = "none",
  1220. flipAlignment = true,
  1221. ...detectOverflowOptions
  1222. } = options;
  1223. const side = getSide(placement);
  1224. const isBasePlacement = getSide(initialPlacement) === initialPlacement;
  1225. const rtl = await (platform2.isRTL == null ? void 0 : platform2.isRTL(elements.floating));
  1226. const fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipAlignment ? [getOppositePlacement(initialPlacement)] : getExpandedPlacements(initialPlacement));
  1227. if (!specifiedFallbackPlacements && fallbackAxisSideDirection !== "none") {
  1228. fallbackPlacements.push(...getOppositeAxisPlacements(initialPlacement, flipAlignment, fallbackAxisSideDirection, rtl));
  1229. }
  1230. const placements = [initialPlacement, ...fallbackPlacements];
  1231. const overflow = await detectOverflow(state, detectOverflowOptions);
  1232. const overflows = [];
  1233. let overflowsData = ((_middlewareData$flip = middlewareData.flip) == null ? void 0 : _middlewareData$flip.overflows) || [];
  1234. if (checkMainAxis) {
  1235. overflows.push(overflow[side]);
  1236. }
  1237. if (checkCrossAxis) {
  1238. const {
  1239. main,
  1240. cross
  1241. } = getAlignmentSides(placement, rects, rtl);
  1242. overflows.push(overflow[main], overflow[cross]);
  1243. }
  1244. overflowsData = [...overflowsData, {
  1245. placement,
  1246. overflows
  1247. }];
  1248. if (!overflows.every((side2) => side2 <= 0)) {
  1249. var _middlewareData$flip2, _overflowsData$filter;
  1250. const nextIndex = (((_middlewareData$flip2 = middlewareData.flip) == null ? void 0 : _middlewareData$flip2.index) || 0) + 1;
  1251. const nextPlacement = placements[nextIndex];
  1252. if (nextPlacement) {
  1253. return {
  1254. data: {
  1255. index: nextIndex,
  1256. overflows: overflowsData
  1257. },
  1258. reset: {
  1259. placement: nextPlacement
  1260. }
  1261. };
  1262. }
  1263. 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;
  1264. if (!resetPlacement) {
  1265. switch (fallbackStrategy) {
  1266. case "bestFit": {
  1267. var _overflowsData$map$so;
  1268. const placement2 = (_overflowsData$map$so = overflowsData.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$map$so[0];
  1269. if (placement2) {
  1270. resetPlacement = placement2;
  1271. }
  1272. break;
  1273. }
  1274. case "initialPlacement":
  1275. resetPlacement = initialPlacement;
  1276. break;
  1277. }
  1278. }
  1279. if (placement !== resetPlacement) {
  1280. return {
  1281. reset: {
  1282. placement: resetPlacement
  1283. }
  1284. };
  1285. }
  1286. }
  1287. return {};
  1288. }
  1289. };
  1290. };
  1291. offset = function(value) {
  1292. if (value === void 0) {
  1293. value = 0;
  1294. }
  1295. return {
  1296. name: "offset",
  1297. options: value,
  1298. async fn(state) {
  1299. const {
  1300. x,
  1301. y
  1302. } = state;
  1303. const diffCoords = await convertValueToCoords(state, value);
  1304. return {
  1305. x: x + diffCoords.x,
  1306. y: y + diffCoords.y,
  1307. data: diffCoords
  1308. };
  1309. }
  1310. };
  1311. };
  1312. shift = function(options) {
  1313. if (options === void 0) {
  1314. options = {};
  1315. }
  1316. return {
  1317. name: "shift",
  1318. options,
  1319. async fn(state) {
  1320. const {
  1321. x,
  1322. y,
  1323. placement
  1324. } = state;
  1325. const {
  1326. mainAxis: checkMainAxis = true,
  1327. crossAxis: checkCrossAxis = false,
  1328. limiter = {
  1329. fn: (_ref) => {
  1330. let {
  1331. x: x2,
  1332. y: y2
  1333. } = _ref;
  1334. return {
  1335. x: x2,
  1336. y: y2
  1337. };
  1338. }
  1339. },
  1340. ...detectOverflowOptions
  1341. } = options;
  1342. const coords = {
  1343. x,
  1344. y
  1345. };
  1346. const overflow = await detectOverflow(state, detectOverflowOptions);
  1347. const mainAxis = getMainAxisFromPlacement(getSide(placement));
  1348. const crossAxis = getCrossAxis(mainAxis);
  1349. let mainAxisCoord = coords[mainAxis];
  1350. let crossAxisCoord = coords[crossAxis];
  1351. if (checkMainAxis) {
  1352. const minSide = mainAxis === "y" ? "top" : "left";
  1353. const maxSide = mainAxis === "y" ? "bottom" : "right";
  1354. const min3 = mainAxisCoord + overflow[minSide];
  1355. const max3 = mainAxisCoord - overflow[maxSide];
  1356. mainAxisCoord = within(min3, mainAxisCoord, max3);
  1357. }
  1358. if (checkCrossAxis) {
  1359. const minSide = crossAxis === "y" ? "top" : "left";
  1360. const maxSide = crossAxis === "y" ? "bottom" : "right";
  1361. const min3 = crossAxisCoord + overflow[minSide];
  1362. const max3 = crossAxisCoord - overflow[maxSide];
  1363. crossAxisCoord = within(min3, crossAxisCoord, max3);
  1364. }
  1365. const limitedCoords = limiter.fn({
  1366. ...state,
  1367. [mainAxis]: mainAxisCoord,
  1368. [crossAxis]: crossAxisCoord
  1369. });
  1370. return {
  1371. ...limitedCoords,
  1372. data: {
  1373. x: limitedCoords.x - x,
  1374. y: limitedCoords.y - y
  1375. }
  1376. };
  1377. }
  1378. };
  1379. };
  1380. }
  1381. });
  1382.  
  1383. // node_modules/.pnpm/@floating-ui+dom@1.2.1/node_modules/@floating-ui/dom/dist/floating-ui.dom.mjs
  1384. function getWindow(node) {
  1385. var _node$ownerDocument;
  1386. return ((_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.defaultView) || window;
  1387. }
  1388. function getComputedStyle$1(element) {
  1389. return getWindow(element).getComputedStyle(element);
  1390. }
  1391. function getCssDimensions(element) {
  1392. const css = getComputedStyle$1(element);
  1393. let width = parseFloat(css.width);
  1394. let height = parseFloat(css.height);
  1395. const offsetWidth = element.offsetWidth;
  1396. const offsetHeight = element.offsetHeight;
  1397. const shouldFallback = round(width) !== offsetWidth || round(height) !== offsetHeight;
  1398. if (shouldFallback) {
  1399. width = offsetWidth;
  1400. height = offsetHeight;
  1401. }
  1402. return {
  1403. width,
  1404. height,
  1405. fallback: shouldFallback
  1406. };
  1407. }
  1408. function getNodeName(node) {
  1409. return isNode(node) ? (node.nodeName || "").toLowerCase() : "";
  1410. }
  1411. function getUAString() {
  1412. if (uaString) {
  1413. return uaString;
  1414. }
  1415. const uaData = navigator.userAgentData;
  1416. if (uaData && Array.isArray(uaData.brands)) {
  1417. uaString = uaData.brands.map((item) => item.brand + "/" + item.version).join(" ");
  1418. return uaString;
  1419. }
  1420. return navigator.userAgent;
  1421. }
  1422. function isHTMLElement(value) {
  1423. return value instanceof getWindow(value).HTMLElement;
  1424. }
  1425. function isElement(value) {
  1426. return value instanceof getWindow(value).Element;
  1427. }
  1428. function isNode(value) {
  1429. return value instanceof getWindow(value).Node;
  1430. }
  1431. function isShadowRoot(node) {
  1432. if (typeof ShadowRoot === "undefined") {
  1433. return false;
  1434. }
  1435. const OwnElement = getWindow(node).ShadowRoot;
  1436. return node instanceof OwnElement || node instanceof ShadowRoot;
  1437. }
  1438. function isOverflowElement(element) {
  1439. const {
  1440. overflow,
  1441. overflowX,
  1442. overflowY,
  1443. display
  1444. } = getComputedStyle$1(element);
  1445. return /auto|scroll|overlay|hidden|clip/.test(overflow + overflowY + overflowX) && !["inline", "contents"].includes(display);
  1446. }
  1447. function isTableElement(element) {
  1448. return ["table", "td", "th"].includes(getNodeName(element));
  1449. }
  1450. function isContainingBlock(element) {
  1451. const isFirefox2 = /firefox/i.test(getUAString());
  1452. const css = getComputedStyle$1(element);
  1453. const backdropFilter = css.backdropFilter || css.WebkitBackdropFilter;
  1454. return css.transform !== "none" || css.perspective !== "none" || (backdropFilter ? backdropFilter !== "none" : false) || isFirefox2 && css.willChange === "filter" || isFirefox2 && (css.filter ? css.filter !== "none" : false) || ["transform", "perspective"].some((value) => css.willChange.includes(value)) || ["paint", "layout", "strict", "content"].some((value) => {
  1455. const contain = css.contain;
  1456. return contain != null ? contain.includes(value) : false;
  1457. });
  1458. }
  1459. function isClientRectVisualViewportBased() {
  1460. return /^((?!chrome|android).)*safari/i.test(getUAString());
  1461. }
  1462. function isLastTraversableNode(node) {
  1463. return ["html", "body", "#document"].includes(getNodeName(node));
  1464. }
  1465. function unwrapElement(element) {
  1466. return !isElement(element) ? element.contextElement : element;
  1467. }
  1468. function getScale(element) {
  1469. const domElement = unwrapElement(element);
  1470. if (!isHTMLElement(domElement)) {
  1471. return FALLBACK_SCALE;
  1472. }
  1473. const rect = domElement.getBoundingClientRect();
  1474. const {
  1475. width,
  1476. height,
  1477. fallback
  1478. } = getCssDimensions(domElement);
  1479. let x = (fallback ? round(rect.width) : rect.width) / width;
  1480. let y = (fallback ? round(rect.height) : rect.height) / height;
  1481. if (!x || !Number.isFinite(x)) {
  1482. x = 1;
  1483. }
  1484. if (!y || !Number.isFinite(y)) {
  1485. y = 1;
  1486. }
  1487. return {
  1488. x,
  1489. y
  1490. };
  1491. }
  1492. function getBoundingClientRect(element, includeScale, isFixedStrategy, offsetParent) {
  1493. var _win$visualViewport, _win$visualViewport2;
  1494. if (includeScale === void 0) {
  1495. includeScale = false;
  1496. }
  1497. if (isFixedStrategy === void 0) {
  1498. isFixedStrategy = false;
  1499. }
  1500. const clientRect = element.getBoundingClientRect();
  1501. const domElement = unwrapElement(element);
  1502. let scale = FALLBACK_SCALE;
  1503. if (includeScale) {
  1504. if (offsetParent) {
  1505. if (isElement(offsetParent)) {
  1506. scale = getScale(offsetParent);
  1507. }
  1508. } else {
  1509. scale = getScale(element);
  1510. }
  1511. }
  1512. const win = domElement ? getWindow(domElement) : window;
  1513. const addVisualOffsets = isClientRectVisualViewportBased() && isFixedStrategy;
  1514. let x = (clientRect.left + (addVisualOffsets ? ((_win$visualViewport = win.visualViewport) == null ? void 0 : _win$visualViewport.offsetLeft) || 0 : 0)) / scale.x;
  1515. let y = (clientRect.top + (addVisualOffsets ? ((_win$visualViewport2 = win.visualViewport) == null ? void 0 : _win$visualViewport2.offsetTop) || 0 : 0)) / scale.y;
  1516. let width = clientRect.width / scale.x;
  1517. let height = clientRect.height / scale.y;
  1518. if (domElement) {
  1519. const win2 = getWindow(domElement);
  1520. const offsetWin = offsetParent && isElement(offsetParent) ? getWindow(offsetParent) : offsetParent;
  1521. let currentIFrame = win2.frameElement;
  1522. while (currentIFrame && offsetParent && offsetWin !== win2) {
  1523. const iframeScale = getScale(currentIFrame);
  1524. const iframeRect = currentIFrame.getBoundingClientRect();
  1525. const css = getComputedStyle(currentIFrame);
  1526. iframeRect.x += (currentIFrame.clientLeft + parseFloat(css.paddingLeft)) * iframeScale.x;
  1527. iframeRect.y += (currentIFrame.clientTop + parseFloat(css.paddingTop)) * iframeScale.y;
  1528. x *= iframeScale.x;
  1529. y *= iframeScale.y;
  1530. width *= iframeScale.x;
  1531. height *= iframeScale.y;
  1532. x += iframeRect.x;
  1533. y += iframeRect.y;
  1534. currentIFrame = getWindow(currentIFrame).frameElement;
  1535. }
  1536. }
  1537. return {
  1538. width,
  1539. height,
  1540. top: y,
  1541. right: x + width,
  1542. bottom: y + height,
  1543. left: x,
  1544. x,
  1545. y
  1546. };
  1547. }
  1548. function getDocumentElement(node) {
  1549. return ((isNode(node) ? node.ownerDocument : node.document) || window.document).documentElement;
  1550. }
  1551. function getNodeScroll(element) {
  1552. if (isElement(element)) {
  1553. return {
  1554. scrollLeft: element.scrollLeft,
  1555. scrollTop: element.scrollTop
  1556. };
  1557. }
  1558. return {
  1559. scrollLeft: element.pageXOffset,
  1560. scrollTop: element.pageYOffset
  1561. };
  1562. }
  1563. function convertOffsetParentRelativeRectToViewportRelativeRect(_ref) {
  1564. let {
  1565. rect,
  1566. offsetParent,
  1567. strategy
  1568. } = _ref;
  1569. const isOffsetParentAnElement = isHTMLElement(offsetParent);
  1570. const documentElement = getDocumentElement(offsetParent);
  1571. if (offsetParent === documentElement) {
  1572. return rect;
  1573. }
  1574. let scroll = {
  1575. scrollLeft: 0,
  1576. scrollTop: 0
  1577. };
  1578. let scale = {
  1579. x: 1,
  1580. y: 1
  1581. };
  1582. const offsets = {
  1583. x: 0,
  1584. y: 0
  1585. };
  1586. if (isOffsetParentAnElement || !isOffsetParentAnElement && strategy !== "fixed") {
  1587. if (getNodeName(offsetParent) !== "body" || isOverflowElement(documentElement)) {
  1588. scroll = getNodeScroll(offsetParent);
  1589. }
  1590. if (isHTMLElement(offsetParent)) {
  1591. const offsetRect = getBoundingClientRect(offsetParent);
  1592. scale = getScale(offsetParent);
  1593. offsets.x = offsetRect.x + offsetParent.clientLeft;
  1594. offsets.y = offsetRect.y + offsetParent.clientTop;
  1595. }
  1596. }
  1597. return {
  1598. width: rect.width * scale.x,
  1599. height: rect.height * scale.y,
  1600. x: rect.x * scale.x - scroll.scrollLeft * scale.x + offsets.x,
  1601. y: rect.y * scale.y - scroll.scrollTop * scale.y + offsets.y
  1602. };
  1603. }
  1604. function getWindowScrollBarX(element) {
  1605. return getBoundingClientRect(getDocumentElement(element)).left + getNodeScroll(element).scrollLeft;
  1606. }
  1607. function getDocumentRect(element) {
  1608. const html = getDocumentElement(element);
  1609. const scroll = getNodeScroll(element);
  1610. const body = element.ownerDocument.body;
  1611. const width = max2(html.scrollWidth, html.clientWidth, body.scrollWidth, body.clientWidth);
  1612. const height = max2(html.scrollHeight, html.clientHeight, body.scrollHeight, body.clientHeight);
  1613. let x = -scroll.scrollLeft + getWindowScrollBarX(element);
  1614. const y = -scroll.scrollTop;
  1615. if (getComputedStyle$1(body).direction === "rtl") {
  1616. x += max2(html.clientWidth, body.clientWidth) - width;
  1617. }
  1618. return {
  1619. width,
  1620. height,
  1621. x,
  1622. y
  1623. };
  1624. }
  1625. function getParentNode(node) {
  1626. if (getNodeName(node) === "html") {
  1627. return node;
  1628. }
  1629. const result = (
  1630. // Step into the shadow DOM of the parent of a slotted node.
  1631. node.assignedSlot || // DOM Element detected.
  1632. node.parentNode || // ShadowRoot detected.
  1633. isShadowRoot(node) && node.host || // Fallback.
  1634. getDocumentElement(node)
  1635. );
  1636. return isShadowRoot(result) ? result.host : result;
  1637. }
  1638. function getNearestOverflowAncestor(node) {
  1639. const parentNode = getParentNode(node);
  1640. if (isLastTraversableNode(parentNode)) {
  1641. return parentNode.ownerDocument.body;
  1642. }
  1643. if (isHTMLElement(parentNode) && isOverflowElement(parentNode)) {
  1644. return parentNode;
  1645. }
  1646. return getNearestOverflowAncestor(parentNode);
  1647. }
  1648. function getOverflowAncestors(node, list) {
  1649. var _node$ownerDocument;
  1650. if (list === void 0) {
  1651. list = [];
  1652. }
  1653. const scrollableAncestor = getNearestOverflowAncestor(node);
  1654. const isBody = scrollableAncestor === ((_node$ownerDocument = node.ownerDocument) == null ? void 0 : _node$ownerDocument.body);
  1655. const win = getWindow(scrollableAncestor);
  1656. if (isBody) {
  1657. return list.concat(win, win.visualViewport || [], isOverflowElement(scrollableAncestor) ? scrollableAncestor : []);
  1658. }
  1659. return list.concat(scrollableAncestor, getOverflowAncestors(scrollableAncestor));
  1660. }
  1661. function getViewportRect(element, strategy) {
  1662. const win = getWindow(element);
  1663. const html = getDocumentElement(element);
  1664. const visualViewport = win.visualViewport;
  1665. let width = html.clientWidth;
  1666. let height = html.clientHeight;
  1667. let x = 0;
  1668. let y = 0;
  1669. if (visualViewport) {
  1670. width = visualViewport.width;
  1671. height = visualViewport.height;
  1672. const visualViewportBased = isClientRectVisualViewportBased();
  1673. if (!visualViewportBased || visualViewportBased && strategy === "fixed") {
  1674. x = visualViewport.offsetLeft;
  1675. y = visualViewport.offsetTop;
  1676. }
  1677. }
  1678. return {
  1679. width,
  1680. height,
  1681. x,
  1682. y
  1683. };
  1684. }
  1685. function getInnerBoundingClientRect(element, strategy) {
  1686. const clientRect = getBoundingClientRect(element, true, strategy === "fixed");
  1687. const top = clientRect.top + element.clientTop;
  1688. const left = clientRect.left + element.clientLeft;
  1689. const scale = isHTMLElement(element) ? getScale(element) : {
  1690. x: 1,
  1691. y: 1
  1692. };
  1693. const width = element.clientWidth * scale.x;
  1694. const height = element.clientHeight * scale.y;
  1695. const x = left * scale.x;
  1696. const y = top * scale.y;
  1697. return {
  1698. width,
  1699. height,
  1700. x,
  1701. y
  1702. };
  1703. }
  1704. function getClientRectFromClippingAncestor(element, clippingAncestor, strategy) {
  1705. let rect;
  1706. if (clippingAncestor === "viewport") {
  1707. rect = getViewportRect(element, strategy);
  1708. } else if (clippingAncestor === "document") {
  1709. rect = getDocumentRect(getDocumentElement(element));
  1710. } else if (isElement(clippingAncestor)) {
  1711. rect = getInnerBoundingClientRect(clippingAncestor, strategy);
  1712. } else {
  1713. const mutableRect = {
  1714. ...clippingAncestor
  1715. };
  1716. if (isClientRectVisualViewportBased()) {
  1717. var _win$visualViewport, _win$visualViewport2;
  1718. const win = getWindow(element);
  1719. mutableRect.x -= ((_win$visualViewport = win.visualViewport) == null ? void 0 : _win$visualViewport.offsetLeft) || 0;
  1720. mutableRect.y -= ((_win$visualViewport2 = win.visualViewport) == null ? void 0 : _win$visualViewport2.offsetTop) || 0;
  1721. }
  1722. rect = mutableRect;
  1723. }
  1724. return rectToClientRect(rect);
  1725. }
  1726. function getClippingElementAncestors(element, cache) {
  1727. const cachedResult = cache.get(element);
  1728. if (cachedResult) {
  1729. return cachedResult;
  1730. }
  1731. let result = getOverflowAncestors(element).filter((el) => isElement(el) && getNodeName(el) !== "body");
  1732. let currentContainingBlockComputedStyle = null;
  1733. const elementIsFixed = getComputedStyle$1(element).position === "fixed";
  1734. let currentNode = elementIsFixed ? getParentNode(element) : element;
  1735. while (isElement(currentNode) && !isLastTraversableNode(currentNode)) {
  1736. const computedStyle = getComputedStyle$1(currentNode);
  1737. const containingBlock = isContainingBlock(currentNode);
  1738. const shouldIgnoreCurrentNode = computedStyle.position === "fixed";
  1739. if (shouldIgnoreCurrentNode) {
  1740. currentContainingBlockComputedStyle = null;
  1741. } else {
  1742. const shouldDropCurrentNode = elementIsFixed ? !containingBlock && !currentContainingBlockComputedStyle : !containingBlock && computedStyle.position === "static" && !!currentContainingBlockComputedStyle && ["absolute", "fixed"].includes(currentContainingBlockComputedStyle.position);
  1743. if (shouldDropCurrentNode) {
  1744. result = result.filter((ancestor) => ancestor !== currentNode);
  1745. } else {
  1746. currentContainingBlockComputedStyle = computedStyle;
  1747. }
  1748. }
  1749. currentNode = getParentNode(currentNode);
  1750. }
  1751. cache.set(element, result);
  1752. return result;
  1753. }
  1754. function getClippingRect(_ref) {
  1755. let {
  1756. element,
  1757. boundary,
  1758. rootBoundary,
  1759. strategy
  1760. } = _ref;
  1761. const elementClippingAncestors = boundary === "clippingAncestors" ? getClippingElementAncestors(element, this._c) : [].concat(boundary);
  1762. const clippingAncestors = [...elementClippingAncestors, rootBoundary];
  1763. const firstClippingAncestor = clippingAncestors[0];
  1764. const clippingRect = clippingAncestors.reduce((accRect, clippingAncestor) => {
  1765. const rect = getClientRectFromClippingAncestor(element, clippingAncestor, strategy);
  1766. accRect.top = max2(rect.top, accRect.top);
  1767. accRect.right = min2(rect.right, accRect.right);
  1768. accRect.bottom = min2(rect.bottom, accRect.bottom);
  1769. accRect.left = max2(rect.left, accRect.left);
  1770. return accRect;
  1771. }, getClientRectFromClippingAncestor(element, firstClippingAncestor, strategy));
  1772. return {
  1773. width: clippingRect.right - clippingRect.left,
  1774. height: clippingRect.bottom - clippingRect.top,
  1775. x: clippingRect.left,
  1776. y: clippingRect.top
  1777. };
  1778. }
  1779. function getDimensions(element) {
  1780. if (isHTMLElement(element)) {
  1781. return getCssDimensions(element);
  1782. }
  1783. return element.getBoundingClientRect();
  1784. }
  1785. function getTrueOffsetParent(element, polyfill) {
  1786. if (!isHTMLElement(element) || getComputedStyle$1(element).position === "fixed") {
  1787. return null;
  1788. }
  1789. if (polyfill) {
  1790. return polyfill(element);
  1791. }
  1792. return element.offsetParent;
  1793. }
  1794. function getContainingBlock(element) {
  1795. let currentNode = getParentNode(element);
  1796. while (isHTMLElement(currentNode) && !isLastTraversableNode(currentNode)) {
  1797. if (isContainingBlock(currentNode)) {
  1798. return currentNode;
  1799. } else {
  1800. currentNode = getParentNode(currentNode);
  1801. }
  1802. }
  1803. return null;
  1804. }
  1805. function getOffsetParent(element, polyfill) {
  1806. const window2 = getWindow(element);
  1807. let offsetParent = getTrueOffsetParent(element, polyfill);
  1808. while (offsetParent && isTableElement(offsetParent) && getComputedStyle$1(offsetParent).position === "static") {
  1809. offsetParent = getTrueOffsetParent(offsetParent, polyfill);
  1810. }
  1811. if (offsetParent && (getNodeName(offsetParent) === "html" || getNodeName(offsetParent) === "body" && getComputedStyle$1(offsetParent).position === "static" && !isContainingBlock(offsetParent))) {
  1812. return window2;
  1813. }
  1814. return offsetParent || getContainingBlock(element) || window2;
  1815. }
  1816. function getRectRelativeToOffsetParent(element, offsetParent, strategy) {
  1817. const isOffsetParentAnElement = isHTMLElement(offsetParent);
  1818. const documentElement = getDocumentElement(offsetParent);
  1819. const rect = getBoundingClientRect(element, true, strategy === "fixed", offsetParent);
  1820. let scroll = {
  1821. scrollLeft: 0,
  1822. scrollTop: 0
  1823. };
  1824. const offsets = {
  1825. x: 0,
  1826. y: 0
  1827. };
  1828. if (isOffsetParentAnElement || !isOffsetParentAnElement && strategy !== "fixed") {
  1829. if (getNodeName(offsetParent) !== "body" || isOverflowElement(documentElement)) {
  1830. scroll = getNodeScroll(offsetParent);
  1831. }
  1832. if (isHTMLElement(offsetParent)) {
  1833. const offsetRect = getBoundingClientRect(offsetParent, true);
  1834. offsets.x = offsetRect.x + offsetParent.clientLeft;
  1835. offsets.y = offsetRect.y + offsetParent.clientTop;
  1836. } else if (documentElement) {
  1837. offsets.x = getWindowScrollBarX(documentElement);
  1838. }
  1839. }
  1840. return {
  1841. x: rect.left + scroll.scrollLeft - offsets.x,
  1842. y: rect.top + scroll.scrollTop - offsets.y,
  1843. width: rect.width,
  1844. height: rect.height
  1845. };
  1846. }
  1847. var min2, max2, round, uaString, FALLBACK_SCALE, platform, computePosition2;
  1848. var init_floating_ui_dom = __esm({
  1849. "node_modules/.pnpm/@floating-ui+dom@1.2.1/node_modules/@floating-ui/dom/dist/floating-ui.dom.mjs"() {
  1850. init_floating_ui_core();
  1851. init_floating_ui_core();
  1852. min2 = Math.min;
  1853. max2 = Math.max;
  1854. round = Math.round;
  1855. FALLBACK_SCALE = {
  1856. x: 1,
  1857. y: 1
  1858. };
  1859. platform = {
  1860. getClippingRect,
  1861. convertOffsetParentRelativeRectToViewportRelativeRect,
  1862. isElement,
  1863. getDimensions,
  1864. getOffsetParent,
  1865. getDocumentElement,
  1866. getScale,
  1867. async getElementRects(_ref) {
  1868. let {
  1869. reference,
  1870. floating,
  1871. strategy
  1872. } = _ref;
  1873. const getOffsetParentFn = this.getOffsetParent || getOffsetParent;
  1874. const getDimensionsFn = this.getDimensions;
  1875. return {
  1876. reference: getRectRelativeToOffsetParent(reference, await getOffsetParentFn(floating), strategy),
  1877. floating: {
  1878. x: 0,
  1879. y: 0,
  1880. ...await getDimensionsFn(floating)
  1881. }
  1882. };
  1883. },
  1884. getClientRects: (element) => Array.from(element.getClientRects()),
  1885. isRTL: (element) => getComputedStyle$1(element).direction === "rtl"
  1886. };
  1887. computePosition2 = (reference, floating, options) => {
  1888. const cache = /* @__PURE__ */ new Map();
  1889. const mergedOptions = {
  1890. platform,
  1891. ...options
  1892. };
  1893. const platformWithCache = {
  1894. ...mergedOptions.platform,
  1895. _c: cache
  1896. };
  1897. return computePosition(reference, floating, {
  1898. ...mergedOptions,
  1899. platform: platformWithCache
  1900. });
  1901. };
  1902. }
  1903. });
  1904.  
  1905. // src/components/popup.ts
  1906. function createPopup(props) {
  1907. const {
  1908. root,
  1909. trigger,
  1910. triggerType = "click",
  1911. content,
  1912. options,
  1913. onOpen,
  1914. onClose,
  1915. placement = "bottom-start",
  1916. offsetOptions = { mainAxis: 5, crossAxis: 5 }
  1917. } = props;
  1918. const $popupContent = $('<div class="v2p-popup-content">');
  1919. const $popup = $('<div class="v2p-popup" tabindex="0">').css("visibility", "hidden").append($popupContent);
  1920. root.append($popup);
  1921. if (content) {
  1922. $popup.append(content);
  1923. }
  1924. const popup = $popup.get(0);
  1925. const handleClickOutside = (ev) => {
  1926. if ($(ev.target).closest(popup).length === 0) {
  1927. handlePopupClose();
  1928. }
  1929. };
  1930. const handlePopupClose = () => {
  1931. $popup.css("visibility", "hidden");
  1932. $(document).off("click", handleClickOutside);
  1933. onClose?.();
  1934. popupControl.onClose?.();
  1935. };
  1936. const handlePopupOpen = ($reference) => {
  1937. if (!$reference) {
  1938. return;
  1939. }
  1940. setTimeout(() => {
  1941. $(document).on("click", handleClickOutside);
  1942. });
  1943. const referenceElement = $reference.get(0);
  1944. computePosition2(referenceElement, popup, {
  1945. placement,
  1946. middleware: [offset(offsetOptions), flip(), shift({ padding: 8 })],
  1947. ...options
  1948. }).then(({ x, y }) => {
  1949. Object.assign(popup.style, {
  1950. left: `${x}px`,
  1951. top: `${y}px`
  1952. });
  1953. $popup.css("visibility", "visible");
  1954. }).catch(() => {
  1955. handlePopupClose();
  1956. createToast({ message: "\u274C Popup \u6E32\u67D3\u5931\u8D25" });
  1957. });
  1958. onOpen?.();
  1959. };
  1960. const popupControl = {
  1961. $content: $popupContent,
  1962. isOver: false,
  1963. open: (reference) => {
  1964. handlePopupOpen(reference);
  1965. },
  1966. close: handlePopupClose
  1967. };
  1968. if (triggerType === "hover") {
  1969. $popup.on("mouseover", () => {
  1970. if (!popupControl.isOver) {
  1971. popupControl.isOver = true;
  1972. $popup.off("mouseleave").on("mouseleave", () => {
  1973. popupControl.isOver = false;
  1974. setTimeout(() => {
  1975. if (!popupControl.isOver) {
  1976. popupControl.close();
  1977. }
  1978. }, hoverDelay);
  1979. });
  1980. }
  1981. });
  1982. }
  1983. trigger?.on("click", () => {
  1984. if (popup.style.visibility !== "hidden") {
  1985. handlePopupClose();
  1986. } else {
  1987. handlePopupOpen(trigger);
  1988. }
  1989. });
  1990. return popupControl;
  1991. }
  1992. var hoverDelay;
  1993. var init_popup = __esm({
  1994. "src/components/popup.ts"() {
  1995. "use strict";
  1996. init_floating_ui_dom();
  1997. init_toast();
  1998. hoverDelay = 350;
  1999. }
  2000. });
  2001.  
  2002. // src/contents/topic/avatar.ts
  2003. function processAvatar(params) {
  2004. const { $cellDom, popupControl, commentData, onSetTagsClick: onSetTags } = params;
  2005. const { memberName, memberAvatar, memberLink } = commentData;
  2006. let abortController = null;
  2007. const $avatar = $cellDom.find(".avatar");
  2008. const handleOver = () => {
  2009. popupControl.close();
  2010. popupControl.open($avatar);
  2011. const $content = $(`<div class="v2p-member-card"><div class="v2p-info"><div class="v2p-info-left"><a class="v2p-avatar-box" href="${memberLink}"><img class="v2p-avatar" src="${memberAvatar}"></a></div><div class="v2p-info-right"><div class="v2p-username"><a href="${memberLink}">${memberName}</a></div><div class="v2p-no v2p-loading"></div><div class="v2p-created-date v2p-loading"></div></div></div><div class="v2p-bio" style="disply:none;"></div><div class="v2p-member-card-actions"></div></div>`);
  2012. popupControl.$content.empty().append($content);
  2013. createButton({ children: "\u6DFB\u52A0\u7528\u6237\u6807\u7B7E" }).on("click", () => {
  2014. popupControl.close();
  2015. onSetTags?.();
  2016. }).appendTo($(".v2p-member-card-actions"));
  2017. void (async () => {
  2018. if (!memberDataCache.has(memberName)) {
  2019. abortController = new AbortController();
  2020. popupControl.onClose = () => {
  2021. abortController?.abort();
  2022. };
  2023. try {
  2024. const memberData = await fetchUserInfo(memberName, {
  2025. signal: abortController.signal
  2026. });
  2027. memberDataCache.set(memberName, memberData);
  2028. } catch (err) {
  2029. if (err && typeof err === "object" && "name" in err && err.name !== "AbortError") {
  2030. $content.html(`<span>\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25</span>`);
  2031. }
  2032. return null;
  2033. }
  2034. }
  2035. const data = memberDataCache.get(memberName);
  2036. if (data) {
  2037. $content.find(".v2p-no").removeClass("v2p-loading").text(`V2EX \u7B2C ${data.id} \u53F7\u4F1A\u5458`);
  2038. $content.find(".v2p-created-date").removeClass("v2p-loading").text(`\u52A0\u5165\u4E8E ${formatTimestamp(data.created)}`);
  2039. if (data.bio && data.bio.trim().length > 0) {
  2040. $content.find(".v2p-bio").css("disply", "block").text(data.bio);
  2041. }
  2042. }
  2043. })();
  2044. };
  2045. let isOver = false;
  2046. $avatar.on("mouseover", () => {
  2047. isOver = true;
  2048. setTimeout(() => {
  2049. if (isOver) {
  2050. handleOver();
  2051. }
  2052. }, hoverDelay);
  2053. }).on("mouseleave", () => {
  2054. isOver = false;
  2055. setTimeout(() => {
  2056. if (!popupControl.isOver && !isOver) {
  2057. popupControl.close();
  2058. }
  2059. }, hoverDelay);
  2060. }).wrap(`<a href="/member/${commentData.memberName}" style="cursor: pointer;">`);
  2061. }
  2062. var memberDataCache;
  2063. var init_avatar = __esm({
  2064. "src/contents/topic/avatar.ts"() {
  2065. "use strict";
  2066. init_button();
  2067. init_popup();
  2068. init_services();
  2069. init_utils();
  2070. memberDataCache = /* @__PURE__ */ new Map();
  2071. }
  2072. });
  2073.  
  2074. // src/contents/topic/content.ts
  2075. function handlingContent() {
  2076. const storage = getStorageSync();
  2077. const options = storage["options" /* Options */];
  2078. if (options.openInNewTab) {
  2079. $topicContentBox.find(".topic_content a[href]").prop("target", "_blank").prop("rel", "noopener noreferrer");
  2080. }
  2081. {
  2082. const $topicContents = $topicContentBox.find(".subtle > .topic_content");
  2083. const textLength = $topicContents.text().length;
  2084. if (textLength >= 200) {
  2085. $topicContents.each((_, topicContent) => {
  2086. if (textLength >= 400) {
  2087. topicContent.style.fontSize = "14px";
  2088. }
  2089. topicContent.style.fontSize = "14.5px";
  2090. });
  2091. }
  2092. }
  2093. {
  2094. const topicBtn = $(".topic_buttons .tb").addClass("v2p-tb v2p-hover-btn");
  2095. topicBtn.eq(0).append(`<span class="v2p-tb-icon">${iconStar}</span>`);
  2096. topicBtn.eq(1).append(`<span class="v2p-tb-icon">${iconTwitter}</span>`);
  2097. topicBtn.eq(2).append(`<span class="v2p-tb-icon">${iconIgnore}</span>`);
  2098. topicBtn.eq(3).append(`<span class="v2p-tb-icon">${iconLove}</span>`);
  2099. }
  2100. }
  2101. function processReplyContent($cellDom, replyContentOptions) {
  2102. if (!replyContentOptions.autoFold || $cellDom.find(".v2p-reply-content").length > 0) {
  2103. return;
  2104. }
  2105. const $replyContent = $cellDom.find(".reply_content");
  2106. const contentHeight = $replyContent.height() ?? 0;
  2107. const shouldCollapsed = contentHeight + READABLE_CONTENT_HEIGHT >= MAX_CONTENT_HEIGHT;
  2108. if (shouldCollapsed) {
  2109. const collapsedCSS = {
  2110. maxHeight: `${READABLE_CONTENT_HEIGHT}px`,
  2111. overflow: "hidden",
  2112. paddingBottom: "0"
  2113. };
  2114. const $contentBox = $('<div class="v2p-reply-content v2p-collapsed">').css(collapsedCSS);
  2115. const $expandBtn = createButton({ children: "\u5C55\u5F00\u56DE\u590D", className: "v2p-expand-btn" });
  2116. const toggleContent = () => {
  2117. const collapsed = $contentBox.hasClass("v2p-collapsed");
  2118. $contentBox.toggleClass("v2p-collapsed").css(
  2119. collapsed ? { maxHeight: "none", overflow: "auto", paddingBottom: "40px" } : collapsedCSS
  2120. );
  2121. $expandBtn.html(collapsed ? "\u6536\u8D77\u56DE\u590D" : "\u5C55\u5F00\u56DE\u590D");
  2122. };
  2123. $expandBtn.on("click", () => {
  2124. toggleContent();
  2125. });
  2126. $contentBox.append($replyContent.clone()).replaceAll($replyContent).append($expandBtn);
  2127. }
  2128. }
  2129. function updateMemberTag(memberName, tags) {
  2130. const $v2pTags = $(`.v2p-tags-${memberName}`);
  2131. const tagsValue = tags?.map((it) => it.name).join("\uFF0C");
  2132. if ($v2pTags.length > 0) {
  2133. if (tagsValue) {
  2134. $v2pTags.html(`<b>#</b>&nbsp;${tagsValue}`);
  2135. } else {
  2136. $v2pTags.remove();
  2137. }
  2138. } else {
  2139. if (tagsValue) {
  2140. $(`<div class="v2p-reply-tags v2p-tags-${memberName}"><b>#</b>&nbsp;${tagsValue}</div>`).on("click", () => {
  2141. openTagsSetter(memberName);
  2142. }).insertBefore(
  2143. $commentCells.filter(`:has(> table strong > a[href="/member/${memberName}"])`).find("> table .reply_content")
  2144. );
  2145. }
  2146. }
  2147. }
  2148. function openTagsSetter(memberName) {
  2149. void (async () => {
  2150. const storage = await getStorage(false);
  2151. const latestTagsData = storage["member-tag" /* MemberTag */];
  2152. const tagsValue = latestTagsData ? Reflect.has(latestTagsData, memberName) ? latestTagsData[memberName].tags?.map((it) => it.name).join("\uFF0C") : void 0 : void 0;
  2153. const newTagsValue = window.prompt(
  2154. `\u5BF9 @${memberName} \u8BBE\u7F6E\u6807\u7B7E\uFF0C\u591A\u4E2A\u6807\u7B7E\u4EE5\u9017\u53F7\uFF08\uFF0C\uFF09\u5206\u9694\u3002`,
  2155. tagsValue
  2156. );
  2157. if (newTagsValue !== null) {
  2158. const tags = newTagsValue.trim().length > 0 ? newTagsValue.split(/,|,/g).map((it) => ({ name: it })) : void 0;
  2159. await setMemberTags(memberName, tags);
  2160. updateMemberTag(memberName, tags);
  2161. }
  2162. })();
  2163. }
  2164. var init_content = __esm({
  2165. "src/contents/topic/content.ts"() {
  2166. "use strict";
  2167. init_button();
  2168. init_constants();
  2169. init_icons();
  2170. init_utils();
  2171. init_globals();
  2172. init_helpers();
  2173. }
  2174. });
  2175.  
  2176. // src/contents/topic/comment.ts
  2177. function handlingPopularComments() {
  2178. const popularCommentData = commentDataList.filter(({ likes }) => likes > 0).sort((a, b) => b.likes - a.likes);
  2179. const popularCount = popularCommentData.length;
  2180. const $popularBtn = $(
  2181. `<span class="v2p-tool v2p-hover-btn"><span class="v2p-tool-icon">${iconHeart}</span>\u70ED\u95E8\u56DE\u590D</span>`
  2182. );
  2183. $(".v2p-tools").prepend($popularBtn);
  2184. if (popularCount <= 0) {
  2185. $popularBtn.addClass("v2p-hover-btn-disabled").contents().last().replaceWith("\u6682\u65E0\u70ED\u95E8");
  2186. return;
  2187. }
  2188. const model = createModel({
  2189. root: $commentBox,
  2190. title: `\u672C\u9875\u5171\u6709 ${popularCommentData.length} \u6761\u70ED\u95E8\u56DE\u590D`,
  2191. onMount: ({ $content }) => {
  2192. const $template = $("<div>");
  2193. popularCommentData.forEach(({ index, refMemberNames }) => {
  2194. const $clonedCells = $commentCells.eq(index).clone();
  2195. $clonedCells.find(".v2p-controls > a:has(.v2p-control-reply)").remove();
  2196. $clonedCells.find(".no").css("pointer-events", "none");
  2197. const firstRefMember = refMemberNames?.at(0);
  2198. if (firstRefMember) {
  2199. const replyMember = commentDataList.findLast(
  2200. (it, idx) => idx < index && it.memberName === firstRefMember
  2201. );
  2202. if (replyMember) {
  2203. const $refCell = $(`<div class="v2p-topic-reply-ref"><div class="v2p-topic-reply"><div class="v2p-topic-reply-member"><a href="${replyMember.memberAvatar}"><img class="v2p-topic-reply-avatar" src="${replyMember.memberAvatar}"><span>${replyMember.memberName}</span></a>\uFF1A </div><div class="v2p-topic-reply-content">${escapeHTML(replyMember.content)}</div></div></div>`);
  2204. $clonedCells.prepend($refCell);
  2205. }
  2206. }
  2207. $template.append($clonedCells);
  2208. });
  2209. $content.css({ padding: "0 20px" }).append($template.html());
  2210. },
  2211. onOpen: ({ $container }) => {
  2212. $container.find('.cell[id^="r_"]').each((_, cellDom) => {
  2213. const storage = getStorageSync();
  2214. const options = storage["options" /* Options */];
  2215. processReplyContent($(cellDom), options.replyContent);
  2216. });
  2217. }
  2218. });
  2219. {
  2220. const commentBoxCount = $commentBox.find(".cell:first-of-type > span.gray");
  2221. const countText = commentBoxCount.text();
  2222. const newCountText = countText.substring(0, countText.indexOf("\u56DE\u590D") + 2);
  2223. const countTextSpan = `<span class="count-text">${newCountText}</span><span class="v2p-dot">\xB7</span>${popularCount} \u6761\u70ED\u95E8\u56DE\u590D`;
  2224. $popularBtn.on("click", () => {
  2225. model.open();
  2226. });
  2227. commentBoxCount.empty().append(countTextSpan);
  2228. }
  2229. }
  2230. function handlingControls() {
  2231. const crtlAreas = $commentTableRows.find("> td:last-of-type > .fr");
  2232. crtlAreas.each((_, el) => {
  2233. const ctrlArea = $(el);
  2234. const $controls = $('<span class="v2p-controls">');
  2235. const thankIcon = $(`<span class="v2p-control v2p-control-thank">${iconHeart}</span>`);
  2236. const thankArea = ctrlArea.find("> .thank_area");
  2237. const thanked = thankArea.hasClass("thanked");
  2238. if (thanked) {
  2239. thankIcon.addClass("v2p-thanked");
  2240. $controls.append($("<a>").append(thankIcon));
  2241. } else {
  2242. const thankEle = thankArea.find("> .thank");
  2243. const $hide = thankEle.eq(0).removeClass("thank");
  2244. const $thank = thankEle.eq(1).removeClass("thank");
  2245. $hide.html(`<span class="v2p-control v2p-hover-btn v2p-control-hide">${iconHide}</span>`);
  2246. thankIcon.addClass("v2p-hover-btn");
  2247. $thank.empty().append(thankIcon);
  2248. $thank.on("click", () => {
  2249. const $cell = ctrlArea.closest('.cell[id^="r_"]');
  2250. const $likesBox = $cell.find("> table .v2p-likes-box");
  2251. const likes = Number($likesBox.text());
  2252. const $clonedIconHeart = $likesBox.find(".v2p-icon-heart").clone();
  2253. if (likes > 0) {
  2254. $likesBox.addClass("v2p-thanked").empty().append($clonedIconHeart, ` ${likes + 1}`);
  2255. } else {
  2256. $(`<span class="small v2p-likes-box v2p-thanked" style="position:relative;top:-1px;"> &nbsp;&nbsp;<span class="v2p-icon-heart">${iconHeart}</span>1 </span>`).insertAfter($cell.find("> table .ago"));
  2257. }
  2258. thankIcon.addClass("v2p-thanked");
  2259. $hide.hide();
  2260. $thank.off("click");
  2261. createToast({ message: "\u2764\uFE0F \u5DF2\u611F\u8C22\u56DE\u590D" });
  2262. });
  2263. $controls.append($hide).append($thank);
  2264. }
  2265. const $reply = ctrlArea.find("a:last-of-type");
  2266. $reply.find('> img[alt="Reply"]').replaceWith(`<span class="v2p-control v2p-hover-btn v2p-control-reply">${iconReply}</span>`);
  2267. $controls.append($reply);
  2268. thankArea.remove();
  2269. const floorNum = ctrlArea.find(".no").clone();
  2270. $reply.on("click", () => {
  2271. const replyVal = $replyTextArea.val();
  2272. if (typeof replyVal === "string" && replyVal.length > 0) {
  2273. const floor = floorNum.text();
  2274. const replyComment = commentDataList.find((it) => it.floor === floor);
  2275. if (replyComment) {
  2276. const replyMemberName = replyComment.memberName;
  2277. const moreThanOneReply = commentDataList.findIndex(
  2278. (it) => it.memberName === replyMemberName && it.floor !== floor
  2279. ) !== -1;
  2280. if (moreThanOneReply) {
  2281. insertTextToReplyInput(`#${floor} `);
  2282. }
  2283. }
  2284. }
  2285. });
  2286. ctrlArea.empty().append($controls, floorNum);
  2287. });
  2288. }
  2289. async function handlingComments() {
  2290. const storage = getStorageSync();
  2291. const tagData = storage["member-tag" /* MemberTag */];
  2292. const options = storage["options" /* Options */];
  2293. if (options.reply.preload !== "off") {
  2294. const $paging = $(".v2p-paging");
  2295. if ($paging.length > 0) {
  2296. const $pagingTop = $paging.eq(0);
  2297. const $pagingBottom = $paging.eq(1);
  2298. const toastControl = createToast({ message: "\u6B63\u5728\u9884\u52A0\u8F7D\u56DE\u590D\uFF0C\u8BF7\u7A0D\u5019...", duration: 0 });
  2299. try {
  2300. const $pagingBottomNormal = $pagingBottom.find(".page_normal");
  2301. const $pageCurrent = $pagingTop.find(".page_current");
  2302. const currentPage = $pageCurrent.text();
  2303. if (currentPage === "1") {
  2304. const $pageNormal = $pagingTop.find(".page_normal");
  2305. const pages = [];
  2306. $pageNormal.each((i, ele) => {
  2307. if (i <= 1) {
  2308. if (ele.textContent) {
  2309. ele.classList.add("page_current");
  2310. ele.classList.remove("page_normal");
  2311. $pagingBottomNormal.eq(i).addClass("page_current").removeClass("page_normal");
  2312. pages.push(ele.textContent);
  2313. }
  2314. }
  2315. });
  2316. if (pages.length > 0) {
  2317. const pagesText = await Promise.all(
  2318. pages.map((p) => fetchTopicPage(window.location.pathname, p))
  2319. );
  2320. pagesText.map((pageText) => {
  2321. $pagingBottom.before($(pageText).find('.cell[id^="r_"]'));
  2322. });
  2323. }
  2324. updateCommentCells();
  2325. }
  2326. toastControl.clear();
  2327. } catch {
  2328. createToast({ message: "\u274C \u52A0\u8F7D\u591A\u9875\u56DE\u590D\u5931\u8D25" });
  2329. }
  2330. }
  2331. }
  2332. commentDataList = $commentTableRows.map((idx, tr) => {
  2333. const id = $commentCells[idx].id;
  2334. const $tr = $(tr);
  2335. const $td = $tr.find("> td:nth-child(3)");
  2336. const thanked = $tr.find("> td:last-of-type > .fr").find("> .thank_area").hasClass("thanked");
  2337. const $member = $td.find("> strong > a");
  2338. const memberName = $member.text();
  2339. const memberLink = $member.prop("href");
  2340. const memberAvatar = $tr.find(".avatar").prop("src");
  2341. const content = $td.find("> .reply_content").text();
  2342. const likes = Number($td.find("span.small").text());
  2343. const floor = $td.find("span.no").text();
  2344. const memberNameMatches = Array.from(content.matchAll(/@([a-zA-Z0-9]+)/g));
  2345. const refMemberNames = memberNameMatches.length > 0 ? memberNameMatches.map(([, name]) => {
  2346. return name;
  2347. }) : void 0;
  2348. const floorNumberMatches = Array.from(content.matchAll(/#(\d+)/g));
  2349. const refFloors = floorNumberMatches.length > 0 ? floorNumberMatches.map(([, floor2]) => {
  2350. return floor2;
  2351. }) : void 0;
  2352. return {
  2353. id,
  2354. memberName,
  2355. memberLink,
  2356. memberAvatar,
  2357. content,
  2358. likes,
  2359. floor,
  2360. index: idx,
  2361. refMemberNames,
  2362. refFloors,
  2363. thanked
  2364. };
  2365. }).get();
  2366. {
  2367. const popupControl = createPopup({
  2368. root: $commentBox,
  2369. triggerType: "hover",
  2370. placement: "right-start",
  2371. offsetOptions: { mainAxis: 8, crossAxis: -4 }
  2372. });
  2373. const membersHasSetTags = /* @__PURE__ */ new Set();
  2374. $commentCells.each((i, cellDom) => {
  2375. const currentComment = commentDataList.at(i);
  2376. if (currentComment?.id !== cellDom.id) {
  2377. return;
  2378. }
  2379. const $cellDom = $(cellDom);
  2380. const { memberName, thanked } = currentComment;
  2381. processAvatar({
  2382. $cellDom,
  2383. popupControl,
  2384. commentData: currentComment,
  2385. onSetTagsClick: () => {
  2386. openTagsSetter(memberName);
  2387. }
  2388. });
  2389. if (memberName === loginName) {
  2390. $cellDom.find(".badges").append(`<div class="badge ${memberName === topicOwnerName ? "mod" : "you"}">YOU</div>`);
  2391. }
  2392. const $likesBox = $cellDom.find(".small.fade").addClass("v2p-likes-box");
  2393. $likesBox.find('img[alt="\u2764\uFE0F"]').replaceWith(`<span class="v2p-icon-heart">${iconHeart}</span>`);
  2394. if (thanked) {
  2395. $likesBox.addClass("v2p-thanked");
  2396. }
  2397. if (tagData && Reflect.has(tagData, memberName) && !membersHasSetTags.has(memberName)) {
  2398. updateMemberTag(memberName, tagData[memberName].tags);
  2399. membersHasSetTags.add(memberName);
  2400. }
  2401. });
  2402. handlingControls();
  2403. handlingPopularComments();
  2404. }
  2405. {
  2406. const display = options.nestedReply.display;
  2407. $commentCells.each((i, cellDom) => {
  2408. const $cellDom = $(cellDom);
  2409. const dataFromIndex = commentDataList.at(i);
  2410. processReplyContent($cellDom, options.replyContent);
  2411. const currentComment = dataFromIndex?.id === cellDom.id ? dataFromIndex : commentDataList.find((data) => data.id === cellDom.id);
  2412. if (currentComment) {
  2413. const { refMemberNames, refFloors } = currentComment;
  2414. if (!refMemberNames || refMemberNames.length === 0) {
  2415. return;
  2416. }
  2417. for (const refName of refMemberNames) {
  2418. for (let j = i - 1; j >= 0; j--) {
  2419. const { memberName: compareName, floor: eachFloor } = commentDataList.at(j) || {};
  2420. if (compareName === refName) {
  2421. const firstRefFloor = refFloors?.at(0);
  2422. if (firstRefFloor && firstRefFloor !== eachFloor) {
  2423. const targetIdx = commentDataList.slice(0, j).findIndex((data) => data.floor === firstRefFloor && data.memberName === refName);
  2424. if (targetIdx >= 0) {
  2425. $commentCells.eq(targetIdx).append(cellDom);
  2426. return;
  2427. }
  2428. }
  2429. if (display === "indent") {
  2430. cellDom.classList.add("v2p-indent");
  2431. }
  2432. $commentCells.eq(j).append(cellDom);
  2433. return;
  2434. }
  2435. }
  2436. }
  2437. }
  2438. });
  2439. }
  2440. }
  2441. var commentDataList;
  2442. var init_comment = __esm({
  2443. "src/contents/topic/comment.ts"() {
  2444. "use strict";
  2445. init_model();
  2446. init_popup();
  2447. init_toast();
  2448. init_constants();
  2449. init_icons();
  2450. init_services();
  2451. init_utils();
  2452. init_globals();
  2453. init_helpers();
  2454. init_avatar();
  2455. init_content();
  2456. commentDataList = [];
  2457. }
  2458. });
  2459.  
  2460. // src/contents/topic/paging.ts
  2461. function handlingPaging() {
  2462. const $notCommentCells = $commentBox.find('> .cell:not([id^="r_"])');
  2463. if ($notCommentCells.length <= 1) {
  2464. return;
  2465. }
  2466. const pagingCells = $notCommentCells.slice(1).addClass("v2p-paging");
  2467. const pageBtns = pagingCells.find(".super.button");
  2468. pageBtns.eq(0).addClass("v2p-prev-btn");
  2469. pageBtns.eq(1).addClass("v2p-next-btn");
  2470. }
  2471. var init_paging = __esm({
  2472. "src/contents/topic/paging.ts"() {
  2473. "use strict";
  2474. init_globals();
  2475. }
  2476. });
  2477.  
  2478. // src/contents/topic/reply.ts
  2479. function handlingReplyActions() {
  2480. const os = getOS();
  2481. const replyBtnText = `\u56DE\u590D<kbd>${os === "macos" ? "Cmd" : "Ctrl"}+Enter</kbd>`;
  2482. const $replyBtn = createButton({
  2483. children: replyBtnText,
  2484. type: "submit"
  2485. }).replaceAll($replyBox.find('input[type="submit"]'));
  2486. $replyForm.on("submit", () => {
  2487. $replyBtn.text("\u63D0\u4EA4\u56DE\u590D\u4E2D...").prop("disabled", true);
  2488. setTimeout(() => {
  2489. $replyBtn.html(replyBtnText).prop("disabled", false);
  2490. }, 5e3);
  2491. });
  2492. document.addEventListener("keydown", (ev) => {
  2493. if (ev.key === "Enter" && (ev.ctrlKey || ev.metaKey)) {
  2494. ev.preventDefault();
  2495. $replyForm.trigger("submit");
  2496. }
  2497. });
  2498. {
  2499. const emoticonGroup = $('<div class="v2p-emoji-group">');
  2500. const emoticonList = $('<div class="v2p-emoji-list">');
  2501. const emoticonSpan = $('<span class="v2p-emoji">');
  2502. const groups = emoticons.map((emojiGroup) => {
  2503. const group = emoticonGroup.clone();
  2504. group.append(`<div class="v2p-emoji-title">${emojiGroup.title}</div>`);
  2505. const list = emoticonList.clone().append(
  2506. emojiGroup.list.map((emoji) => {
  2507. const emoticon = emoticonSpan.clone().text(emoji).on("click", () => {
  2508. insertTextToReplyInput(emoji);
  2509. });
  2510. return emoticon;
  2511. })
  2512. );
  2513. group.append(list);
  2514. return group;
  2515. });
  2516. const emoticonsBox = $('<div class="v2p-emoticons-box">').append(groups);
  2517. const $emojiBtn = createButton({ children: iconEmoji }).insertAfter($replyBtn);
  2518. const $emojiContent = $('<div class="v2p-emoji-container">').append(emoticonsBox).appendTo($replyBox).on("click", () => {
  2519. focusReplyInput();
  2520. });
  2521. const keyupHandler = (ev) => {
  2522. if (ev.key === "Escape") {
  2523. ev.preventDefault();
  2524. emojiPopup.close();
  2525. }
  2526. };
  2527. $emojiBtn.on("click", () => {
  2528. focusReplyInput();
  2529. });
  2530. const emojiPopup = createPopup({
  2531. root: $replyBox,
  2532. trigger: $emojiBtn,
  2533. content: $emojiContent,
  2534. options: { placement: "right-end" },
  2535. onOpen: () => {
  2536. $(document.body).on("keydown", keyupHandler);
  2537. },
  2538. onClose: () => {
  2539. $(document.body).off("keydown", keyupHandler);
  2540. }
  2541. });
  2542. }
  2543. {
  2544. $replyBox.find("#undock-button, #undock-button + a").addClass("v2p-hover-btn").css("padding", "5px 4px");
  2545. }
  2546. }
  2547. function handleReply() {
  2548. const $tools = $(`<div class="v2p-reply-tools-box v2p-hover-btn"><span class="v2p-reply-tools-icon">${iconTool}</span> \u5DE5\u5177\u7BB1 </div>`);
  2549. const $toolContent = $(`<div class="v2p-reply-tool-content"><div class="v2p-reply-tool v2p-reply-tool-encode">\u6587\u5B57\u8F6C Base64</div><div class="v2p-reply-tool v2p-reply-tool-img">\u4E0A\u4F20\u56FE\u7247</div></div>`);
  2550. const toolsPopup = createPopup({
  2551. root: $replyBox,
  2552. trigger: $tools,
  2553. content: $toolContent,
  2554. offsetOptions: { mainAxis: 5, crossAxis: -5 }
  2555. });
  2556. $toolContent.find(".v2p-reply-tool-encode").on("click", () => {
  2557. focusReplyInput();
  2558. toolsPopup.close();
  2559. setTimeout(() => {
  2560. 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");
  2561. if (inputText) {
  2562. try {
  2563. const encodedText = window.btoa(window.encodeURIComponent(inputText));
  2564. insertTextToReplyInput(encodedText);
  2565. } catch (err) {
  2566. createToast({ message: "\u8BE5\u6587\u672C\u65E0\u6CD5\u7F16\u7801\u4E3A Base64" });
  2567. }
  2568. }
  2569. });
  2570. });
  2571. const uploadTip = "\u9009\u62E9\u3001\u7C98\u8D34\u3001\u62D6\u653E\u4E0A\u4F20\u56FE\u7247\u3002";
  2572. const $uploadBar = $(`<div class="v2p-reply-upload-bar">${uploadTip}</div>`);
  2573. const handleUploadImage = (file) => {
  2574. const placeholder = "[\u4E0A\u4F20\u56FE\u7247\u4E2D...]";
  2575. insertTextToReplyInput(` ${placeholder} `);
  2576. $uploadBar.addClass("v2p-reply-upload-bar-disabled").text("\u6B63\u5728\u4E0A\u4F20\u56FE\u7247...");
  2577. const replacePlaceholder = (imgLink) => {
  2578. const val = $replyTextArea.val();
  2579. if (typeof val === "string") {
  2580. const newVal = val.replace(placeholder, imgLink);
  2581. $replyTextArea.val(newVal).trigger("focus");
  2582. }
  2583. };
  2584. uploadImage(file).then((imgLink) => {
  2585. replacePlaceholder(imgLink);
  2586. }).catch(() => {
  2587. replacePlaceholder("");
  2588. window.alert("\u274C \u4E0A\u4F20\u56FE\u7247\u5931\u8D25\uFF0C\u8BF7\u6253\u5F00\u63A7\u5236\u53F0\u67E5\u770B\u539F\u56E0");
  2589. }).finally(() => {
  2590. $uploadBar.removeClass("v2p-reply-upload-bar-disabled").text(uploadTip);
  2591. });
  2592. };
  2593. const handleClickUploadImage = () => {
  2594. focusReplyInput();
  2595. toolsPopup.close();
  2596. const imgInput = document.createElement("input");
  2597. imgInput.style.display = "none";
  2598. imgInput.type = "file";
  2599. imgInput.accept = "image/*";
  2600. imgInput.addEventListener("change", () => {
  2601. const selectedFile = imgInput.files?.[0];
  2602. if (selectedFile) {
  2603. handleUploadImage(selectedFile);
  2604. }
  2605. });
  2606. imgInput.click();
  2607. };
  2608. $toolContent.find(".v2p-reply-tool-img").on("click", () => {
  2609. handleClickUploadImage();
  2610. });
  2611. $replyBox.find("> .flex-row-end").prepend($tools);
  2612. document.addEventListener("paste", (ev) => {
  2613. if (!(ev instanceof ClipboardEvent) || !replyTextArea?.matches(":focus")) {
  2614. return;
  2615. }
  2616. const items = ev.clipboardData?.items;
  2617. if (!items) {
  2618. return;
  2619. }
  2620. const imageItem = Array.from(items).find((item) => item.type.includes("image"));
  2621. if (imageItem) {
  2622. const file = imageItem.getAsFile();
  2623. if (file) {
  2624. handleUploadImage(file);
  2625. }
  2626. }
  2627. });
  2628. replyTextArea?.addEventListener("drop", (ev) => {
  2629. ev.preventDefault();
  2630. if (!(ev instanceof DragEvent)) {
  2631. return;
  2632. }
  2633. const file = ev.dataTransfer?.files[0];
  2634. if (file) {
  2635. handleUploadImage(file);
  2636. }
  2637. });
  2638. $replyTextArea.wrap('<div class="v2p-reply-wrap">').attr("placeholder", "\u7559\u4E0B\u5BF9\u4ED6\u4EBA\u6709\u5E2E\u52A9\u7684\u56DE\u590D");
  2639. $(".flex-one-row:last-of-type > .gray").text("");
  2640. $uploadBar.on("click", () => {
  2641. if (!$uploadBar.hasClass("v2p-reply-upload-bar-disabled")) {
  2642. handleClickUploadImage();
  2643. }
  2644. });
  2645. $(".v2p-reply-wrap").append($uploadBar);
  2646. handlingReplyActions();
  2647. }
  2648. var init_reply = __esm({
  2649. "src/contents/topic/reply.ts"() {
  2650. "use strict";
  2651. init_button();
  2652. init_popup();
  2653. init_toast();
  2654. init_constants();
  2655. init_icons();
  2656. init_services();
  2657. init_utils();
  2658. init_globals();
  2659. init_helpers();
  2660. }
  2661. });
  2662.  
  2663. // src/contents/topic/index.ts
  2664. var topic_exports = {};
  2665. var init_topic = __esm({
  2666. "src/contents/topic/index.ts"() {
  2667. "use strict";
  2668. init_constants();
  2669. init_icons();
  2670. init_utils();
  2671. init_globals();
  2672. init_comment();
  2673. init_content();
  2674. init_paging();
  2675. init_reply();
  2676. void (async () => {
  2677. const storage = await getStorage();
  2678. const options = storage["options" /* Options */];
  2679. if (options.openInNewTab) {
  2680. $commentTableRows.find("> td:nth-child(3) > strong > a").prop("target", "_blank");
  2681. }
  2682. {
  2683. const $tools = $(`<div class="cell v2p-tools"><span class="v2p-tool v2p-hover-btn v2p-tool-reply"><span class="v2p-tool-icon">${iconReply}</span>\u56DE\u590D\u4E3B\u9898 </span><span class="v2p-tool v2p-hover-btn v2p-tool-scroll-top"><span class="v2p-tool-icon">${iconScrollTop}</span>\u56DE\u5230\u9876\u90E8 </span></div>`);
  2684. $tools.find(".v2p-tool-reply").on("click", () => {
  2685. $replyTextArea.trigger("focus");
  2686. });
  2687. $tools.find(".v2p-tool-scroll-top").on("click", () => {
  2688. window.scrollTo({ top: 0, behavior: "smooth" });
  2689. });
  2690. $('#Rightbar > .box:has("#member-activity")').addClass("v2p-tool-box").append($tools);
  2691. }
  2692. {
  2693. $(document).on("keydown", (ev) => {
  2694. if (!ev.isDefaultPrevented()) {
  2695. if (ev.key === "Escape") {
  2696. const $replyContent = $("#reply_content");
  2697. if ($replyBox.hasClass("reply-box-sticky")) {
  2698. $replyBox.removeClass("reply-box-sticky");
  2699. $("#undock-button").css("display", "none");
  2700. }
  2701. $replyContent.trigger("blur");
  2702. }
  2703. }
  2704. });
  2705. }
  2706. handlingContent();
  2707. if (document.referrer !== "") {
  2708. if (document.referrer.includes(document.location.pathname)) {
  2709. const url = new URL(document.location.href);
  2710. const page = url.searchParams.get("p");
  2711. if (page && page !== "1") {
  2712. document.querySelector(".topic_buttons")?.scrollIntoView({ behavior: "smooth" });
  2713. }
  2714. }
  2715. }
  2716. handlingPaging();
  2717. void handlingComments();
  2718. handleReply();
  2719. })();
  2720. }
  2721. });
  2722.  
  2723. // node_modules/.pnpm/webext-patterns@1.3.0/node_modules/webext-patterns/index.js
  2724. var patternValidationRegex = /^(https?|wss?|file|ftp|\*):\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^file:\/\/\/.*$|^resource:\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^about:/;
  2725. var isFirefox = typeof navigator === "object" && navigator.userAgent.includes("Firefox/");
  2726. var allStarsRegex = isFirefox ? /^(https?|wss?):[/][/][^/]+([/].*)?$/ : /^https?:[/][/][^/]+([/].*)?$/;
  2727. var allUrlsRegex = /^(https?|file|ftp):[/]+/;
  2728. function getRawPatternRegex(matchPattern) {
  2729. if (!patternValidationRegex.test(matchPattern)) {
  2730. throw new Error(matchPattern + " is an invalid pattern, it must match " + String(patternValidationRegex));
  2731. }
  2732. let [, protocol, host, pathname] = matchPattern.split(/(^[^:]+:[/][/])([^/]+)?/);
  2733. protocol = protocol.replace("*", isFirefox ? "(https?|wss?)" : "https?").replace(/[/]/g, "[/]");
  2734. host = (host !== null && host !== void 0 ? host : "").replace(/^[*][.]/, "([^/]+.)*").replace(/^[*]$/, "[^/]+").replace(/[.]/g, "[.]").replace(/[*]$/g, "[^.]+");
  2735. pathname = pathname.replace(/[/]/g, "[/]").replace(/[.]/g, "[.]").replace(/[*]/g, ".*");
  2736. return "^" + protocol + host + "(" + pathname + ")?$";
  2737. }
  2738. function patternToRegex(...matchPatterns) {
  2739. if (matchPatterns.length === 0) {
  2740. return /$./;
  2741. }
  2742. if (matchPatterns.includes("<all_urls>")) {
  2743. return allUrlsRegex;
  2744. }
  2745. if (matchPatterns.includes("*://*/*")) {
  2746. return allStarsRegex;
  2747. }
  2748. return new RegExp(matchPatterns.map((x) => getRawPatternRegex(x)).join("|"));
  2749. }
  2750.  
  2751. // src/user-scripts/style.ts
  2752. var style = `:root{--zidx-serach: 100;--zidx-tabs: 10;--zidx-tools-card: 10;--zidx-reply-box: 99;--zidx-model-header: 50;--zidx-model-mask: 888;--zidx-toast: 999;--zidx-tip: 10;--zidx-popup: 99;--zidx-expand-mask: 10;--zidx-expand-btn: 20}: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-font-secondary: var(--v2p-color-main-400);--v2p-color-bg-content: #fff;--v2p-color-bg-footer: var(--v2p-color-bg-content);--v2p-color-bg-hover-btn: var(--v2p-color-main-200);--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-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-border-darker: var(--v2p-color-main-300);--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: 10px;--button-background-color: var(--v2p-color-main-100);--button-background-hover-color: var(--v2p-color-main-200);--button-hover-color: var(--button-background-hover-color);--button-foreground-color: var(--v2p-color-main-500);--button-foreground-hover-color: var(--v2p-color-main-600);--button-border-color: var(--v2p-color-main-300);--button-border-hover-color: var(--v2p-color-main-400);color:var(--v2p-color-foreground);font-family:system-ui,sans-serif;background-color:var(--v2p-color-background)}:root body #Logo{background-image:url("https://www.v2ex.com/static/img/v2ex@2x.png")}:root body ::selection{color:var(--v2p-color-main-100);background-color:var(--v2p-color-main-700)}:root body img::selection{background-color:var(--v2p-color-main-500)}:root body.v2p-theme-dark,:root[data-darkreader-scheme=dark] body{--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-bg-content: #22272e;--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-200);--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-heart: #ef4444;--v2p-color-heart-fill: #fca5a5;--v2p-color-mask: rgb(99 110 123 / 40%);--v2p-color-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-color: var(--v2p-color-bg-content);--box-background-alt-color: var(--v2p-color-main-100);--box-background-hover-color: var(--v2p-color-main-300);--button-background-color: #373e47;--button-background-hover-color: #444c56;--button-hover-color: var(--button-background-hover-color);--button-foreground-color: var(--v2p-color-foreground);--button-foreground-hover-color: var(--v2p-color-foreground);--button-border-color: var(--v2p-color-border);--button-border-hover-color: #768390}:root body.v2p-theme-dark #Logo,:root[data-darkreader-scheme=dark] body #Logo{background-image:url("https://www.v2ex.com/static/img/v2ex-alt@2x.png")}:root body.v2p-theme-dark ::selection,:root[data-darkreader-scheme=dark] body ::selection{color:var(--v2p-color-background);background-color:var(--v2p-color-foreground)}:root body.v2p-theme-dark img::selection,:root[data-darkreader-scheme=dark] body img::selection{background-color:var(--v2p-color-foreground)}@supports selector(:has(*)){:root body:has(#Wrapper.Night){--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-bg-content: #22272e;--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-200);--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-heart: #ef4444;--v2p-color-heart-fill: #fca5a5;--v2p-color-mask: rgb(99 110 123 / 40%);--v2p-color-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-color: var(--v2p-color-bg-content);--box-background-alt-color: var(--v2p-color-main-100);--box-background-hover-color: var(--v2p-color-main-300);--button-background-color: #373e47;--button-background-hover-color: #444c56;--button-hover-color: var(--button-background-hover-color);--button-foreground-color: var(--v2p-color-foreground);--button-foreground-hover-color: var(--v2p-color-foreground);--button-border-color: var(--v2p-color-border);--button-border-hover-color: #768390}:root body:has(#Wrapper.Night) #Logo{background-image:url("https://www.v2ex.com/static/img/v2ex-alt@2x.png")}:root body:has(#Wrapper.Night) ::selection{color:var(--v2p-color-background);background-color:var(--v2p-color-foreground)}:root body:has(#Wrapper.Night) img::selection{background-color:var(--v2p-color-foreground)}} :root{color-scheme:light}:root:has(#Wrapper.Night){color-scheme:dark}:root html,:root body{min-height:100vh}body{overflow:overlay;scrollbar-gutter:stable}body h1{font-weight:bold}body a{text-decoration:none;cursor:default}body a[href]{cursor:pointer}body a:hover{text-decoration:underline 1px;text-underline-offset:.5ex}body #Top{height:55px;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 #Wrapper{background-color:inherit;background-image:none}body #Wrapper.Night{background-color:inherit;background-image:none}body #Wrapper .content{display:flex;gap:25px}body #Leftbar{order:1;float:none}body #Main{flex:1;order:2;max-width:85vw;margin:0}body #Rightbar{order:3;float:none}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;background-size:14px 14px;opacity:.6;filter:none}body #search-container.active{background-color:var(--v2p-color-bg-search-active)}body #search-container #search-result{top:42px;z-index:var(--zidx-serach);color:var(--v2p-color-main-600);font-size:14px;background:var(--v2p-color-bg-widget);border:1px solid var(--box-border-color);box-shadow:var(--v2p-widget-shadow);backdrop-filter:blur(16px)}body #search-container #search-result .fade{color:var(--v2p-color-main-600)}body #search-container #search-result .search-item{color:var(--v2p-color-foreground);font-weight:bold;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{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-weight:bold;font-size:22px}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{position:relative;display:inline-flex;gap:5px;align-items:center;height:28px;padding:0 12px;color:var(--button-foreground-color);font-weight:500;font-size:14px;font-family:inherit;line-height:28px;white-space:nowrap;text-shadow:none;background:var(--button-background-color);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);cursor:pointer;transition:color .25s,background-color .25s,box-shadow .25s;user-select:none}body .button.normal:is(:hover:enabled,:active:enabled),body .button.super:is(:hover:enabled,:active:enabled){color:var(--button-foreground-hover-color);font-weight:500;text-shadow:none;background:var(--button-hover-color);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(--button-foreground-color) !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){color:var(--button-foreground-color);font-weight:500;text-shadow:none;background:var(--button-background-color);box-shadow:0 1.8px 0 var(--box-background-hover-color),0 1.8px 0 var(--button-background-color);cursor:default;opacity:.8;pointer-events:none}body .button.normal kbd,body .button.super kbd{position:relative;right:-4px;padding:0 3px;font-size:90%;font-family:inherit;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{padding:2px 5px;font-weight:bold;border:1px solid var(--v2p-color-accent-400);user-select:none}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-weight:normal;font-size:12px;line-height:1}body a.node:is(:active,:link,:visited){padding:5px 6px;color:var(--v2p-color-font-secondary);font-size:13px;background-color:var(--v2p-color-main-100);border-radius:4px}body a.node:is(:active,:link,:visited):hover{color:var(--v2p-color-main-500);background-color:var(--v2p-color-main-200)}body .outdated{font-size:12px;border-color:var(--v2p-color-main-200);border-bottom:none}body :is(.page_normal,.page_current):is(:link,:visited){padding:6px 9px;font-size:14px;border:none;border-radius:4px;user-select:none}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){font-weight:bold;background-color:var(--box-background-hover-color);box-shadow:none;pointer-events:none}body .page_input{display:none}body .dock_area{background:var(--v2p-color-main-200)}body .member-activity-bar{background-color:var(--v2p-color-main-200)}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}body #topic_supplement{height:unset;min-height:550px !important;max-height:800px !important;overflow:hidden;color:currentColor;font-size:15px;background-color:var(--v2p-color-bg-input);border:1px solid var(--button-border-color);border-radius:8px;transition:opacity .25s;resize:none;overflow-y:auto}body #topic_supplement::placeholder{color:var(--v2p-color-main-500);font-size:15px}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{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-height:1.4;text-shadow:none}body form textarea#topic_title{height:unset;min-height:75px !important;max-height:800px !important;overflow:hidden;color:currentColor;font-size:15px;background-color:var(--v2p-color-bg-input);border:1px solid var(--button-border-color);border-radius:8px;transition:opacity .25s;resize:none}body form textarea#topic_title::placeholder{color:var(--v2p-color-main-500);font-size:15px}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{height:unset;min-height:30px !important;max-height:800px !important;overflow:hidden;color:currentColor;font-size:15px;background-color:var(--v2p-color-bg-input);border:1px solid var(--button-border-color);border-radius:8px;transition:opacity .25s;resize:none}body form #topic_title::placeholder{color:var(--v2p-color-main-500);font-size:15px}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{height:unset;min-height:120px !important;max-height:800px !important;overflow:hidden;color:currentColor;font-size:15px;background-color:var(--v2p-color-bg-input);border:1px solid var(--button-border-color);border-radius:8px;transition:opacity .25s;resize:none}body form #topic_content::placeholder{color:var(--v2p-color-main-500);font-size:15px}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 #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{font-size:13px;cursor:pointer}body #syntax-selector label{color:var(--v2p-color-foreground)}body .snow{color:var(--v2p-color-main-400)}body .orange-dot{background:var(--v2p-color-orange-400)}body form[action="/notes/new"] .cell{background-color:rgba(0,0,0,0) !important}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) a[href^=http]{text-decoration:underline 2px;text-underline-offset:.46ex;color:currentColor;background-color:var(--v2p-color-main-100)}body :is(.topic_content,.reply_content,.v2p-topic-preview-content) a[href^=http]:hover{background-color:var(--v2p-color-main-200)}body :is(.topic_content,.reply_content,.v2p-topic-preview-content) a[href*="v2ex.com/t"],body :is(.topic_content,.reply_content,.v2p-topic-preview-content) a[href^="/t"],body :is(.topic_content,.reply_content,.v2p-topic-preview-content) a[href^="/go"]{text-decoration:underline 2px;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) a[href*="v2ex.com/t"]:hover,body :is(.topic_content,.reply_content,.v2p-topic-preview-content) a[href^="/t"]:hover,body :is(.topic_content,.reply_content,.v2p-topic-preview-content) a[href^="/go"]:hover{color:var(--v2p-color-accent-500);background-color:var(--v2p-color-accent-50)}body .select2-container--default .select2-selection--single{background-color:var(--v2p-color-background);border:1px solid var(--v2p-color-border)}body .select2-container--default .select2-selection--single .select2-selection__placeholder{color:var(--v2p-color-foreground)}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 .social_label:is(:link,:visited,:active){background-color:var(--v2p-color-main-100);box-shadow:none}body .social_label:is(:link,:visited,:active):hover{background-color:var(--v2p-color-main-200)}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;color:var(--v2p-color-foreground);font-weight:600;text-shadow:none;background:var(--v2p-color-main-100)}body .balance_area:hover,body a.balance_area:is(:link,:visited):hover{background:var(--v2p-color-main-200)}.box .tag:link,.box .tag:visited{color:var(--v2p-color-font-secondary);font-size:12px;background-color:var(--v2p-color-main-100);border-radius:5px}.box .tag::before{color:var(--v2p-color-main-500)}.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-weight:400;font-size:14px}#Top .tools .top{height:26px;margin-left:0;padding:0 6px;color:var(--v2p-color-main-500);line-height:26px;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-main-100)}#Main>.box{padding:0 12px}#Main>.box.node-header>.cell{margin:0 -12px}#Main>.box .cell{padding:20px 10px;background-image:none !important}#Main>.box .cell_ops{padding:15px 5px}#Main .topic_buttons{display:flex;flex-wrap:wrap;align-items:center;padding:8px 0;column-gap:5px;background:none}#Main .topic_buttons .topic_stats{flex:1;order:99;float:none;margin-left:10px;padding:0 !important;font-size:12px;text-shadow:none}#Main .topic_buttons .topic_thanked{font-size:12px}#Main .topic_buttons a.tb:link{display:flex;flex-direction:row-reverse;align-items:center;padding:5px;white-space:nowrap;text-shadow:none;column-gap:5px;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-main-100)}#Main .subtle{background-color:var(--v2p-color-bg-subtle);border-left:3px solid var(--v2p-color-accent-200)}#Main .subtle .topic_content{font-size:15px}#Main .vote:link{color:var(--v2p-color-main-500);border-color:var(--v2p-color-main-300);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-font-secondary)}#Main .cell .topic_info{position:relative;display:flex;align-items:center;user-select:none;pointer-events:none}#Main .cell .topic_info::after{position:absolute;top:0;right:0;bottom:-6px;left:0;z-index:1;background-color:var(--v2p-color-bg-content);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{position:relative;z-index:2;pointer-events:auto}#Main .cell .topic_info a[href^="/member"]{color:var(--v2p-color-main-500);font-weight:500}#Main .cell .count_livid{display:inline-block;padding:5px 10px;font-weight:400;font-size:12px;white-space:nowrap;border-radius:5px;user-select:none;color:var(--v2p-color-main-500);background-color:var(--v2p-color-main-200)}#Main .cell .count_orange{display:inline-block;padding:5px 10px;font-weight:400;font-size:12px;white-space:nowrap;border-radius:5px;user-select:none;color:var(--v2p-color-main-100);font-weight:bold;background-color:var(--v2p-color-orange-400)}#Main .cell .item_title .topic-link{font-weight:bold}#Main .cell.item tr>td:nth-child(2){width:30px}#Main .box>.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] .reply_content{padding-bottom:10px}#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{width:40px !important;height:40px !important;border-radius:5px;aspect-ratio:1}#Main .cell[id^=r]>table~.cell[id^=r]{--bg-reply: var(--v2p-color-bg-reply);position:relative;z-index:var(--zidx-expand-btn);padding:15px 0 0 15px;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:15px;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;font-size:15px}#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-main-600);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{position:relative;top:-3px;user-select:none}#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{position:relative;top:-4px;padding:5px 10px;color:var(--v2p-color-main-350);font-size:12px;background-color:rgba(0,0,0,0);border-radius:5px;user-select:none}#Main #Tabs{position:sticky;top:0;z-index:var(--zidx-tabs);display:flex;flex-wrap:wrap;gap:6px 8px;align-items:center;padding:10px;background-color:var(--v2p-color-bg-content);border-bottom:1px solid var(--box-border-color);user-select:none}#Main #Tabs .tab{margin:0}#Main #SecondaryTabs{padding:10px;background-color:var(--v2p-color-main-100);border-radius:5px}#Main .topic_content,#Main .reply_content{font-size:15.4px}#Main .topic_content a[href^="/member"],#Main .reply_content a[href^="/member"]{position:relative;bottom:1px;color:var(--v2p-color-main-500);font-size:13px;text-decoration:underline;text-underline-offset:.4ex}#Main .thank_area{font-size:12px}#Main .tab{color:var(--v2p-color-foreground);background-color:rgba(0,0,0,0);user-select:none}#Main .tab:not(.v2p-hover-btn):hover{background-color:var(--v2p-color-main-100)}#Main .tab_current{color:var(--box-background-color);background-color:var(--box-foreground-color);user-select:none}#Main #reply-box.reply-box-sticky{bottom:20px;z-index:var(--zidx-reply-box);margin:0 -10px;padding:0 22px;overflow:visible;border:none;border-radius:var(--box-border-radius);outline:2px solid var(--v2p-color-main-200)}#Main #reply-box .v2p-reply-wrap #reply_content{background-color:rgba(0,0,0,0);border:none}#Main #reply-box .v2p-reply-wrap #reply_content:focus{background-color:var(--v2p-color-bg-content);outline:none}#Main #reply-box .v2p-reply-wrap #reply_content::placeholder{color:var(--v2p-color-main-500);font-size:14px}#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-main-100)}#Main #notifications .cell[id^=n] .topic-link:visited{color:var(--v2p-color-foreground)}#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-main-300)}#Rightbar .cell:has(.light-toggle){font-size:13px}#Rightbar a.dark:is(:link,:active,:visited,:hover){color:var(--v2p-color-main-500)}#Rightbar a.dark:is(:link,:active,:visited,:hover):hover{color:var(--v2p-color-main-600)}#Bottom{position:sticky;top:100%}#Bottom a.dark{font-weight:400;font-size:13px}#Bottom a.dark:is(:link,:active,:visited,:hover){color:var(--v2p-color-main-500)} \uFEFFbody{position:relative}body.v2p-modal-open{overflow:hidden}body .button.v2p-prev-btn,body .button.v2p-next-btn{padding:0 15px}.v2p-hover-btn{position:relative;z-index:1;margin:0;white-space:nowrap;text-decoration:none;background:none;background-color:rgba(0,0,0,0);cursor:pointer;transition:color .2s;user-select:none}.v2p-hover-btn::before{position:absolute;top:0;right:-5px;bottom:0;left:-5px;z-index:-1;background-color:var(--v2p-color-bg-hover-btn);border-radius:5px;transform:scale(0.65);opacity:0;transition:background-color .2s,color .2s,transform .2s,opacity .2s;content:""}.v2p-hover-btn:hover{text-decoration:none}.v2p-hover-btn:hover::before{transform:scale(1);opacity:1}.v2p-hover-btn-disabled{opacity:.8;pointer-events:none}.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:hover .v2p-topic-preview-btn{visibility:visible}#Rightbar .v2p-info-row{display:block;color:var(--v2p-color-accent-500);font-size:12px;text-align:center}#Rightbar .v2p-info-row:hover{text-decoration:none;background-color:var(--v2p-color-accent-50)}.v2p-tool-box{position:sticky;top:20px;z-index:var(--zidx-tools-card)}.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;color:var(--v2p-color-main-600);font-size:12px}.v2p-tool{display:inline-flex;gap:0 5px;align-items:center;padding:3px 0}.v2p-tool .v2p-tool-icon{width:16px;height:16px}.v2p-topic-preview-btn{position:relative;top:-1px;margin-left:10px;color:var(--button-foreground-color);font-size:14px;background-color:var(--button-hover-color);border:none;border-radius:3px;outline:none;visibility:hidden;cursor:pointer}.v2p-topic-preview{padding:25px;line-height:1.4}.v2p-tp-info-bar{display:flex;gap:10px;align-items:center;margin-bottom:10px}.v2p-tp-info,.v2p-tp-read{display:inline-flex;gap:20px;align-items:center;padding:5px 10px;overflow:hidden;font-size:13px;background-color:var(--v2p-color-main-200);border-radius:5px}.v2p-tp-read{gap:4px;cursor:pointer;user-select:none}.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;border-radius:3px}a.v2p-topic-preview-title-link:hover{text-decoration:underline 2px;text-underline-offset:.46ex}.v2p-dot{margin:0 8px;font-weight:800;font-size:20px;font-size:15px}.v2p-paging{background:none !important}.v2p-paging.cell{border-bottom:none}.v2p-model-mask{position:fixed;z-index:var(--zidx-model-mask);padding:60px;overflow:hidden;overflow-y:auto;background-color:var(--v2p-color-mask);inset:0}.v2p-popup{position:absolute;top:0;left:0;z-index:var(--zidx-popup);font-size:14px;background:var(--v2p-color-bg-widget);border:1px solid var(--box-border-color);border-radius:8px;box-shadow:var(--v2p-widget-shadow);backdrop-filter:blur(16px)}.v2p-popup-content{width:max-content;overflow-y:auto}.v2p-toast{position:fixed;top:50px;left:50%;z-index:var(--zidx-toast);padding:10px 15px;color:var(--v2p-color-background);font-size:14px;background:var(--v2p-color-foreground);border-radius:8px;box-shadow:var(--v2p-toast-shadow);transform:translateX(-50%)}.v2p-model-main{position:relative;box-sizing:border-box;width:800px;height:100%;margin:0 auto;overflow-x:hidden;overflow-y:auto;background-color:var(--v2p-color-bg-content);border-radius:var(--box-border-radius)}.v2p-model-header{position:sticky;top:0;right:0;left:0;z-index:var(--zidx-model-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-model-title{padding:2px 0;overflow:hidden;font-weight:bold;font-size:16px;white-space:nowrap;text-overflow:ellipsis}.v2p-model-actions{display:flex;gap:0 10px;align-items:center;margin-left:auto}.v2p-model-loading{display:flex;align-items:center;justify-content:center;padding:50px 0;color:currentcolor}.v2p-model-loading .v2p-icon-loading{position:relative;right:-13px;width:50px}.v2p-no-pat{padding:30px 10px;font-size:15px;text-align:center}.v2p-no-pat .v2p-no-pat-title{font-weight:bold;font-size:16px}.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-main-100);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-main-100);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{position:relative;top:3px;display:inline-flex;align-items:center;column-gap:5px;user-select:none}.v2p-likes-box.v2p-thanked{color:var(--v2p-color-heart);font-weight:bold;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-controls{opacity:1}}.v2p-controls{display:inline-flex;align-items:center;margin-right:15px;font-size:12px;column-gap:15px;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-main-500)}.v2p-control:hover{color:var(--v2p-color-main-600)}.v2p-control.v2p-thanked{color:var(--v2p-color-heart);cursor:default}.v2p-control::after{z-index:var(--zidx-tip);width:max-content;min-width:30px;padding:2px 5px;overflow:hidden;color:var(--v2p-color-foreground);font-size:12px;white-space:nowrap;text-align:center;background-color:var(--v2p-color-bg-tooltip);border-radius:4px;box-shadow:var(--v2p-widget-shadow);pointer-events:none;position:absolute;top:-8px;transform:translateY(-100%);opacity:0}.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-main-400)}.topic_buttons .v2p-tb.v2p-hover-btn:hover{color:currentColor}.topic_buttons .v2p-tb.v2p-hover-btn::after{display:none}.v2p-tb-icon{width:15px;height:15px}.v2p-emoji-container{max-height:285px;padding:15px 18px;overflow-y:auto;color:var(--v2p-color-main-600)}.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{display:inline-block;width:73px;height:73px;overflow:hidden;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-weight:bold;font-size:16px}.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{display:-webkit-box;overflow:hidden;-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{display:inline-block;margin-bottom:2px;padding:0 3px;font-size:12px;background-color:var(--v2p-color-main-200);border-radius:3px;cursor:pointer}.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:6px;font-size:20px}.v2p-emoji{padding:2px;border-radius:4px;cursor:pointer}.v2p-emoji:hover{background-color:var(--box-background-hover-color)}.v2p-decode{position:relative;padding:2px 4px;color:var(--v2p-color-orange-400);font-size:13px;text-decoration:none;background-color:var(--v2p-color-orange-50);cursor:copy}.v2p-decode:hover{color:var(--v2p-color-orange-400)}.v2p-decode:hover::after{opacity:1}.v2p-decode::after{z-index:var(--zidx-tip);width:max-content;min-width:30px;padding:2px 5px;overflow:hidden;color:var(--v2p-color-foreground);font-size:12px;white-space:nowrap;text-align:center;background-color:var(--v2p-color-bg-tooltip);border-radius:4px;box-shadow:var(--v2p-widget-shadow);pointer-events:none;position:absolute;top:-8px;left:50%;transform:translate(-50%, -100%);opacity:0;content:attr(data-title)}.v2p-reply-content{position:relative}.v2p-reply-content .v2p-expand-btn.normal.button{position:absolute;bottom:5px;left:50%;z-index:var(--zidx-expand-btn);font-weight:400;font-size:12px;transform:translateX(-50%)}.v2p-reply-content.v2p-collapsed::before{position:absolute;right:0;bottom:0;left:0;z-index:var(--zidx-expand-mask);height:130px;background:linear-gradient(to top, var(--bg-reply) 10px, transparent);content:"";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(--button-foreground-color);background:var(--button-hover-color);box-shadow:var(--button-hover-shadow)}.v2p-empty-content{display:flex;flex-direction:column;align-items:center;padding-top:20px;color:var(--v2p-color-font-secondary);font-size:14px}.v2p-empty-content .v2p-text-emoji{font-size:20px}.v2p-topic-reply-ref{margin:0 -10px 15px;padding:5px 10px;color:var(--v2p-color-main-500);font-size:13px;background-color:var(--v2p-color-main-100);border-radius:5px}.v2p-topic-reply-box{margin-top:50px;padding:30px 0;color:var(--v2p-color-main-500);font-size:14px;line-height:1.55;border-top:1px solid var(--v2p-color-divider)}.v2p-topic-reply~.v2p-topic-reply{margin-top:15px}.v2p-topic-reply-member{display:inline;color:var(--v2p-color-main-700);font-weight:bold}.v2p-topic-reply-avatar{position:relative;top:2px;width:15px;height:15px;margin-right:5px;object-fit:cover;overflow:hidden;background-color:var(--v2p-color-main-200);border-radius:2px}.v2p-topic-reply-content{display:inline}.v2p-more-reply-tip{margin-top:20px;color:var(--v2p-color-main-400);font-size:13px;text-align:center}.v2p-reply-wrap{height:unset;min-height:140px !important;max-height:800px !important;overflow:hidden;color:currentColor;font-size:15px;background-color:var(--v2p-color-bg-input);border:1px solid var(--button-border-color);border-radius:8px;transition:opacity .25s;resize:none}.v2p-reply-wrap::placeholder{color:var(--v2p-color-main-500);font-size:15px}.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-upload-bar{padding:6px 10px;color:var(--v2p-color-main-500);font-size:12px;background-color:var(--v2p-color-bg-input);border-top:1px dashed var(--v2p-color-main-300);cursor:pointer}.v2p-reply-upload-bar-disabled{pointer-events:none}.v2p-footer{position:relative;display:flex;align-items:center;justify-content:space-between;padding:35px 10px;color:var(--v2p-color-main-500);font-size:12px;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%;display:inline-flex;box-sizing:border-box;padding:3px 25px;background-color:var(--v2p-color-bg-footer);transform:translateX(-50%)}.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>span{width:20px}.v2p-color-mode-toggle{width:22px;height:22px;opacity:.8}.v2p-color-mode-toggle:hover{opacity:1}.v2p-reply-tools-box{position:relative;display:inline-flex;gap:0 5px;align-items:center;margin-right:auto;padding:2px 0;font-size:13px}.v2p-reply-tools-icon{display:inline-block;width:20px;height:20px}.v2p-reply-tool-content{padding:5px;border-radius:5px}.v2p-reply-tool{padding:5px 10px;white-space:nowrap;border-radius:4px;cursor:pointer}.v2p-reply-tool:hover{background-color:var(--v2p-color-main-200)} `;
  2753.  
  2754. // src/user-scripts/index.ts
  2755. if (typeof window.GM_addStyle !== "undefined") {
  2756. window.GM_addStyle(style);
  2757. } else {
  2758. document.addEventListener("DOMContentLoaded", () => {
  2759. $(`<style type='text/css'>${style}</style>`).appendTo("head");
  2760. });
  2761. }
  2762. document.addEventListener("DOMContentLoaded", () => {
  2763. const commonRegex = patternToRegex("https://v2ex.com/*", "https://www.v2ex.com/*");
  2764. const topicRegex = patternToRegex("https://v2ex.com/t/*", "https://www.v2ex.com/t/*");
  2765. const url = window.location.href;
  2766. void (async () => {
  2767. if (commonRegex.test(url)) {
  2768. Promise.resolve().then(() => init_common());
  2769. Promise.resolve().then(() => init_home());
  2770. }
  2771. if (topicRegex.test(url)) {
  2772. await Promise.resolve().then(() => (init_topic(), topic_exports));
  2773. }
  2774. })();
  2775. });