🏠 Home 

bangumi-comment-enhance

Improve comment reading experience, hide certain comments, sort featured comments by reaction count or reply count, and more.


Install this script?
  1. // ==UserScript==
  2. // @name bangumi-comment-enhance
  3. // @version 0.2.4
  4. // @description Improve comment reading experience, hide certain comments, sort featured comments by reaction count or reply count, and more.
  5. // @author Flynn Cao
  6. // @namespace https://flynncao.uk/
  7. // @match https://bangumi.tv/*
  8. // @match https://chii.in/*
  9. // @match https://bgm.tv/*
  10. // @include /^https?:\/\/(((fast\.)?bgm\.tv)|chii\.in|bangumi\.tv)*/
  11. // @license MIT
  12. // ==/UserScript==
  13. 'use strict'
  14. // https://www.iconfont.cn/collections/detail?spm=a313x.user_detail.i1.dc64b3430.57e63a81itWm4A&cid=12086
  15. const Icons = {
  16. answerSheet:
  17. '<svg t="1741855047626" class="icon" viewBox="0 0 #### ####" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2040" width="256" height="256"><path d="M188.8 135.7c-29.7 0-53.8 24.1-53.8 53.7v644.7c0 29.7 24.1 53.7 53.8 53.7h645.4c29.7 0 53.8-24.1 53.8-53.7V189.4c0-29.7-24.1-53.7-53.8-53.7H188.8z m-13-71.1h671.5c61.8 0 111.9 50.1 111.9 111.8v670.8c0 61.7-50.1 111.8-111.9 111.8H175.8C114 959 63.9 909 63.9 847.2V176.4c0-61.8 50.1-111.8 111.9-111.8z m0 0" fill="#333333" p-id="2041"></path><path d="M328 328h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36zM556 332h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36zM784 332h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36z" fill="#333333" p-id="2042"></path><path d="M328 546h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36zM556 550h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36zM784 550h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36z" fill="#333333" p-id="2043"></path><path d="M328 764h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36zM556 768h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36zM784 768h-88c-19.8 0-36-16.2-36-36s16.2-36 36-36h88c19.8 0 36 16.2 36 36s-16.2 36-36 36z" fill="#333333" p-id="2044"></path></svg>',
  18. sorting:
  19. '<svg t="1741855109866" class="icon" viewBox="0 0 #### ####" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2338" width="256" height="256"><path d="M375 898c-19.8 0-36-16.2-36-36V162c0-19.8 16.2-36 36-36s36 16.2 36 36v700c0 19.8-16.2 36-36 36z" fill="#333333" p-id="2339"></path><path d="M398.2 889.6c-15.2 12.7-38 10.7-50.7-4.4L136.6 633.9c-12.7-15.2-10.7-38 4.4-50.7 15.2-12.7 38-10.7 50.7 4.4l210.8 251.3c12.8 15.2 10.8 38-4.3 50.7zM649 126c19.8 0 36 16.2 36 36v700c0 19.8-16.2 36-36 36s-36-16.2-36-36V162c0-19.8 16.2-36 36-36z" fill="#333333" p-id="2340"></path><path d="M625.8 134.4c15.2-12.7 38-10.7 50.7 4.4l210.8 251.3c12.7 15.2 10.7 38-4.4 50.7-15.2 12.7-38 10.7-50.7-4.4L621.4 185.1c-12.7-15.2-10.7-38 4.4-50.7z" fill="#333333" p-id="2341"></path></svg>',
  20. font: '<svg t="1741855156691" class="icon" viewBox="0 0 #### ####" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2635" width="256" height="256"><path d="M859 201H165c-19.8 0-36-16.2-36-36s16.2-36 36-36h694c19.8 0 36 16.2 36 36s-16.2 36-36 36z" fill="#585757" p-id="2636"></path><path d="M476 859V165c0-19.8 16.2-36 36-36s36 16.2 36 36v694c0 19.8-16.2 36-36 36s-36-16.2-36-36z" fill="#585757" p-id="2637"></path></svg>',
  21. gear: '<svg t="1741861365461" class="icon" viewBox="0 0 #### ####" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2783" data-darkreader-inline-fill="" width="256" height="256"><path d="M594.9 64.8c36.8-0.4 66.9 29.1 67.3 65.9v7.8c0 38.2 31.5 69.4 70.2 69.4 12.3 0 24.5-3.3 35-9.3l7.1-4.1c10.3-5.9 22.1-9 33.9-9 23.9 0 46.2 12.5 58.3 32.8L949.9 359c18.7 31.6 7.6 71.9-24.6 90.1l-6.9 3.9c-34 19.2-45.7 61.2-26.4 93.8 6.1 10.3 14.9 18.9 25.4 24.8l7 3.9c32.3 18 43.6 58.5 24.8 90.2L866 806.3c-9.1 15.2-23.8 26.2-41 30.6-17.1 4.4-35.3 2.2-50.7-6.4l-7-3.9c-21.9-12.2-48.5-12.4-70.6-0.4-10.7 5.9-19.7 14.5-25.9 25-6.1 10.4-9.4 22.1-9.3 33.8v7.8c0.1 17.8-7.2 34.7-20 47.1-12.6 12.2-29.6 19-47.2 19H428c-36.6 0.3-66.7-29-67.2-65.5l-0.1-7.8c-0.1-18.4-7.6-36-20.8-48.8-22.5-22-56.9-26.5-84.3-10.9l-7 4.1c-10.3 5.8-22 8.9-33.8 8.9-23.9 0-46.1-12.4-58.2-32.8L73.2 665.2c-8.9-15.1-11.3-33.2-6.7-50.1 4.6-16.9 15.8-31.3 31.2-39.8l6.8-3.9c16.2-9 28.2-24.2 33.1-42.1 4.9-17.4 2.4-36.1-6.9-51.6-6.2-10.4-15.1-19-25.7-24.9l-6.9-3.9c-15.5-8.4-27-22.8-31.7-39.8-4.7-17-2.3-35.2 6.7-50.4L156.3 218c9-15.1 23.8-26.2 41-30.6 17.1-4.4 35.3-2.1 50.7 6.5l7.1 3.9c21.9 12.3 48.6 12.5 70.7 0.5 10.8-5.9 19.8-14.6 26-25.1 6.1-10.4 9.3-22.2 9.2-34.1v-7.9c-0.2-17.8 7-34.8 19.8-47.2 12.6-12.3 29.7-19.1 47.5-19.1h166.6z m-163.2 71c-3.1 0-6.1 1.2-8.4 3.3-1.9 1.8-2.9 4.2-2.9 6.8l0.1 7.6c0.2 21.2-5.4 42-16.3 60.3a120.02 120.02 0 0 1-45.2 43.7c-37.4 20.4-82.6 20.2-119.7-0.7l-6.8-3.8c-2.8-1.6-6.1-2-9.2-1.2-2.8 0.7-5.3 2.5-6.8 5l-80 135.1c-2.7 4.5-1.1 10.2 4.1 13l6.7 3.7c18.6 10.3 34 25.3 44.7 43.4 16.3 27.6 20.6 59.9 12.1 90.8-8.5 30.8-29 56.9-56.9 72.5l-6.6 3.7c-5 2.9-6.6 8.5-3.9 12.9l80 135.1c1.9 3.2 5.7 5.3 10 5.3 2.1 0 4.3-0.5 6.1-1.6l6.8-3.8c18.1-10.3 38.8-15.8 59.9-15.8 31.8 0 62 12.3 84.7 34.4 23 22.5 35.9 52.6 36 84.7v7.5c0 5.2 4.9 9.9 11.3 9.9h160c3.2 0 6.2-1.2 8.3-3.3 1.8-1.7 2.9-4.2 2.9-6.7v-7.5c-0.1-20.9 5.6-41.6 16.4-59.8 10.8-18.3 26.4-33.4 45.1-43.7 37.3-20.4 82.4-20.2 119.5 0.6l6.7 3.8c2.8 1.5 6.1 1.9 9.2 1.1 2.8-0.7 5.3-2.5 6.8-5l80-135c2.7-4.5 1.1-10.2-4-13l-6.7-3.7c-18.4-10.2-33.7-25.2-44.4-43.3-33.8-57.1-13.4-130.5 45-163.5l6.6-3.7c5.1-2.9 6.6-8.5 3.9-13l-79.9-135.1c-2.2-3.4-6-5.4-10-5.3-2.1 0-4.3 0.5-6.1 1.6l-6.8 3.8c-18.3 10.5-39.1 16-60.2 16-66.5 0.2-120.6-53.5-120.8-119.9v-7.5c0-5.3-4.8-10-11.3-10l-160 0.3z m-3.4-15.5" p-id="2784"></path><path d="M512 584c39.8 0 72-32.2 72-72s-32.2-72-72-72-72 32.2-72 72 32.2 72 72 72z m0 72c-79.5 0-144-64.5-144-144s64.5-144 144-144 144 64.5 144 144-64.5 144-144 144z m0 0" p-id="2785"></path></svg>',
  22. }
  23. const NAMESPACE = 'BangumiCommentEnhance'
  24. const Storage = {
  25. set(key, value) {
  26. localStorage.setItem(`${NAMESPACE}_${key}`, JSON.stringify(value))
  27. },
  28. get(key) {
  29. const value = localStorage.getItem(`${NAMESPACE}_${key}`)
  30. return value ? JSON.parse(value) : undefined
  31. },
  32. async init(settings) {
  33. const keys = Object.keys(settings)
  34. for (const key of keys) {
  35. const value = Storage.get(key)
  36. if (value === undefined) {
  37. Storage.set(key, settings[key])
  38. }
  39. }
  40. },
  41. }
  42. function initSettings(userSettings) {
  43. // Create and inject styles
  44. const injectStyles = () => {
  45. const styleEl = document.createElement('style')
  46. styleEl.textContent = `
  47. .fixed-container {
  48. position: fixed;
  49. z-index: 100;
  50. width: calc(100vw - 50px);
  51. max-width: 380px;
  52. background-color: rgba(255, 255, 255, 0.8);
  53. backdrop-filter: blur(8px);
  54. left: 50%;
  55. top: 50%;
  56. transform: translate(-50%, -50%);
  57. border-radius: 12px;
  58. box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
  59. padding: 30px;
  60. text-align: center;
  61. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  62. box-sizing: border-box;
  63. display: none;
  64. }
  65. [data-theme="dark"] .fixed-container {
  66. background-color: rgba(30, 30, 30, 0.8);
  67. color: #fff;
  68. }
  69. .container-header {
  70. display: flex;
  71. justify-content: space-between;
  72. align-items: center;
  73. margin-bottom: 16px;
  74. }
  75. .dropdown-select {
  76. padding: 8px;
  77. padding-right:16px;
  78. border-radius: 6px;
  79. border: 1px solid #e2e2e2;
  80. background-color: #f5f5f5;
  81. font-size: 14px;
  82. width: 100%;
  83. }
  84. [data-theme="dark"] .dropdown-select {
  85. background-color: #333;
  86. border-color: #555;
  87. color: #fff;
  88. }
  89. .checkbox-container {
  90. display: flex;
  91. align-items: center;
  92. margin-bottom: 16x;
  93. text-align: left;
  94. font-size:14px;
  95. }
  96. .input-group {
  97. display: flex;
  98. align-items: center;
  99. margin-bottom: 16px;
  100. justify-content:flex-start;
  101. }
  102. .input-group label {
  103. text-align: left;
  104. font-size: 14px;
  105. margin-right:8px;
  106. }
  107. .input-group input {
  108. max-width: 40px;
  109. padding: 6px;
  110. border-radius: 6px;
  111. border: 1px solid #e2e2e2;
  112. text-align: center;
  113. }
  114. [data-theme="dark"] .input-group input {
  115. background-color: #333;
  116. border-color: #555;
  117. color: #fff;
  118. }
  119. .button-group {
  120. display: flex;
  121. justify-content: space-between;
  122. gap: 12px;
  123. }
  124. .button-group button {
  125. flex: 1;
  126. padding: 10px;
  127. border-radius: 6px;
  128. border: none;
  129. font-size: 16px;
  130. cursor: pointer;
  131. }
  132. .cancel-btn {
  133. background-color: white;
  134. border: 1px solid #e2e2e2;
  135. }
  136. [data-theme="dark"] .cancel-btn {
  137. background-color: #333;
  138. border-color: #555;
  139. color: #fff;
  140. }
  141. .save-btn {
  142. background-color: #333;
  143. color: white;
  144. }
  145. [data-theme="dark"] .save-btn {
  146. background-color: #555;
  147. }
  148. button:hover{
  149. filter: brightness(1.5);
  150. transition: all 0.3s;
  151. }
  152. strong svg{
  153. max-width:21px;
  154. max-height:21px;
  155. transform: translateY(2px);
  156. margin-right: 10px;
  157. }
  158. [data-theme="dark"] strong svg{
  159. filter: invert(1);
  160. }
  161. input[type="checkbox"]{
  162. width:20px;
  163. height:20px;
  164. margin:0;
  165. cursor:pointer;
  166. }
  167. .checkbox-container input[type="checkbox"] {
  168. margin-right: 12px;
  169. transform: translateY(1.5px);
  170. }
  171. `
  172. document.head.append(styleEl)
  173. }
  174. // Create DOM elements and construct the UI
  175. const createSettingsDialog = () => {
  176. // Create container
  177. const container = document.createElement('div')
  178. container.className = 'fixed-container'
  179. // Create header with dropdown
  180. const header = document.createElement('div')
  181. header.className = 'container-header'
  182. const spacerLeft = document.createElement('div')
  183. spacerLeft.style.width = '24px'
  184. const dropdown = document.createElement('select')
  185. dropdown.className = 'dropdown-select'
  186. const optionHot = document.createElement('option')
  187. optionHot.value = 'reactionCount'
  188. optionHot.textContent = '按热度(贴贴数)排序'
  189. const optionReply = document.createElement('option')
  190. optionReply.value = 'replyCount'
  191. optionReply.textContent = '按评论数排序'
  192. const optionRecent = document.createElement('option')
  193. optionRecent.value = 'newFirst'
  194. optionRecent.textContent = '按时间排序(最新在前)'
  195. const optionOld = document.createElement('option')
  196. optionOld.value = 'oldFirst'
  197. optionOld.textContent = '按时间排序(最旧在前)'
  198. dropdown.append(optionHot)
  199. dropdown.append(optionRecent)
  200. dropdown.append(optionOld)
  201. dropdown.append(optionReply)
  202. dropdown.value = userSettings.sortMode || 'reactionCount'
  203. const spacerRight = document.createElement('div')
  204. spacerRight.style.width = '24px'
  205. header.append($('<strong></strong>').html(Icons.sorting)[0])
  206. header.append(dropdown)
  207. header.append(spacerRight)
  208. // Create checkbox
  209. const checkboxContainer = document.createElement('div')
  210. checkboxContainer.className = 'checkbox-container'
  211. const checkbox = document.createElement('input')
  212. checkbox.type = 'checkbox'
  213. checkbox.id = 'showMine'
  214. checkbox.checked = userSettings.stickyMentioned || false
  215. const checkboxLabel = document.createElement('label')
  216. checkboxLabel.htmlFor = 'showMine'
  217. checkboxLabel.textContent = '置顶我发表/回复我的帖子'
  218. checkboxContainer.append(checkbox)
  219. checkboxContainer.append(checkboxLabel)
  220. // Create min effective number input
  221. const minEffGroup = document.createElement('div')
  222. minEffGroup.className = 'input-group'
  223. const minEffLabel = document.createElement('label')
  224. minEffLabel.htmlFor = 'minEffectiveNumber'
  225. minEffLabel.textContent = '最低有效字数 (>=0)'
  226. const minEffInput = document.createElement('input')
  227. minEffInput.type = 'number'
  228. minEffInput.id = 'minEffectiveNumber'
  229. minEffInput.value = userSettings.minimumFeaturedCommentLength || 0
  230. minEffGroup.append($('<strong></strong>').html(Icons.font)[0])
  231. minEffGroup.append(minEffLabel)
  232. minEffGroup.append(minEffInput)
  233. // Create max selected posts input
  234. const maxPostsGroup = document.createElement('div')
  235. maxPostsGroup.className = 'input-group'
  236. const maxPostsLabel = document.createElement('label')
  237. maxPostsLabel.htmlFor = 'maxSelectedPosts'
  238. maxPostsLabel.textContent = '最大精选评论数 (>0)'
  239. const maxPostsInput = document.createElement('input')
  240. maxPostsInput.type = 'number'
  241. maxPostsInput.id = 'maxSelectedPosts'
  242. maxPostsInput.value = userSettings.maxFeaturedComments || 1
  243. maxPostsGroup.append($('<strong></strong>').html(Icons.answerSheet)[0])
  244. maxPostsGroup.append(maxPostsLabel)
  245. maxPostsGroup.append(maxPostsInput)
  246. const spaceHr = document.createElement('hr')
  247. spaceHr.style.marginBottom = '16px'
  248. spaceHr.style.border = 'none'
  249. // Create buttons
  250. const buttonGroup = document.createElement('div')
  251. buttonGroup.className = 'button-group'
  252. const cancelBtn = document.createElement('button')
  253. cancelBtn.className = 'cancel-btn'
  254. cancelBtn.textContent = '取消'
  255. const saveBtn = document.createElement('button')
  256. saveBtn.className = 'save-btn'
  257. saveBtn.textContent = '保存'
  258. buttonGroup.append(cancelBtn)
  259. buttonGroup.append(saveBtn)
  260. // Assemble everything
  261. container.append(header)
  262. container.append(minEffGroup)
  263. container.append(maxPostsGroup)
  264. container.append(checkboxContainer)
  265. container.append(spaceHr)
  266. container.append(buttonGroup)
  267. // Add to document
  268. document.body.append(container)
  269. return {
  270. container,
  271. dropdown,
  272. checkbox,
  273. minEffInput,
  274. maxPostsInput,
  275. cancelBtn,
  276. saveBtn,
  277. }
  278. }
  279. // Initialize settings from localStorage
  280. const initSettings = (elements) => {
  281. const { dropdown, checkbox, minEffInput, maxPostsInput } = elements
  282. if (localStorage.getItem('sortBy')) {
  283. dropdown.value = localStorage.getItem('sortBy')
  284. }
  285. if (localStorage.getItem('showMine') !== null) {
  286. checkbox.checked = localStorage.getItem('showMine') === 'true'
  287. }
  288. if (localStorage.getItem('minEffectiveNumber')) {
  289. minEffInput.value = localStorage.getItem('minEffectiveNumber')
  290. }
  291. if (localStorage.getItem('maxSelectedPosts')) {
  292. maxPostsInput.value = localStorage.getItem('maxSelectedPosts')
  293. }
  294. }
  295. // Save settings
  296. const saveSettings = (elements) => {
  297. const { container, dropdown, checkbox, minEffInput, maxPostsInput } = elements
  298. Storage.set(
  299. 'minimumFeaturedCommentLength',
  300. Math.max(Number.parseInt(minEffInput.value) || 0, 0),
  301. )
  302. Storage.set(
  303. 'maxFeaturedComments',
  304. Number.parseInt(maxPostsInput.value) > 0 ? Number.parseInt(maxPostsInput.value) : 1,
  305. )
  306. // Storage.set('hidePlainComments', setHidePlainCommentsInput.is(':checked'))
  307. Storage.set('stickyMentioned', checkbox.checked)
  308. Storage.set('sortMode', dropdown.value)
  309. // Trigger custom event
  310. const event = new CustomEvent('settingsSaved', {
  311. detail: {
  312. sortBy: dropdown.value,
  313. showMine: checkbox.checked,
  314. minEffectiveNumber: Number.parseInt(minEffInput.value),
  315. maxSelectedPosts: Number.parseInt(maxPostsInput.value),
  316. },
  317. })
  318. document.dispatchEvent(event)
  319. // jQuery compatibility
  320. if (window.jQuery) {
  321. jQuery(document).trigger('settingsSaved', {
  322. sortBy: dropdown.value,
  323. showMine: checkbox.checked,
  324. minEffectiveNumber: Number.parseInt(minEffInput.value),
  325. maxSelectedPosts: Number.parseInt(maxPostsInput.value),
  326. })
  327. }
  328. hideDialog(container)
  329. }
  330. // Show dialog
  331. const showDialog = (container) => {
  332. container.style.display = 'block'
  333. }
  334. // Hide dialog
  335. const hideDialog = (container) => {
  336. container.style.display = 'none'
  337. }
  338. // Main initialization function
  339. const init = () => {
  340. // Inject CSS
  341. injectStyles()
  342. // Create the dialog
  343. const elements = createSettingsDialog()
  344. // Initialize settings
  345. initSettings(elements)
  346. // Setup event listeners
  347. elements.saveBtn.addEventListener('click', () => saveSettings(elements))
  348. elements.cancelBtn.addEventListener('click', () => hideDialog(elements.container))
  349. // Expose API
  350. window.settingsDialog = {
  351. show: () => showDialog(elements.container),
  352. hide: () => hideDialog(elements.container),
  353. save: () => saveSettings(elements),
  354. getElements: () => elements,
  355. }
  356. }
  357. // Auto-initialize when DOM is ready
  358. if (document.readyState === 'loading') {
  359. document.addEventListener('DOMContentLoaded', init)
  360. } else {
  361. init()
  362. }
  363. }
  364. const BGM_EP_REGEX = /^https:\/\/(((fast\.)?bgm\.tv)|(chii\.in)|(bangumi\.tv))\/ep\/\d+/
  365. const BGM_GROUP_REGEX =
  366. /^https:\/\/(((fast\.)?bgm\.tv)|(chii\.in)|(bangumi\.tv))\/group\/topic\/\d+/
  367. // quickSort is not strictly needed cause JavaScript has built-in sort method based on quicksort/selection algorithm
  368. function quickSort(arr, sortKey, changeCompareDirection = false) {
  369. if (arr.length <= 1) {
  370. return arr
  371. }
  372. const pivot = arr[0]
  373. const left = []
  374. const right = []
  375. for (let i = 1; i < arr.length; i++) {
  376. const element = arr[i]
  377. const elementImportant = element.important || false
  378. const pivotImportant = pivot.important || false
  379. let compareR###lt
  380. if (elementImportant !== pivotImportant) {
  381. compareR###lt = elementImportant // true if element is important and pivot is not
  382. } else if (changeCompareDirection) {
  383. compareR###lt = element[sortKey] < pivot[sortKey]
  384. } else {
  385. compareR###lt = element[sortKey] > pivot[sortKey]
  386. }
  387. if (compareR###lt) {
  388. left.push(element)
  389. } else {
  390. right.push(element)
  391. }
  392. }
  393. return quickSort(left, sortKey, changeCompareDirection).concat(
  394. pivot,
  395. quickSort(right, sortKey, changeCompareDirection),
  396. )
  397. }
  398. function purifiedDatetimeInMillionSeconds(timestamp) {
  399. return new Date(timestamp.trim().replace('- ', '')).getTime()
  400. }
  401. function processComments(userSettings) {
  402. // check if the target element is valid
  403. const username = $('.idBadgerNeue .avatar').attr('href')
  404. ? $('.idBadgerNeue .avatar').attr('href').split('/user/')[1]
  405. : ''
  406. const conservedPostID =
  407. $(location).attr('href').split('#').length > 1 ? $(location).attr('href').split('#')[1] : null
  408. const allCommentRows = $('.row.row_reply.clearit')
  409. let plainCommentsCount = 0
  410. let featuredCommentsCount = 0
  411. const minimumContentLength = userSettings.minimumFeaturedCommentLength
  412. const container = $('#comment_list')
  413. const plainCommentElements = []
  414. const featuredCommentElements = []
  415. let conservedRow = null
  416. allCommentRows.each(function (index, row) {
  417. const that = $(this)
  418. const content = $(row)
  419. .find(BGM_EP_REGEX.test(location.href) ? '.message.clearit' : '.inner')
  420. .text()
  421. let commentScore = 0
  422. // prioritize @me comments on
  423. const highlightMentionedColor = '#ff8c00'
  424. const subReplyContent = that.find('.topic_sub_reply')
  425. const commentsCount = subReplyContent.find('.sub_reply_bg').length
  426. const mentionedInMainComment =
  427. userSettings.stickyMentioned &&
  428. that.find('.avatar').attr('href').split('/user/')[1] === username
  429. let mentionedInSubReply = false
  430. if (mentionedInMainComment) {
  431. that.css('border-color', highlightMentionedColor)
  432. that.css('border-width', '1px')
  433. that.css('border-style', 'dashed')
  434. commentScore += 10000
  435. }
  436. that.find(`.topic_sub_reply .sub_reply_bg.clearit`).each(function (index, element) {
  437. if (userSettings.stickyMentioned && $(element).attr('data-item-user') === username) {
  438. $(element).css('border-color', highlightMentionedColor)
  439. $(element).css('border-width', '1px')
  440. $(element).css('border-style', 'dashed')
  441. commentScore += 1000
  442. mentionedInSubReply = true
  443. }
  444. })
  445. const important = mentionedInMainComment || mentionedInSubReply
  446. that.find('span.num').each(function (index, element) {
  447. commentScore += Number.parseInt($(element).text())
  448. })
  449. const hasConservedReply = conservedPostID && that.find(`#${conservedPostID}`).length > 0
  450. if (hasConservedReply) conservedRow = row
  451. if (!hasConservedReply) subReplyContent.hide()
  452. const timestampArea = that.find('.action').first()
  453. if (commentsCount !== 0) {
  454. const a = $(
  455. `<a class="expand_all" href="javascript:void(0)" style="margin:0 3px 0 5px;"><span class="ico ico_reply">展开(+${commentsCount})</span></a>`,
  456. )
  457. mentionedInSubReply && a.css('color', highlightMentionedColor)
  458. a.on('click', function () {
  459. subReplyContent.slideToggle()
  460. })
  461. const el = $(`<div class="action"></div>`).append(a)
  462. timestampArea.after(el)
  463. }
  464. // check if this comment meets the requirement of minimumContentLength
  465. const isShortReply = content.trim().length < minimumContentLength
  466. let isFeatured =
  467. userSettings.sortMode === 'reactionCount' ? commentScore >= 1 : commentsCount >= 1
  468. if (isShortReply || featuredCommentsCount >= userSettings.maxFeaturedComments) {
  469. isFeatured = false
  470. }
  471. // conserved reply must be fixed
  472. if (hasConservedReply || important) {
  473. isFeatured = true
  474. }
  475. const timestamp = isFeatured
  476. ? $(row)
  477. .find('.action:eq(0) small')
  478. .first()
  479. .contents()
  480. .filter(function () {
  481. return this.nodeType === 3 // Node.TEXT_NODE === 3
  482. })
  483. .first()
  484. .text()
  485. : $(row).find('small').text().trim()
  486. if (isFeatured) {
  487. featuredCommentsCount++
  488. featuredCommentElements.push({
  489. element: row,
  490. score: commentScore,
  491. commentsCount,
  492. timestampNumber: purifiedDatetimeInMillionSeconds(timestamp),
  493. important,
  494. })
  495. } else {
  496. plainCommentsCount++
  497. plainCommentElements.push({
  498. element: row,
  499. score: commentScore,
  500. timestamp,
  501. timestampNumber: purifiedDatetimeInMillionSeconds(timestamp),
  502. })
  503. }
  504. })
  505. return {
  506. plainCommentsCount,
  507. featuredCommentsCount,
  508. container,
  509. plainCommentElements,
  510. featuredCommentElements,
  511. conservedRow,
  512. }
  513. }
  514. ;(async function () {
  515. if (!BGM_EP_REGEX.test(location.href) && !BGM_GROUP_REGEX.test(location.href)) {
  516. return
  517. }
  518. Storage.init({
  519. hidePlainComments: true,
  520. minimumFeaturedCommentLength: 15,
  521. maxFeaturedComments: 99,
  522. sortMode: 'reactionCount',
  523. stickyMentioned: false,
  524. })
  525. const userSettings = {
  526. hidePlainComments: Storage.get('hidePlainComments'),
  527. minimumFeaturedCommentLength: Storage.get('minimumFeaturedCommentLength'),
  528. maxFeaturedComments: Storage.get('maxFeaturedComments'),
  529. sortMode: Storage.get('sortMode'),
  530. stickyMentioned: Storage.get('stickyMentioned'),
  531. }
  532. const sortModeData = userSettings.sortMode || 'reactionCount'
  533. /**
  534. * Main
  535. */
  536. let {
  537. plainCommentsCount,
  538. featuredCommentsCount,
  539. container,
  540. plainCommentElements,
  541. featuredCommentElements,
  542. conservedRow,
  543. } = processComments(userSettings)
  544. let stateBar = container.find('.row_state.clearit')
  545. if (stateBar.length === 0) {
  546. stateBar = $(`<div id class="row_state clearit"></div>`)
  547. }
  548. const hiddenCommentsInfo = $(
  549. `<div class="filtered" id="toggleFilteredBtn" style="cursor:pointer;color:#48a2c3;">点击展开/折叠剩余${plainCommentsCount}条普通评论</div>`,
  550. ).click(function () {
  551. $('#comment_list_plain').slideToggle()
  552. })
  553. stateBar.append(hiddenCommentsInfo)
  554. container.find('.row').detach()
  555. const settingBtn = $('<strong></strong>')
  556. .css({
  557. display: 'inline-block',
  558. width: '20px',
  559. height: '20px',
  560. transform: 'translate(4px, -3px)',
  561. cursor: 'pointer',
  562. })
  563. .html(Icons.gear)
  564. .click(() => window.settingsDialog.show())
  565. container.append(
  566. $(
  567. '<h3 style="padding:10px;display:flex;width:100%;align-items:center;">所有精选评论</h3>',
  568. ).append(settingBtn),
  569. )
  570. const trinity = {
  571. reactionCount() {
  572. featuredCommentElements = quickSort(featuredCommentElements, 'score')
  573. },
  574. replyCount() {
  575. featuredCommentElements = quickSort(featuredCommentElements, 'commentsCount')
  576. },
  577. oldFirst() {
  578. featuredCommentElements = quickSort(featuredCommentElements, 'timestampNumber', true)
  579. },
  580. newFirst() {
  581. featuredCommentElements = quickSort(featuredCommentElements, 'timestampNumber')
  582. },
  583. }
  584. trinity[sortModeData]()
  585. /**
  586. * Append components
  587. */
  588. featuredCommentElements.forEach(function (element) {
  589. container.append($(element.element))
  590. })
  591. plainCommentElements.forEach(function (element) {
  592. container.append($(element.element))
  593. })
  594. container.append(stateBar)
  595. const plainCommentsContainer = $('<div id="comment_list_plain" style="margin-top:2rem;"></div>')
  596. if (userSettings.hidePlainComments) {
  597. plainCommentsContainer.hide()
  598. }
  599. plainCommentElements.forEach(function (element) {
  600. plainCommentsContainer.append($(element.element))
  601. })
  602. container.append(plainCommentsContainer)
  603. // Scroll to conserved row if exists
  604. if (conservedRow) {
  605. $('html, body').animate(
  606. {
  607. scrollTop: $(conservedRow).offset().top,
  608. },
  609. 2000,
  610. )
  611. }
  612. $('#sortMethodSelect').val(sortModeData)
  613. if (featuredCommentsCount < 10 && userSettings.hidePlainComments === true) {
  614. $('#toggleFilteredBtn').click()
  615. }
  616. initSettings(userSettings)
  617. // control center
  618. $(document).on('settingsSaved', (event, data) => {
  619. location.reload()
  620. })
  621. })()