- // ==UserScript==
- // @name PTT web enhanced
- // @namespace 2CF9973A-28C9-11EC-9EA6-98F49F6E8EAB
- // @version 2.8.1
- // @description Enhance user experience of PTT web
- // @author Rick0
- // @match https://www.ptt.cc/*
- // @grant GM.xmlHttpRequest
- // @connect imgur.com
- // @connect ptt.cc
- // @run-at document-start
- // @compatible firefox Tampermonkey, Violentmonkey, Greasemonkey 4.11+
- // @compatible chrome Tampermonkey, Violentmonkey
- // @license MIT
- // ==/UserScript==
-
- (function() {
- 'use strict'
-
- // == basic methods ==
-
- function createElement(html) {
- let template = document.createElement('template')
- template.innerHTML = html
-
- return template.content.firstChild
- }
-
- 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 addStyle (cssCode) {
- document.head.append(createElement(`<style>${cssCode}</style>`))
- }
-
- function getImgurInfo (imgurUrl) {
- return new Promise((resolve, reject) => {
- let urlData = new URL(imgurUrl)
-
- if (regExpData.imgur.idExt.test(urlData.pathname)) {
- let imageId = RegExp.$1
- fetch(`https://api.imgur.com/3/image/${imageId}`, {
- method: 'GET',
- referrerPolicy: 'no-referrer',
- headers: {
- Authorization: 'Client-ID b654e1b04c90bc8'
- },
- })
- .then(res => res.json())
- .then(json => resolve(json.data))
- .catch(err => reject(err))
- } else if (regExpData.imgur.album.test(urlData.pathname)) {
- let albumId = RegExp.$1
- fetch(`https://api.imgur.com/3/album/${albumId}/images`, {
- method: 'GET',
- referrerPolicy: 'no-referrer',
- headers: {
- Authorization: 'Client-ID b654e1b04c90bc8'
- },
- })
- .then(res => res.json())
- .then(json => resolve(json.data[0]))
- .catch(err => reject(err))
- } else if (regExpData.imgur.gallery.test(urlData.pathname)) {
- let galleryId = RegExp.$1
- fetch(`https://api.imgur.com/3/gallery/${galleryId}/images`, {
- method: 'GET',
- referrerPolicy: 'no-referrer',
- headers: {
- Authorization: 'Client-ID b654e1b04c90bc8'
- },
- })
- .then(res => res.json())
- .then(json => resolve(Array.isArray(json.data) ? json.data[0] : json.data))
- .catch(err => reject(err))
- } else {
- reject(new Error(`不支援的格式: ${imgurUrl}`))
- }
- })
- }
-
- // == dependent methods ==
-
- 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 addHeadlines () {
- let boardToolsEl = document.querySelector('.btn-group.btn-group-dir')
- let headlinesUrl = `/bbs/${boardData.name}/search?q=recommend%3A100`
- let headlinesEl = createElement(`<a class="btn" href="${headlinesUrl}">爆文</a>`)
- // 如果在爆文搜尋頁面,按鈕加上樣式
- if (/[\?&]q=recommend%3A100/.test(location.search)) headlinesEl.classList.add('selected')
- boardToolsEl.append(headlinesEl)
- }
-
- function addSearch () {
- // 設定 css
- addStyle(
- `#navigation {
- display: flex;
- }
- #navigation > * {
- white-space: nowrap;
- }
- .ellipsis {
- text-overflow: ellipsis;
- overflow: hidden;
- }`
- )
-
- // 系列文
- let title = document.querySelectorAll('.article-metaline')[1]
- .querySelector('.article-meta-value')
- .textContent.match(/^(?:(?:Re|Fw): +)?(.+)$/)[1]
- let titleEl = createElement(`<a class="board ellipsis" style="cursor: pointer;">系列 ${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(/^[^ ]+/)[0]
- let authorEl = createElement(`<a class="board" style="cursor: pointer;">作者 ${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 imgurUrl = triggerRichcontent.dataset.imgurUrl
-
- getImgurInfo(imgurUrl)
- .then(imgurInfo => {
- let attachment
- if (imgurInfo.animated) {
- attachment = createElement(`<video src="https://i.imgur.com/${imgurInfo.id}.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/${imgurInfo.id}h.jpg" alt>`)
- attachment.addEventListener('load', function (e) {
- triggerRichcontent.removeAttribute('style')
- })
- }
- triggerRichcontent.append(attachment)
- })
- .catch(err => {
- triggerRichcontent.remove()
- })
- 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"]')) {
- // 建立 richcontent
- let prevRichcontentEl = getPrevRichcontentEl(link)
- let richcontent = createElement(`<div class="richcontent" style="min-height: 30vh;" data-imgur-url="${link.href}"></div>`)
- lazyObserver.observe(richcontent)
- insertElementToNextLine(prevRichcontentEl, richcontent)
- }
- }
-
-
- // == main ==
-
- var regExpData = {
- imgur: {
- idExt: /^\/(\w+)(?:\.(\w+))?$/,
- album: /\/a\/(\w+)/,
- gallery: /\/gallery\/(\w+)/,
- },
- }
- var pageData = {
- set metaReferrer (value) {
- if (this.metaReferrer !== undefined) {
- document.querySelector('meta[name="referrer"]').content = value
- } else {
- document.head.append(createElement(`<meta name="referrer" content="${value}">`))
- }
- },
-
- get metaReferrer () {
- return document.querySelector('meta[name="referrer"]')?.content
- },
-
- get isMobile () {
- return navigator.userAgentData.mobile
- },
- }
- var boardData = (() => {
- let result = {}
-
- if (/^\/(bbs|man)\/([^\/]+)(?:\/[^\/]+)*\/(?:M|G)\.\d+\.A\.[0-9A-F]{3}\.html/.test(location.pathname)) {
- result = {
- type: 'post',
- area: RegExp.$1,
- name: RegExp.$2,
- is404: document.title === '404',
- }
- } else if (/^\/(bbs|man)\/([^\/]+)(?:\/[^\/]+)*\/index(\d*).html/.test(location.pathname)) {
- result = {
- type: 'index',
- area: RegExp.$1,
- name: RegExp.$2,
- pageNum: RegExp.$3 === '' ? 0 : parseInt(RegExp.$3, 10),
- }
- } else if (/^\/(bbs|man)\/([^\/]+)\/search/.test(location.pathname)) {
- result = {
- type: 'search',
- area: RegExp.$1,
- name: RegExp.$2,
- isHeadline: /[\?&]q=recommend%3A100/.test(location.search),
- }
- } else if (location.pathname === '/ask/over18') {
- result = {
- type: 'over18',
- }
- }
-
- return result
- })()
-
- switch (boardData.type) {
- case 'over18':
- agreeOver18()
- break
- }
-
- document.addEventListener('DOMContentLoaded', function () {
- switch (boardData.type) {
- case 'post':
- if (!boardData.is404) {
- pageData.metaReferrer = 'no-referrer'
- pttImageEnhanced()
- // 只有一般看板頁面需要,排除精華區
- if (boardData.area === 'bbs') {
- addSearch()
- }
- }
- break
-
- case 'index':
- case 'search':
- addHeadlines()
- // 手機因為排版關係,使用最新來被爆文取代,但精華區並沒有最新按鈕,所以要排除
- if (pageData.isMobile && boardData.area === 'bbs') {
- let oldestEl = document.querySelector('.btn.wide')
- oldestEl.insertAdjacentElement('beforebegin', document.querySelectorAll('.btn')[2])
- oldestEl.remove()
- }
- break
- }
- }, { once: true })
- })()