PTT web enhanced

Enhance user experience of PTT web

目前為 2021-10-26 提交的版本,檢視 最新版本

// ==UserScript==
// @name         PTT web enhanced
// @namespace    2CF9973A-28C9-11EC-9EA6-98F49F6E8EAB
// @version      2.7
// @description  Enhance user experience of PTT web
// @author       Rick0
// @match        https://www.ptt.cc/*
// @grant        GM.xmlHttpRequest
// @connect      imgur.com
// @run-at       document-start
// @compatible   firefox Tampermonkey, Violentmonkey
// @compatible   chrome Tampermonkey, Violentmonkey
// @license      Beerware
// ==/UserScript==

(function() {
  'use strict'

  // == independent methods ==

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

  function isImgurMp4Exist (imgurId) {
    return new Promise((resolve) => {
      GM.xmlHttpRequest({
        url: `https://i.imgur.com/${imgurId}.mp4`,
        method: 'HEAD',
        headers: {
          referer: 'https://imgur.com/',
        },
        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 agreeOver18 () {
    document.cookie = `over18=1;path=/;expires=${(new Date(2100, 0)).toUTCString()}`
    location.replace(`https://www.ptt.cc/${decodeURIComponent(location.search.match(/[?&]from=([^&]+)/)[1])}`) 
  }

  function addStyle (cssCode) {
    document.head.append(createElement(cssCode))
  }

  function setNoReferrer () {
    document.head.append(createElement('<meta name="referrer" content="no-referrer">'))
  }

  // == dependent methods ==

  function addSearch () {
    // 系列文
    let title = document.querySelectorAll('.article-metaline')[1]
      .querySelector('.article-meta-value')
      .textContent.match(/^(?:(?:Re|Fw): +)?(.+)$/)[1]
    let titleEl = createElement(`<a class="board ellipsis" href="javascript:void(0);">系列 ${title}</a>`)
    let titleUrl = `${location.pathname.match(/^(.+\/).+?$/)[1]}search?q=${encodeURIComponent(`thread:${title}`).replace(/%20/g, '+')}`
    titleEl.addEventListener('click', function (e) {
      location.href = titleUrl
    })

    // 同作者
    let author = document.querySelectorAll('.article-metaline')[0]
      .querySelector('.article-meta-value')
      .textContent.match(/^(.+) +\([^\)]*?\)$/)[1]
    let authorEl = createElement(`<a class="board" href="javascript:void(0);">作者 ${author}</a>`)
    let authorUrl = `${location.pathname.match(/^(.+\/).+?$/)[1]}search?q=${encodeURIComponent(`author:${author}`).replace(/%20/g, '+')}`
    authorEl.addEventListener('click', function (e) {
      location.href = authorUrl
    })

    // 插入到畫面中
    let navigation = document.querySelector('#navigation')
    navigation.firstElementChild.remove()
    navigation.insertAdjacentElement('afterbegin', titleEl)
    navigation.insertAdjacentElement('afterbegin', createElement('<div class="bar"></div>'))
    navigation.insertAdjacentElement('beforeend', authorEl)
  }

  function pttImageEnhanced () {
    function getPrevRichcontentEl (el) {
      while (el.parentElement.id !== 'main-content') {
        el = el.parentElement
      }
      return el
    }

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

    // == 建立 lazy observer ==
    let onEnterView = function (entries, observer) {
      for (let entry of entries) {
        if (entry.isIntersecting) {
          // 目標進入畫面
          let triggerRichcontent = entry.target
          let imgurId = triggerRichcontent.dataset.imgurId

          isImgurMp4Exist(imgurId)
            .then(hasVideo => {
              let attachment
              if (hasVideo) {
                attachment = createElement(`<video src="https://i.imgur.com/${imgurId}.mp4" autoplay loop muted style="max-width: 100%;max-height: 800px;"></video>`)
                attachment.addEventListener('loadedmetadata', function (e) {
                  triggerRichcontent.removeAttribute('style')
                })
              } else {
                attachment = createElement(`<img src="https://i.imgur.com/${imgurId}h.jpg" alt>`)
                attachment.addEventListener('load', function (e) {
                  triggerRichcontent.removeAttribute('style')
                })
              }
              triggerRichcontent.append(attachment)
            })
            .catch(err => err)
          observer.unobserve(triggerRichcontent)
        }
      }
    }
    let options = {
      rootMargin: '200%',
    }
    let lazyObserver = new IntersectionObserver(onEnterView, options)

    for (let link of document.querySelectorAll('.bbs-screen.bbs-content a[href*="imgur.com"]')) {
      let urlData = new URL(link)
      if (globalRegExpData.imgurIdExtRegExp.test(urlData.pathname)) {
        let imgurId = RegExp.$1
        // 建立 richcontent
        let prevRichcontentEl = getPrevRichcontentEl(link)
        let richcontent = createElement(`<div class="richcontent" style="min-height: 30vh;" data-imgur-id="${imgurId}"></div>`)
        lazyObserver.observe(richcontent)
        insertElementToNextLine(prevRichcontentEl, richcontent)
      }
    }
  }


  // == main ==

  var globalRegExpData = {
    imgurIdExtRegExp: /^\/(\w{7})\w?(?:\.(\w+))?$/,
  }

  
  if (/^(?:\/[^\/]+?)+\/(?:M|G)\.\d+\.A\.[0-9A-F]{3}\.html/.test(location.pathname)) {
    document.addEventListener('DOMContentLoaded', function () {
      addStyle(
        `<style>
          #navigation {
            display: flex;
          }
          #navigation > * {
            white-space: nowrap;
          }
          .ellipsis {
            text-overflow: ellipsis;
            overflow: hidden;
          }
        </style>`
      )
      setNoReferrer()
      pttImageEnhanced()
      addSearch()
      // document.querySelector('.article-metaline-right')?.remove?.()
    }, { once: true })
  // 看板主頁
  // } else if (/^\/bbs\/[^\/]+?\/index.html/.test(location.pathname)) {
  //   console.log('看板主頁')
  // // 搜尋結果
  // } else if (/^\/bbs\/[^\/]+?\/search/.test(location.pathname)) {
  //   console.log('搜尋頁面')
  // 已成年同意
  } else if (location.pathname === '/ask/over18') {
    agreeOver18()
  }
})()