Enhance user experience of PTT web
当前为
// ==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 })
}
})()