PTT web enhanced

Enhance user experience of PTT web

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

// ==UserScript==
// @name         PTT web enhanced
// @namespace    2CF9973A-28C9-11EC-9EA6-98F49F6E8EAB
// @version      2.5
// @description  Enhance user experience of PTT web
// @author       Rick0
// @match        https://www.ptt.cc/bbs/*/*.html*
// @match        https://www.ptt.cc/ask/over18?*
// @exclude      https://www.ptt.cc/bbs/*/index.html
// @grant        GM.xmlHttpRequest
// @connect      imgur.com
// @run-at       document-start
// ==/UserScript==

(function() {
  'use strict'

  // == independent methods ==

  function createElement(html) {
    let template = document.createElement('template')
    template.innerHTML = html
    
    return template.content.firstChild
  }

  function isUrlExist (url, headers = {}) {
    return new Promise((resolve) => {
      GM.xmlHttpRequest({
        url,
        method: 'HEAD',
        headers,
        onload: function (res) {
          if ([200, 304].includes(res.status) && res.finalUrl !== 'https://i.imgur.com/removed.png') {
            resolve(true)
          } else {
            resolve(false)
          }
        },
        onerror: function (err) {
          resolve(false)
        },
      })
    })
  }

  function insertElementToNextLine (positionElement, element) {
    let positionNextSibling = positionElement.nextSibling
    switch (positionNextSibling?.nodeType) {
      case Node.TEXT_NODE:
        positionNextSibling.parentNode.replaceChild(element, positionNextSibling)
        let textMatchList = positionNextSibling.data.match(/^([^\n]*)\n?(.*)$/s)
        if (textMatchList[1] !== undefined) element.insertAdjacentText('beforebegin', textMatchList[1])
        if (textMatchList[2] !== undefined) element.insertAdjacentText('afterend', textMatchList[2])
        break
        
      case Node.ELEMENT_NODE:
      case undefined:
        positionElement.insertAdjacentElement('afterend', element)
        break
      
      default:
        throw new Error('insertElementToNextLine receive invalid positionElement')
    }
  }

  function getImgurInfo (originalUrl) {
    return new Promise((resolve, reject) => {
      let imgurInfo = {
        id: undefined,
        hasVideo: undefined,
        get imgurUrl () {
          return this.id !== undefined ? `https://i.imgur.com/${this.id}.jpg` : undefined
        },
        get embedUrl () {
          if (this.id !== undefined) {
            return this.hasVideo ? `https://i.imgur.com/${this.id}.mp4` : `https://i.imgur.com/${this.id}h.jpg`
          } else {
            return undefined
          }
        },
      }
      let infoHeaders = {
        referer: 'https://imgur.com/',
      }
  
      let link = new URL(originalUrl)
      // URL 的 pathname 最少會有 / ,所以利用正則來去頭尾 / 後切割,最後面的 / 的後面如果沒有值不會被列入
      let pathList = link.pathname !== '/' ? link.pathname.match(/^\/(.*?)\/?$/)[1].split('/') : []
      let imgurIdRegExp = /^\w{7}/
  
      // 取得 id
      switch (pathList.length) {
        // 按照 pathname 的層數來分類處理
        // 只有一層,只可能是 id / id.ext 的格式
        case 1: {
          let idMatchList = pathList[0].match(imgurIdRegExp)
          if (idMatchList !== null) {
            imgurInfo.id = idMatchList[0]
          } else {
            reject(imgurInfo)
            retrun
          }
        }
        break

        default:
          reject(imgurInfo)
          retrun
      }

      isUrlExist(`https://i.imgur.com/${imgurInfo.id}.mp4`, infoHeaders)
        // 確認是否有影片格式的存在
        .then(hasVideo => {
          imgurInfo.hasVideo = hasVideo
          resolve(imgurInfo)
        })
        .catch(err => {
          reject(imgurInfo)
        })
    })
  }

  function agreeOver18 () {
    document.cookie = `over18=1;path=/;expires=${(new Date(2100, 0)).toUTCString()}`
    location.replace(`https://www.ptt.cc/${decodeURIComponent(location.search.match(/[?&]from=([^&]+)/)[1])}`) 
  }

  // == dependent methods ==

  function pttImageEnhanced () {
    function embedImg (href, prevRichcontentElement) {
      getImgurInfo(href)
        .then(imgurInfo => {
          let richcontent = createElement('<div class="richcontent"></div>')
          if (imgurInfo.hasVideo) {
            richcontent.innerHTML = `<video data-src="${imgurInfo.embedUrl}" autoplay loop muted style="max-width: 100%;max-height: 800px;"></video>`
            videoLazyObserver.observe(richcontent.querySelector(':scope > video'))
          } else {
            richcontent.innerHTML = `<img src="${imgurInfo.embedUrl}" alt loading="lazy">`
          }
          insertElementToNextLine(prevRichcontentElement, richcontent)
        })
        .catch(err => err)
    }

    // == 取消所有 ptt web 原生的 imgur 圖片載入 ==
    for (let img of document.querySelectorAll('.richcontent > img[src*="imgur.com"]')) {
      img.src = ''
      img.parentElement.remove()
    }

    // == 取消外連資源的 referrer ==
    document.head.appendChild(createElement('<meta name="referrer" content="no-referrer">'))

    // == 建立 video lazy observer ==
    let onEnterView = function (entries, observer) {
      for (let entry of entries) {
        if (entry.isIntersecting) {
          // 目標進入畫面
          let video = entry.target
          video.src = video.dataset.src
          video.removeAttribute('data-src')
          observer.unobserve(video)
        }
      }
    }
    let options = {
      rootMargin: '50%',
    }
    let videoLazyObserver = new IntersectionObserver(onEnterView, options)

    // == 處理內文的部分 ==
    for (let a of document.querySelectorAll('.bbs-screen.bbs-content > a[href*="imgur.com"]')) {
      embedImg(a.href, a)
    }

    // == 處理推/噓文的部分 ==
    for (let a of document.querySelectorAll('.f3.push-content > a[href*="imgur.com"]')) {
      embedImg(a.href, a.closest('.push'))
    }
  }

  function searchSameArticle () {
    let titleElement = document.querySelectorAll('.article-metaline')[1].querySelector('.article-meta-value')
    titleElement.className = 'article-meta-tag'
    let title = titleElement.textContent.match(/^(?:Re: +)?(.+)$/)[1]
    let url = `${location.pathname.match(/^(.+\/).+?$/)[1]}search?q=${encodeURIComponent(title)}`
    titleElement.outerHTML = `<a href="${url}">${titleElement.outerHTML}</a>`
  }

  // == main ==

  if (location.pathname === '/ask/over18') {
    agreeOver18()
  } else {
    document.addEventListener('DOMContentLoaded', function () {
      pttImageEnhanced()
      searchSameArticle()
    }, { once: true })
  }
})()

QingJ © 2025

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