PTT web image enhanced

Enhance PTT web image load performance

当前为 2021-10-17 提交的版本,查看 最新版本

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