場外大樓過濾器

已經厭倦看到一堆不感興趣的大樓? 這就是你需要的

  1. // ==UserScript==
  2. // @name 場外大樓過濾器
  3. // @description 已經厭倦看到一堆不感興趣的大樓? 這就是你需要的
  4. // @namespace nathan60107
  5. // @author nathan60107(貝果)
  6. // @version 1.1.3
  7. // @homepage https://home.gamer.com.tw/creationCategory.php?owner=nathan60107&c=425332
  8. // @match https://forum.gamer.com.tw/B.php?*bsn=60076*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=gamer.com.tw
  10. // @require https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js
  11. // @require https://code.jquery.com/jquery-3.6.3.min.js
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // @run-at document-start
  15. // @noframes
  16. // ==/UserScript==
  17.  
  18. let $ = jQuery
  19. let dd = (...d) => {
  20. if (window.BAHAID && window.BAHAID !== 'nathan60107') return
  21. d.forEach((it) => { console.log(it) })
  22. }
  23.  
  24. /**
  25. * @param {boolean} bool
  26. */
  27. function isCheck(bool) {
  28. return bool ? 'checked' : ''
  29. }
  30. /**
  31. * @param {string} condi
  32. * @param {string} curr
  33. */
  34. function isSelected(condi, curr) {
  35. return condi === curr ? 'selected' : ''
  36. }
  37. /**
  38. * @param {string|undefined} a
  39. * @param {string} b
  40. */
  41. function versionCompare(a, b) {
  42. if (!a) return -1
  43. return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' })
  44. }
  45. /**
  46. * @param {number} reply
  47. * @param {number} like
  48. */
  49. function userCondition(reply, like) {
  50. if (!setting.filterReply && !setting.filterLike) {
  51. return false
  52. } else if (!setting.filterLike) {
  53. return reply > setting.replyLimit
  54. } else if (!setting.filterReply) {
  55. return like > setting.likeLimit
  56. } else {
  57. return [
  58. reply > setting.replyLimit,
  59. like > setting.likeLimit,
  60. ].reduce(
  61. setting.likeCondition === 'or'
  62. ? (a, b) => a || b : (a, b) => a && b
  63. )
  64. }
  65. }
  66. /** @param {{title: string, snA: string}[]} whitelist */
  67. function whitelistToString(whitelist) {
  68. return whitelist.map(wl =>
  69. `<li>
  70. -
  71. <a href="https://forum.gamer.com.tw/C.php?bsn=60076&snA=${wl.snA}">${wl.title}</a>
  72. <button data-snA="${wl.snA}">刪除</button>
  73. </li>`
  74. ).join('')
  75. }
  76.  
  77. /**
  78. * @type {defaultSetting}
  79. */
  80. let setting = {}
  81. const defaultSetting = {
  82. dataVersion: GM_info.script.version,
  83. enableLoadMore: true,
  84. enableFilter: true,
  85. filterReply: true,
  86. replyLimit: 1000,
  87. filterLike: false,
  88. likeCondition: 'and',
  89. likeLimit: 1000,
  90. /** @type {{title: string, snA: string}[]} */
  91. whitelist: []
  92. }
  93. const settingKey = Object.keys(defaultSetting)
  94. let postSet = new Set()
  95. let removeCount = 0
  96. let resetCoundown = -1
  97. let url = new URLSearchParams(window.location.href)
  98. let pageIndex = +(url.get('page') ?? 1)
  99.  
  100. function initSettingPanel() {
  101. for (const key of settingKey) {
  102. setting[key] = GM_getValue(key)
  103. if (setting[key] === undefined) {
  104. setting[key] = defaultSetting[key]
  105. GM_setValue(key, defaultSetting[key])
  106. }
  107. }
  108. if (versionCompare(setting.dataVersion, '1.1.0') < 0) {
  109. resetSetting(false)
  110. toastr.info('由於版本更新,設定已重置,若持續出現此通知請回報錯誤。')
  111. }
  112.  
  113. if (setting.enableLoadMore) $('.b-popular, .b-pager').hide()
  114. $('.BH-qabox1').after(`
  115. <style>
  116. #no-building, #no-building .setting-building, #no-building ul {
  117. display: flex;
  118. flex-direction: column;
  119. gap: 10px;
  120. }
  121. #no-building {
  122. padding: 18px 12px;
  123. }
  124. #no-building .setting-building {
  125. padding-left: 10px;
  126. }
  127. #no-building label {
  128. display: block;
  129. }
  130. #no-building input[type="number"] {
  131. width: 4em;
  132. }
  133. #no-building button, #no-building select {
  134. width: max-content;
  135. }
  136. #no-building .setting-building select {
  137. margin-left: 45px;
  138. }
  139. #no-building input::-webkit-outer-spin-button,
  140. #no-building input::-webkit-inner-spin-button {
  141. -webkit-appearance: none;
  142. margin: 0;
  143. }
  144. #no-building input[type=number] {
  145. -moz-appearance: textfield;
  146. }
  147. </style>
  148. <h5>大樓過濾設定</h5>
  149. <div id="no-building" class="BH-rbox"></div>
  150. `)
  151. updateSettingPanel()
  152. }
  153.  
  154. function updateSettingPanel() {
  155. $('#no-building').html(`
  156. <div class="version">腳本版本:${GM_info.script.version}</div>
  157. <div class="page">目前頁數:${pageIndex}</div>
  158. <div class="info">已過濾大樓: ${removeCount} 棟</div>
  159. <label>
  160. <input ${isCheck(setting.enableLoadMore)} data-key="enableLoadMore" type="checkbox" >
  161. 啟用載入更多內容
  162. </label>
  163. <label>
  164. <input ${isCheck(setting.enableFilter)} data-key="enableFilter" type="checkbox" >
  165. 啟用大樓過濾
  166. </label>
  167. 大樓過濾條件:
  168. <div class="setting-building">
  169. <label>
  170. <input ${isCheck(setting.filterReply)} data-key="filterReply" type="checkbox">
  171. 回覆超過<input value="${setting.replyLimit}" data-key="replyLimit" type="number">樓
  172. </label>
  173. <select data-key="likeCondition">
  174. <option value="and" ${isSelected(setting.likeCondition, 'and')}>且</option>
  175. <option value="or" ${isSelected(setting.likeCondition, 'or')}>或</option>
  176. </select>
  177. <label>
  178. <input ${isCheck(setting.filterLike)} data-key="filterLike" type="checkbox">
  179. 推文超過<input value="${setting.likeLimit}" data-key="likeLimit" type="number">次
  180. </label>
  181. 白名單文章:
  182. <ul>
  183. ${whitelistToString(setting.whitelist)}
  184. </ul>
  185. <input class="setting-whitelist-input" placeholder="在此處貼上文章網址" type="text">
  186. <button class="setting-add-whitelist" type="button">新增白名單</button>
  187. </div>
  188. <button class="setting-reset" type="button">${resetCoundown === -1 ? '重置設定' : `確定? ${resetCoundown}`}</button>
  189. `)
  190. $('#no-building .setting-reset').on('click', confirmReset)
  191. $('#no-building .setting-add-whitelist').on('click', () => modifyWhitelist('add'))
  192. for (const { snA } of setting.whitelist) {
  193. $(`[data-snA="${snA}"]`).on('click', () => modifyWhitelist(snA))
  194. }
  195. for (const [key, val] of Object.entries(defaultSetting)) {
  196. if (Array.isArray(val)) continue
  197.  
  198. let targetKey = typeof val === 'boolean' ? 'checked' : 'value'
  199. $(`#no-building [data-key="${key}"]`).on('change', function (e) {
  200. setting[key] = e.target[targetKey]
  201. if (typeof val === 'number') setting[key] = parseInt(setting[key])
  202. saveSetting()
  203. })
  204. }
  205. }
  206.  
  207. function saveSetting() {
  208. for (const key of settingKey) {
  209. GM_setValue(key, setting[key])
  210. }
  211. toastr.info('大樓過濾設定已更新')
  212. }
  213. function confirmReset() {
  214. // First click of reset setting, countdown
  215. if (resetCoundown === -1) {
  216. resetCoundown = 10
  217. updateSettingPanel()
  218. let itv = setInterval(() => {
  219. resetCoundown--
  220. updateSettingPanel()
  221. if (resetCoundown === -1) clearInterval(itv)
  222. }, 1000)
  223. // Second click, reset setting and reload
  224. } else {
  225. resetSetting()
  226. }
  227. }
  228. function resetSetting(reload = true) {
  229. for (const key of settingKey) {
  230. GM_setValue(key, defaultSetting[key])
  231. }
  232. if (reload) location.reload()
  233. }
  234. /**
  235. * If action is add, add url in input to whitelist.
  236. * Or, delete whiltelist item that snA == action
  237. * @param {'add'|string} action
  238. */
  239. function modifyWhitelist(action) {
  240. if (action === 'add') {
  241. let tempList = _.cloneDeep(setting.whitelist)
  242. let input = $('.setting-whitelist-input').val().match(/snA=(\d+)/)
  243. if (!input) return
  244.  
  245. let snA = input[1]
  246. $.ajax({
  247. url: `https://forum.gamer.com.tw/C.php?bsn=60076&snA=${snA}`
  248. }).done((resHTML) => {
  249. let title = $(resHTML).filter('title').text()
  250. .replace(' @場外休憩區 哈啦板 - 巴哈姆特', '')
  251. .replace(/^【.*】/, '')
  252. tempList.push({ title, snA })
  253. if (_.isEqual(setting.whitelist, tempList)) {
  254. toastr.warning('白名單已存在')
  255. } else {
  256. setting.whitelist = _.uniqBy(tempList, 'snA')
  257. updateSettingPanel()
  258. saveSetting()
  259. }
  260. })
  261. } else {
  262. _.pullAllBy(setting.whitelist, [{ snA: action }], 'snA')
  263. updateSettingPanel()
  264. saveSetting()
  265. }
  266. }
  267.  
  268. /**
  269. * @param {HTMLElement} html
  270. * @param {'append'|'remove'} mode
  271. */
  272. function processHtml(html, mode) {
  273. let snA = $(html).find('.b-list__time__edittime > a').attr('href').match(/snA=([\d]+)/)[1]
  274. let title = $(html).find('.b-list__main__title').text()
  275. let top = $(html).hasClass('b-list__row--sticky')
  276. let like = $(html).find('.b-list__summary__gp').text()
  277. let replyRaw = $(html).find('.b-list__time__edittime > a').attr('href').match(/tnum=([0-9]+)&/)
  278. let reply = replyRaw ? parseInt(replyRaw[1]) - 1 : 0
  279. let inWhitelist = _.filter(setting.whitelist, ['snA', snA]).length !== 0
  280. // Duplicated, skip it
  281. if (postSet.has(snA)) return
  282. // Should be filtered
  283. if (setting.enableFilter && !top && !inWhitelist &&
  284. userCondition(reply, like)) {
  285. if (mode === 'remove') $(html).remove()
  286. removeCount++
  287. // Should be kept
  288. } else {
  289. if (mode === 'append') {
  290. aTag = html.querySelector('.b-list__main a')
  291. aTag.href = aTag.href.replace(/&tnum=\d+&bPage=\d+/, '')
  292. $('form[method="post"] > .b-list-wrap > .b-list > tbody').append(html)
  293. }
  294. }
  295. postSet.add(snA)
  296. }
  297.  
  298. function htmlToPost() {
  299. $('.b-list__row:not(:has(.b-list_ad))').map(
  300. function f() { processHtml(this, 'remove') }
  301. )
  302. updateSettingPanel()
  303. }
  304.  
  305. /**
  306. * @type {IntersectionObserverCallback}
  307. */
  308. function loadMore(entries) {
  309. if (!setting.enableLoadMore || entries.every((val) => !val.isIntersecting)) return
  310.  
  311. pageIndex++
  312. url.set('page', pageIndex)
  313. $.ajax({
  314. url: decodeURIComponent(url.toString())
  315. }).done((resHTML) => {
  316. $(resHTML).find('.b-list__row:not(:has(.b-list_ad))').map(
  317. function f() { processHtml(this, 'append') }
  318. )
  319. updateSettingPanel()
  320. // Register lazyload img and draw non-image thumbnail
  321. Forum.B.lazyThumbnail()
  322. Forum.Common.drawNoImageCanvas()
  323. })
  324. }
  325.  
  326. function initIntersectionObserver() {
  327. let observer = new IntersectionObserver(loadMore)
  328. observer.observe($('#BH-footer')[0])
  329. }
  330.  
  331. (function () {
  332. document.addEventListener("DOMContentLoaded", function () {
  333. initIntersectionObserver()
  334. initSettingPanel()
  335. htmlToPost()
  336. })
  337. })();
  338.  
  339. /**
  340. * Reference:
  341. * [JSDoc](https://ricostacruz.com/til/typescript-jsdoc)
  342. */

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址