PTT web enhanced

Enhance user experience of PTT web

目前为 2021-10-19 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name PTT web enhanced
  3. // @namespace 2CF9973A-28C9-11EC-9EA6-98F49F6E8EAB
  4. // @version 2.5
  5. // @description Enhance user experience of PTT web
  6. // @author Rick0
  7. // @match https://www.ptt.cc/bbs/*/*.html*
  8. // @match https://www.ptt.cc/ask/over18?*
  9. // @exclude https://www.ptt.cc/bbs/*/index.html
  10. // @grant GM.xmlHttpRequest
  11. // @connect imgur.com
  12. // @run-at document-start
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict'
  17.  
  18. // == independent methods ==
  19.  
  20. function createElement(html) {
  21. let template = document.createElement('template')
  22. template.innerHTML = html
  23. return template.content.firstChild
  24. }
  25.  
  26. function isUrlExist (url, headers = {}) {
  27. return new Promise((resolve) => {
  28. GM.xmlHttpRequest({
  29. url,
  30. method: 'HEAD',
  31. headers,
  32. onload: function (res) {
  33. if ([200, 304].includes(res.status) && res.finalUrl !== 'https://i.imgur.com/removed.png') {
  34. resolve(true)
  35. } else {
  36. resolve(false)
  37. }
  38. },
  39. onerror: function (err) {
  40. resolve(false)
  41. },
  42. })
  43. })
  44. }
  45.  
  46. function insertElementToNextLine (positionElement, element) {
  47. let positionNextSibling = positionElement.nextSibling
  48. switch (positionNextSibling?.nodeType) {
  49. case Node.TEXT_NODE:
  50. positionNextSibling.parentNode.replaceChild(element, positionNextSibling)
  51. let textMatchList = positionNextSibling.data.match(/^([^\n]*)\n?(.*)$/s)
  52. if (textMatchList[1] !== undefined) element.insertAdjacentText('beforebegin', textMatchList[1])
  53. if (textMatchList[2] !== undefined) element.insertAdjacentText('afterend', textMatchList[2])
  54. break
  55. case Node.ELEMENT_NODE:
  56. case undefined:
  57. positionElement.insertAdjacentElement('afterend', element)
  58. break
  59. default:
  60. throw new Error('insertElementToNextLine receive invalid positionElement')
  61. }
  62. }
  63.  
  64. function getImgurInfo (originalUrl) {
  65. return new Promise((resolve, reject) => {
  66. let imgurInfo = {
  67. id: undefined,
  68. hasVideo: undefined,
  69. get imgurUrl () {
  70. return this.id !== undefined ? `https://i.imgur.com/${this.id}.jpg` : undefined
  71. },
  72. get embedUrl () {
  73. if (this.id !== undefined) {
  74. return this.hasVideo ? `https://i.imgur.com/${this.id}.mp4` : `https://i.imgur.com/${this.id}h.jpg`
  75. } else {
  76. return undefined
  77. }
  78. },
  79. }
  80. let infoHeaders = {
  81. referer: 'https://imgur.com/',
  82. }
  83. let link = new URL(originalUrl)
  84. // URL 的 pathname 最少會有 / ,所以利用正則來去頭尾 / 後切割,最後面的 / 的後面如果沒有值不會被列入
  85. let pathList = link.pathname !== '/' ? link.pathname.match(/^\/(.*?)\/?$/)[1].split('/') : []
  86. let imgurIdRegExp = /^\w{7}/
  87. // 取得 id
  88. switch (pathList.length) {
  89. // 按照 pathname 的層數來分類處理
  90. // 只有一層,只可能是 id / id.ext 的格式
  91. case 1: {
  92. let idMatchList = pathList[0].match(imgurIdRegExp)
  93. if (idMatchList !== null) {
  94. imgurInfo.id = idMatchList[0]
  95. } else {
  96. reject(imgurInfo)
  97. retrun
  98. }
  99. }
  100. break
  101.  
  102. default:
  103. reject(imgurInfo)
  104. retrun
  105. }
  106.  
  107. isUrlExist(`https://i.imgur.com/${imgurInfo.id}.mp4`, infoHeaders)
  108. // 確認是否有影片格式的存在
  109. .then(hasVideo => {
  110. imgurInfo.hasVideo = hasVideo
  111. resolve(imgurInfo)
  112. })
  113. .catch(err => {
  114. reject(imgurInfo)
  115. })
  116. })
  117. }
  118.  
  119. function agreeOver18 () {
  120. document.cookie = `over18=1;path=/;expires=${(new Date(2100, 0)).toUTCString()}`
  121. location.replace(`https://www.ptt.cc/${decodeURIComponent(location.search.match(/[?&]from=([^&]+)/)[1])}`)
  122. }
  123.  
  124. // == dependent methods ==
  125.  
  126. function pttImageEnhanced () {
  127. function embedImg (href, prevRichcontentElement) {
  128. getImgurInfo(href)
  129. .then(imgurInfo => {
  130. let richcontent = createElement('<div class="richcontent"></div>')
  131. if (imgurInfo.hasVideo) {
  132. richcontent.innerHTML = `<video data-src="${imgurInfo.embedUrl}" autoplay loop muted style="max-width: 100%;max-height: 800px;"></video>`
  133. videoLazyObserver.observe(richcontent.querySelector(':scope > video'))
  134. } else {
  135. richcontent.innerHTML = `<img src="${imgurInfo.embedUrl}" alt loading="lazy">`
  136. }
  137. insertElementToNextLine(prevRichcontentElement, richcontent)
  138. })
  139. .catch(err => err)
  140. }
  141.  
  142. // == 取消所有 ptt web 原生的 imgur 圖片載入 ==
  143. for (let img of document.querySelectorAll('.richcontent > img[src*="imgur.com"]')) {
  144. img.src = ''
  145. img.parentElement.remove()
  146. }
  147.  
  148. // == 取消外連資源的 referrer ==
  149. document.head.appendChild(createElement('<meta name="referrer" content="no-referrer">'))
  150.  
  151. // == 建立 video lazy observer ==
  152. let onEnterView = function (entries, observer) {
  153. for (let entry of entries) {
  154. if (entry.isIntersecting) {
  155. // 目標進入畫面
  156. let video = entry.target
  157. video.src = video.dataset.src
  158. video.removeAttribute('data-src')
  159. observer.unobserve(video)
  160. }
  161. }
  162. }
  163. let options = {
  164. rootMargin: '50%',
  165. }
  166. let videoLazyObserver = new IntersectionObserver(onEnterView, options)
  167.  
  168. // == 處理內文的部分 ==
  169. for (let a of document.querySelectorAll('.bbs-screen.bbs-content > a[href*="imgur.com"]')) {
  170. embedImg(a.href, a)
  171. }
  172.  
  173. // == 處理推/噓文的部分 ==
  174. for (let a of document.querySelectorAll('.f3.push-content > a[href*="imgur.com"]')) {
  175. embedImg(a.href, a.closest('.push'))
  176. }
  177. }
  178.  
  179. function searchSameArticle () {
  180. let titleElement = document.querySelectorAll('.article-metaline')[1].querySelector('.article-meta-value')
  181. titleElement.className = 'article-meta-tag'
  182. let title = titleElement.textContent.match(/^(?:Re: +)?(.+)$/)[1]
  183. let url = `${location.pathname.match(/^(.+\/).+?$/)[1]}search?q=${encodeURIComponent(title)}`
  184. titleElement.outerHTML = `<a href="${url}">${titleElement.outerHTML}</a>`
  185. }
  186.  
  187. // == main ==
  188.  
  189. if (location.pathname === '/ask/over18') {
  190. agreeOver18()
  191. } else {
  192. document.addEventListener('DOMContentLoaded', function () {
  193. pttImageEnhanced()
  194. searchSameArticle()
  195. }, { once: true })
  196. }
  197. })()