2ch Thread List

2ちゃんねるの各板のトップページに整形したスレッド一覧を表示

  1. // ==UserScript==
  2. // @name 2ch Thread List
  3. // @namespace https://gf.qytechs.cn/users/1009-kengo321
  4. // @version 10
  5. // @description 2ちゃんねるの各板のトップページに整形したスレッド一覧を表示
  6. // @grant none
  7. // @match *://*.2ch.net/*
  8. // @match *://*.5ch.net/*
  9. // @license MIT
  10. // @noframes
  11. // @run-at document-start
  12. // ==/UserScript==
  13.  
  14. ;(function() {
  15. 'use strict'
  16.  
  17. var byId = function(id) {
  18. return document.getElementById(id)
  19. }
  20.  
  21. var getBoardId = function() {
  22. var p = window.location.pathname
  23. return p.slice(1, p.indexOf('/', 1))
  24. }
  25.  
  26. var parseSubjectText = (function() {
  27. var lineRegExp = /^(\d+)\.dat<>(.*)\s*\((\d+)\)$/gm
  28. var matchedResults = function(str) {
  29. var result = [], matched = null
  30. while ((matched = lineRegExp.exec(str))) result.push(matched)
  31. return result
  32. }
  33. var removeCopyright = function(str) {
  34. return str.replace('[転載禁止]', '')
  35. .replace('[無断転載禁止]', '')
  36. .replace('&copy;2ch.net', '')
  37. .replace('&#169;2ch.net', '')
  38. .replace('&copy;bbspink.com', '')
  39. .replace('&#169;bbspink.com', '')
  40. }
  41. var newThreadInfo = function(matchedResult, i) {
  42. return {
  43. line: i + 1,
  44. id: parseInt(matchedResult[1], 10),
  45. title: removeCopyright(matchedResult[2].trim()),
  46. resNum: parseInt(matchedResult[3], 10),
  47. }
  48. }
  49. return function(subjectText) {
  50. return matchedResults(subjectText).map(newThreadInfo)
  51. }
  52. })()
  53.  
  54. const millis = {
  55. toSeconds(millis) {
  56. return Math.trunc(millis / 1000)
  57. },
  58. }
  59.  
  60. const addForceProperty = (threadInfos, nowAsSeconds) => {
  61. const secondsInMinute = 60
  62. const secondsInDay = 86400
  63. const lowerLimit = elapsed => Math.max(elapsed, secondsInMinute * 3)
  64. return threadInfos.map(i => {
  65. const elapsed = nowAsSeconds - i.id
  66. const force = elapsed < 0
  67. ? 0
  68. : Math.trunc(i.resNum / (lowerLimit(elapsed) / secondsInDay))
  69. return Object.assign({}, i, {force})
  70. })
  71. }
  72.  
  73. const threadInfos = () => {
  74. return threadInfos.data.slice()
  75. }
  76. threadInfos.data = []
  77. threadInfos.set = infos => {
  78. threadInfos.data = infos.slice()
  79. }
  80.  
  81. var sortThreadInfos = (function() {
  82. var cmp = function(prop) {
  83. return function(a, b) {
  84. if (a[prop] < b[prop]) return -1
  85. if (a[prop] > b[prop]) return 1
  86. return 0
  87. }
  88. }
  89. var negate = function(func) {
  90. return function() { return -func.apply(null, arguments) }
  91. }
  92. var cmpSeq = function(comparators) {
  93. return function(a, b) {
  94. for (var i = 0; i < comparators.length; i++) {
  95. var r = comparators[i](a, b)
  96. if (r !== 0) return r
  97. }
  98. return 0
  99. }
  100. }
  101. var reversableCmpObj = function(prop) {
  102. return {
  103. asc: cmpSeq([cmp(prop), line.asc]),
  104. desc: cmpSeq([negate(cmp(prop)), line.asc]),
  105. }
  106. }
  107. var line = {asc: cmp('line'), desc: negate(cmp('line'))}
  108. var title = reversableCmpObj('title')
  109. var resNum = reversableCmpObj('resNum')
  110. var id = reversableCmpObj('id')
  111. var force = reversableCmpObj('force')
  112. var current = line.asc
  113. var setOrReverseCmp = function(comp) {
  114. return function() {
  115. current = (current === comp.asc ? comp.desc : comp.asc)
  116. }
  117. }
  118. var result = function(threadInfos) {
  119. return threadInfos.slice().sort(current)
  120. }
  121. result.byLineInAsc = function() { current = line.asc }
  122. result.byLine = setOrReverseCmp(line)
  123. result.byTitle = setOrReverseCmp(title)
  124. result.byResNum = setOrReverseCmp(resNum)
  125. result.byId = setOrReverseCmp(id)
  126. result.byForce = setOrReverseCmp(force)
  127. return result
  128. })()
  129.  
  130. var newThreadList = (function() {
  131. var addCells = function(row) {
  132. ;[].slice.call(arguments, 1).forEach(function(content) {
  133. var cell = row.insertCell()
  134. if (['string', 'number'].indexOf(typeof(content)) >= 0) {
  135. cell.textContent = content
  136. } else {
  137. cell.appendChild(content)
  138. }
  139. })
  140. }
  141. var sorter = function(setSortType) {
  142. return function() {
  143. setSortType()
  144. updateThreadList(threadInfos())
  145. }
  146. }
  147. var setTHead = function(tHead) {
  148. var r = tHead.insertRow()
  149. var addTh = function(e) {
  150. var th = r.ownerDocument.createElement('th')
  151. th.textContent = e[0]
  152. th.addEventListener('click', e[1])
  153. r.appendChild(th)
  154. }
  155. ;[['番号', sorter(sortThreadInfos.byLine)],
  156. ['タイトル', sorter(sortThreadInfos.byTitle)],
  157. ['レス', sorter(sortThreadInfos.byResNum)],
  158. ['勢い', sorter(sortThreadInfos.byForce)],
  159. ['作成日時', sorter(sortThreadInfos.byId)],
  160. ].forEach(addTh)
  161. }
  162. var threadUrl = function(threadId) {
  163. return '/test/read.cgi/'
  164. + getBoardId()
  165. + '/'
  166. + threadId
  167. + '/'
  168. }
  169. var decodeEntityRefs = (function() {
  170. var e = document.createElement('span')
  171. return function(html) {
  172. e.innerHTML = html
  173. return e.textContent
  174. }
  175. })()
  176. var newLink = function(threadInfo) {
  177. var result = document.createElement('a')
  178. result.target = '_blank'
  179. result.href = threadUrl(threadInfo.id)
  180. result.textContent = decodeEntityRefs(threadInfo.title)
  181. return result
  182. }
  183. var padZero = function(dateUnit) {
  184. return dateUnit <= 9 ? '0' + dateUnit : '' + dateUnit
  185. }
  186. var toZeroPaddingString = function(date) {
  187. var monthDay = [date.getMonth() + 1, date.getDate()]
  188. var times = [date.getHours(), date.getMinutes(), date.getSeconds()]
  189. return date.getFullYear()
  190. + '/'
  191. + monthDay.map(padZero).join('/')
  192. + ' '
  193. + times.map(padZero).join(':')
  194. }
  195. var setTBody = function(tBody, threadInfos) {
  196. ;(threadInfos || []).forEach(function(info) {
  197. addCells(tBody.insertRow()
  198. , info.line
  199. , newLink(info)
  200. , info.resNum
  201. , info.force
  202. , toZeroPaddingString(new Date(info.id * 1000)))
  203. })
  204. }
  205. return function(threadInfos) {
  206. var result = document.createElement('table')
  207. result.id = 'thread-list'
  208. setTHead(result.createTHead())
  209. setTBody(result.createTBody(), threadInfos)
  210. return result
  211. }
  212. })()
  213.  
  214. var addThreadListBoxIfAbsent = function() {
  215. if (!byId('thread-list-box')) {
  216. var b = document.body
  217. b.insertBefore(newThreadListBox(), b.firstChild)
  218. }
  219. }
  220.  
  221. var replaceThreadListBy = function(threadList) {
  222. var old = threadList.ownerDocument.getElementById('thread-list')
  223. old.parentNode.replaceChild(threadList, old)
  224. }
  225.  
  226. var newTopBar = function() {
  227. var message = document.createElement('span')
  228. message.id = 'thread-list-error-message'
  229. var button = document.createElement('input')
  230. button.id = 'thread-list-reload-button'
  231. button.type = 'button'
  232. button.value = '更新'
  233. button.addEventListener('click', function() {
  234. button.disabled = true
  235. message.textContent = ''
  236. requestSubjectText(getBoardId())
  237. })
  238. var result = document.createElement('div')
  239. result.id = 'thread-list-top-bar'
  240. result.appendChild(button)
  241. result.appendChild(message)
  242. return result
  243. }
  244.  
  245. var newThreadListBox = function() {
  246. var result = document.createElement('div')
  247. result.id = 'thread-list-box'
  248. result.appendChild(newTopBar())
  249. result.appendChild(newThreadList())
  250. return result
  251. }
  252.  
  253. const updateThreadList = threadInfos => {
  254. const sorted = sortThreadInfos(threadInfos)
  255. const list = newThreadList(sorted)
  256. replaceThreadListBy(list)
  257. }
  258.  
  259. var subjectTextLoaded = function(event) {
  260. var xhr = event.target
  261. if (xhr.status === 200) {
  262. const parsed = parseSubjectText(xhr.responseText)
  263. const added = addForceProperty(parsed, millis.toSeconds(Date.now()))
  264. threadInfos.set(added)
  265. updateThreadList(added)
  266. } else {
  267. byId('thread-list-error-message').textContent = xhr.statusText
  268. }
  269. }
  270.  
  271. var requestSubjectText = (function() {
  272. var handler = function(errorMessage, fn) {
  273. return function f(event) {
  274. if (document.body) {
  275. addStyleIfAbsent()
  276. addThreadListBoxIfAbsent()
  277. byId('thread-list-reload-button').disabled = false
  278. byId('thread-list-error-message').textContent = errorMessage
  279. if (fn) fn(event)
  280. } else {
  281. document.addEventListener('DOMContentLoaded', f.bind(this, event))
  282. }
  283. }
  284. }
  285. function getSubjectTxtURL(boardId) {
  286. const l = window.location
  287. return `${l.protocol}//${l.host}/${boardId}/subject.txt`
  288. }
  289. return function(boardId) {
  290. var xhr = new XMLHttpRequest()
  291. xhr.timeout = 30000
  292. xhr.open('GET', getSubjectTxtURL(boardId))
  293. xhr.overrideMimeType('text/plain; charset=shift_jis')
  294. xhr.addEventListener('load', handler('', subjectTextLoaded))
  295. xhr.addEventListener('timeout', handler('タイムアウト'))
  296. xhr.addEventListener('error', handler('エラー'))
  297. xhr.send()
  298. }
  299. })()
  300.  
  301. var addStyleIfAbsent = function() {
  302. if (byId('thread-list-style')) return
  303. var style = document.createElement('style')
  304. style.id = 'thread-list-style'
  305. style.innerHTML = [
  306. '#thread-list {',
  307. ' margin: 0 auto;',
  308. ' border-collapse: collapse;',
  309. '}',
  310. '#thread-list th {',
  311. ' color: white;',
  312. ' background-color: steelblue;',
  313. ' cursor: default;',
  314. '}',
  315. '#thread-list th:hover {',
  316. ' background-color: cornflowerblue;',
  317. '}',
  318. '#thread-list th:active {',
  319. ' background-color: mediumblue;',
  320. '}',
  321. '#thread-list td {',
  322. ' color: black;',
  323. '}',
  324. '#thread-list th, #thread-list td {',
  325. ' border: solid thin lightsteelblue;',
  326. ' padding: 0 0.5em;',
  327. ' line-height: 1.5em;',
  328. '}',
  329. '#thread-list tbody tr:nth-child(odd) {',
  330. ' background-color: azure;',
  331. '}',
  332. '#thread-list tbody tr:nth-child(even) {',
  333. ' background-color: aliceblue;',
  334. '}',
  335. '#thread-list tbody tr:nth-child(5n) {',
  336. ' border-bottom: solid medium lightsteelblue;',
  337. '}',
  338. '#thread-list td:nth-child(4n+1),',
  339. '#thread-list td:nth-child(4n+3),',
  340. '#thread-list td:nth-child(4n+4) {',
  341. ' text-align: right;',
  342. '}',
  343. '#thread-list a:link {',
  344. ' color: black;',
  345. ' text-decoration: none;',
  346. '}',
  347. '#thread-list a:visited {',
  348. ' color: purple;',
  349. '}',
  350. '#thread-list a:hover {',
  351. ' color: maroon;',
  352. ' text-decoration: underline;',
  353. '}',
  354. '#thread-list-box {',
  355. ' background-color: silver;',
  356. '}',
  357. '#thread-list-top-bar {',
  358. ' text-align: center;',
  359. '}',
  360. '#thread-list-error-message {',
  361. ' color: red;',
  362. '}',
  363. ].join('\n')
  364. document.head.appendChild(style)
  365. }
  366.  
  367. var isBoardTopPage = function() {
  368. return /^\/[^/]+\/(?:index\.html)?$/.test(window.location.pathname)
  369. }
  370.  
  371. var main = function() {
  372. if (!isBoardTopPage()) return
  373. requestSubjectText(getBoardId())
  374. }
  375.  
  376. main()
  377. })()

QingJ © 2025

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