Greasy Fork is available in English.
修正 Imgur 在 PTT 上的問題
// ==UserScript== // @name PTT Imgur Fix // @description 修正 Imgur 在 PTT 上的問題 // @namespace eight04.blogspot.com // @match https://www.ptt.cc/bbs/*.html // @match https://www.ptt.cc/man/*.html // @match https://term.ptt.cc/ // @version 0.9.5 // @author eight // @homepage https://github.com/eight04/ptt-imgur-fix // @supportURL https://github.com/eight04/ptt-imgur-fix/issues // @license MIT // @compatible firefox Tampermonkey, Violentmonkey, Greasemonkey 4.11+ // @compatible chrome Tampermonkey, Violentmonkey // @run-at document-start // @grant GM_getValue // @grant GM.getValue // @grant GM_setValue // @grant GM.setValue // @grant GM_deleteValue // @grant GM.deleteValue // @grant GM_addValueChangeListener // @grant GM_registerMenuCommand // @grant GM.registerMenuCommand // @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest // @require https://greasyfork.org/scripts/371339-gm-webextpref/code/GM_webextPref.js?version=961539 // @require https://cdnjs.cloudflare.com/ajax/libs/sentinel-js/0.0.7/sentinel.min.js // @connect imgur.com // ==/UserScript== /* global GM_webextPref sentinel */ const request = typeof GM_xmlhttpRequest === "function" ? GM_xmlhttpRequest : GM.xmlHttpRequest; const pref = GM_webextPref({ default: { term: true, embedYoutube: true, youtubeParameters: "", embedImage: true, embedAlbum: false, embedVideo: true, albumMaxSize: 5, imgurVideo: false, lazyLoad: true, maxWidth: "100%", maxHeight: "none", }, body: [ { key: "embedImage", label: "Embed image", type: "checkbox", }, { key: "embedVideo", label: "Embed video", type: "checkbox", }, { key: "embedAlbum", label: "Embed imgur album. The script would request imgur.com for album info", type: "checkbox", children: [ { key: "albumMaxSize", label: "Maximum number of images to load for an album", type: "number" } ] }, { key: "imgurVideo", label: "Embed imgur video instead of GIF. Reduce file size", type: "checkbox" }, { key: "embedYoutube", label: "Embed youtube video", type: "checkbox", children: [ { key: "youtubeParameters", label: "Youtube player parameters (e.g. rel=0&loop=1)", type: "text", default: "" } ] }, { key: "lazyLoad", label: "Don't load images until scrolled into view", type: "checkbox" }, { key: "maxWidth", label: "Maximum width of image", type: "text", }, { key: "maxHeight", label: "Maximum height of image", type: "text", }, ], navbar: false }); const lazyLoader = (() => { const xo = new IntersectionObserver(onXoChange, {rootMargin: "30% 0px 30% 0px"}); const elMap = new Map; pref.on('change', onPrefChange); return {add, clear}; function clear() { for (const target of elMap.values()) { xo.unobserve(target.el); } elMap.clear(); } function onPrefChange(changes) { if (changes.lazyLoad == null) return; if (changes.lazyLoad) { for (const target of elMap.values()) { xo.observe(target.el); } } else { xo.disconnect(); for (const target of elMap.values()) { target.visible = true; loadTarget(target); showTarget(target); } } } function add(el) { if (elMap.has(el)) return; const target = { el, state: 'pause', visible: false, finalUrl: '', mask: null, width: 0, height: 0 }; elMap.set(el, target); el.classList.add('lazy-target'); if (pref.get('lazyLoad')) { xo.observe(target.el); } else { target.visible = true; loadTarget(target); } } function onXoChange(entries) { for (const entry of entries) { const target = elMap.get(entry.target); if (!target) { // unobserved element continue; } if (entry.isIntersecting) { target.visible = true; loadTarget(target); showTarget(target); } else { target.visible = false; hideTarget(target); } } } async function loadTarget(target) { if (target.state !== 'pause') return; target.state = 'loading'; try { if (target.el.tagName === 'IMG' || target.el.tagName === 'IFRAME') { setSrc(target.el, target.el.dataset.src); await loadMedia(target.el); target.finalUrl = target.el.dataset.src; } else if (target.el.tagName === 'VIDEO') { const r = await fetch(target.el.dataset.src, { referrerPolicy: "no-referrer" }); const b = await r.blob(); const finalUrl = URL.createObjectURL(b); target.finalUrl = finalUrl; target.el.src = finalUrl; await loadMedia(target.el); } else { throw new Error(`Invalid media: ${target.el.tagName}`); } target.state = 'complete'; const {offsetWidth: w, offsetHeight: h} = target.el; target.el.style.aspectRatio = `${w} / ${h}`; if (target.visible) { showTarget(target, false); } else { hideTarget(target); } } catch (err) { console.error(err); target.state = 'pause'; } } function loadMedia(el) { return new Promise((resolve, reject) => { el.classList.add('lazy-load-start'); el.addEventListener('load', onLoad); el.addEventListener('loadeddata', onLoad); el.addEventListener('error', onError); function cleanup() { el.classList.add('lazy-load-end'); el.removeEventListener('load', onLoad); el.removeEventListener('loadeddata', onLoad); el.removeEventListener('error', onError); } function onLoad() { resolve(); cleanup(); } function onError(e) { console.error(e); reject(new Error(`failed loading media: ${el.src}`)); cleanup(); } }); } function showTarget(target, useSrc = true) { if (target.state !== 'complete' && target.state !== 'hidden') return; if (useSrc) { setSrc(target.el, target.finalUrl); loadMedia(target.el) .then(() => { if (target.el.style.width) { target.el.style.width = ''; target.el.style.height = ''; } }); } target.state = 'shown'; } function hideTarget(target) { if (target.state !== 'complete' && target.state !== 'shown') return; if (target.el.tagName === 'IFRAME') return; const {offsetWidth: w, offsetHeight: h} = target.el; if (w && h) { target.el.style.width = `${w}px`; // Waterfox // https://greasyfork.org/zh-TW/scripts/28264-ptt-imgur-fix/discussions/115795 if (!CSS.supports("aspect-ratio", "1/1")) { target.el.style.height = `${h}px`; } } setSrc(target.el, 'about:blank'); target.state = 'hidden'; } })(); document.addEventListener("beforescriptexecute", e => { var url = new URL(e.target.src, location.href); if (url.hostname.endsWith("imgur.com")) { e.preventDefault(); } }); Promise.all([ pref.ready(), domReady() ]) .then(init) .catch(console.error); function domReady() { return new Promise(resolve => { if (document.readyState !== "loading") { resolve(); return; } document.addEventListener("DOMContentLoaded", resolve, {once: true}); }); } function createStyle(css) { const style = document.createElement("style"); style.textContent = css; document.head.appendChild(style); } function init() { createStyle(` .ptt-imgur-fix { max-width: ${pref.get("maxWidth")}; max-height: none; } .ptt-imgur-fix img, .ptt-imgur-fix video, .ptt-imgur-fix iframe { max-width: 100%; max-height: ${pref.get("maxHeight")}; } .lazy-target:not(.lazy-load-end) { /* give them a size so that we don't load them all at once */ min-height: 50vh; } span[type=bbsrow] .richcontent { display: flex; justify-content: center; .resize-container { flex-grow: 1; } iframe { aspect-ratio: 16 / 9; width: 100%; } } `) if (location.hostname === "term.ptt.cc") { if (pref.get("term")) { initTerm(); } } else { initWeb(); } } function initTerm() { const selector = "span[type=bbsrow] a:not(.embeded)"; detectEasyReading({ on: () => sentinel.on(selector, onLink), off: () => { sentinel.off(selector); lazyLoader.clear(); } }); function onLink(node) { node.classList.add("embeded"); if (node.href) { const linkInfo = getLinkInfo(node); const bbsRowDiv = node.closest("span[type=bbsrow] > div"); const hasDefaultContent = !bbsRowDiv.children[1].classList.contains("richcontent"); if (linkInfo.embedable) { const richContent = createRichContent(linkInfo); if (!hasDefaultContent) { bbsRowDiv.appendChild(richContent); } else { bbsRowDiv.children[1].replaceWith(richContent); } } else if (hasDefaultContent) { // remove default content under links bbsRowDiv.children[1].innerHTML = ""; } } } } function waitElement(selector) { return new Promise(resolve => { const id = setInterval(() => { const el = document.querySelector(selector); if (el) { clearInterval(id); resolve(el); } }, 1000); }); } async function detectEasyReading({on, off}) { let state = false; const easyReadingLastRow = await waitElement("#easyReadingLastRow") // const easyReadingLastRow = document.querySelector("#easyReadingLastRow"); const observer = new MutationObserver(onMutations); observer.observe(easyReadingLastRow, {attributes: true, attributeFilter: ["style"]}); function onMutations() { const newState = easyReadingLastRow.style.display === "block"; if (newState === state) { return; } if (newState) { on(); } else { off(); } state = newState; } } function initWeb() { // remove old .richcontent var rich = document.querySelectorAll("#main-content .richcontent"); for (var node of rich) { node.parentNode.removeChild(node); } // embed links var links = document.querySelectorAll("#main-content a"), processed = new Set; for (var link of links) { if (processed.has(link) || !getLinkInfo(link).embedable) { continue; } var [links_, lineEnd] = findLinksInSameLine(link); links_.forEach(l => processed.add(l)); for (const link of links_) { const linkInfo = getLinkInfo(link); if (!linkInfo.embedable) { continue; } const richContent = createRichContent(linkInfo); lineEnd.parentNode.insertBefore(richContent, lineEnd.nextSibling); lineEnd = richContent; } // createRichContent(links_, lineEnd); } } function findLinksInSameLine(node) { var links = []; while (node) { if (node.nodeName == "A") { links.push(node); node = node.nextSibling || node.parentNode.nextSibling; continue; } if (node.nodeType == Node.TEXT_NODE && node.nodeValue.includes("\n")) { return [links, findLineEnd(node)]; } if (node.childNodes.length) { node = node.childNodes[0]; continue; } if (node.nextSibling) { node = node.nextSibling; continue; } if (node.parentNode.id != "main-content") { node = node.parentNode.nextSibling; continue; } throw new Error("Invalid article, missing new line?"); } } function findLineEnd(text) { var index = text.nodeValue.indexOf("\n"); if (index == text.nodeValue.length - 1) { while (text.parentNode.id != "main-content") { text = text.parentNode; } return text; } var pre = document.createTextNode(""); pre.nodeValue = text.nodeValue.slice(0, index + 1); text.nodeValue = text.nodeValue.slice(index + 1); text.parentNode.insertBefore(pre, text); return pre; } function createRichContent(linkInfo) { const richContent = document.createElement("div"); richContent.className = "richcontent ptt-imgur-fix"; const embed = createEmbed(linkInfo, richContent); if (typeof embed === "string") { richContent.innerHTML = embed; } else if (embed) { richContent.appendChild(embed); } const lazyTarget = richContent.querySelector("[data-src]"); if (lazyTarget) { lazyLoader.add(lazyTarget); } return richContent; } function getLinkInfo(link) { return getUrlInfo(link.href); } function getUrlInfo(url) { var match; if ((match = url.match(/\/\/(?:[im]\.)?imgur\.com\/([a-z0-9]{2,})(\.[a-z0-9]{3,4})?/i)) && match[1] != "gallery") { return { type: "imgur", id: match[1], url: url, embedable: pref.get("embedImage"), extension: match[2] && match[2].toLowerCase() }; } if ((match = url.match(/\/\/(?:[im]\.)?imgur\.com\/(?:a|gallery)\/([a-z0-9]{2,})/i))) { return { type: "imgur-album", id: match[1], url: url, embedable: pref.get("embedAlbum") }; } if ( (match = url.match(/youtube\.com\/watch?.*?v=([a-z0-9_-]{9,12})/i)) || (match = url.match(/(?:youtu\.be|youtube\.com\/embed)\/([a-z0-9_-]{9,12})/i)) || (match = url.match(/youtube\.com\/shorts\/([a-z0-9_-]{9,12})/i)) || (match = url.match(/youtube\.com\/live\/([a-z0-9_-]{9,12})/i)) ) { return { type: "youtube", id: match[1], url: url, embedable: pref.get("embedYoutube") }; } if ((match = url.match(/\/\/pbs\.twimg\.com\/media\/([a-z0-9_-]+\.(?:jpg|png))/i))) { return { type: "twitter", id: match[1], url: url, embedable: pref.get("embedImage") }; } if ((match = url.match(/\/\/pbs\.twimg\.com\/media\/([a-z0-9_-]+)\?.*format=([\w]+)/i))) { const ext = match[2] === "webp" ? ".jpg" : `.${match[2]}`; return { type: "twitter", id: `${match[1]}${ext}`, url: url, embedable: pref.get("embedImage") }; } if (/^[^?#]+\.(?:jpg|png|gif|jpeg|webp|apng|avif|jfif|pjpeg|pjp|svg)(?:$|[?#])/i.test(url)) { return { type: "image", id: null, url: url, embedable: pref.get("embedImage") }; } if (/.*\.(?:mp4|webm|ogg)(?:$|[?#])/i.test(url)) { return { type: "video", id: null, url: url, embedable: pref.get("embedVideo") }; } return { type: "url", id: null, url: url, embedable: false }; } function createEmbed(info, container) { if (info.type == "imgur") { let extension = info.extension || ".jpg"; if (extension === ".gif" && pref.get("imgurVideo")) { extension = ".mp4"; } if (extension === ".gifv") { extension = pref.get("imgurVideo") ? ".mp4" : ".gif"; } const url = `//i.imgur.com/${info.id}${extension}`; if (extension !== ".mp4") { return `<img referrerpolicy="no-referrer" data-src="${url}">`; } const video = document.createElement("video"); video.loop = true; video.autoplay = true; video.controls = true; video.dataset.src = url; video.muted = true; return video; } if (info.type == "youtube") { return `<div class="resize-container"><div class="resize-content"><iframe class="youtube-player" type="text/html" data-src="//www.youtube.com/embed/${info.id}?${mergeParams(new URL(info.url).search, pref.get("youtubeParameters"))}" frameborder="0" allowfullscreen></iframe></div></div>`; } if (info.type == "image") { return `<img referrerpolicy="no-referrer" data-src="${info.url}">`; } if (info.type == "video") { const video = document.createElement("video"); video.controls = true; video.dataset.src = info.url; return video; } if (info.type == "twitter") { const image = new Image; const urls = [ `//pbs.twimg.com/media/${info.id}:orig`, `//pbs.twimg.com/media/${info.id.replace(/\.jpg\b/, ".png")}:orig`, `//pbs.twimg.com/media/${info.id}:large`, `//pbs.twimg.com/media/${info.id}`, ]; image.dataset.src = urls.shift(); const onerror = function onerror() { if (!urls.length || !image.src.endsWith(image.dataset.src)) { // not loaded yet return; } const newUrl = urls.shift(); image.dataset.src = newUrl; image.src = newUrl; }; const onload = () => { image.removeEventListener("error", onerror); image.removeEventListener("load", onload); } image.addEventListener("error", onerror); image.addEventListener("load", onload); return image; } if (info.type == "imgur-album") { container.textContent = "Loading album..."; request({ method: "GET", url: `https://api.imgur.com/post/v1/albums/${info.id}?client_id=546c25a59c58ad7&include=media`, responseType: "json", onload(response) { if (response.status < 200 || response.status >= 300) { container.textContent = `${response.status} ${response.statusText}`; return; } container.textContent = ""; const urls = response.response.media.map(m => m.url); let i = 0; const loadImages = (count = Infinity) => { const els = []; for (; i < urls.length && count--; i++) { els.push(createRichContent(getUrlInfo(urls[i]))); } container.append(...els); }; loadImages(pref.get("albumMaxSize")); if (i < urls.length) { const button = document.createElement("button"); button.textContent = `Load all images (${urls.length - i} more)`; button.addEventListener('click', () => { button.remove(); loadImages(); }); container.appendChild(button); } } }); return; } throw new Error(`Invalid type: ${info.type}`); } function mergeParams(origSearch, userSearch) { const r###lt = new URLSearchParams(); for (const [key, value] of new URLSearchParams(origSearch)) { if (key === "t") { r###lt.set("start", value); } else { r###lt.set(key, value); } } for (const [key, value] of new URLSearchParams(userSearch)) { r###lt.set(key, value); } return r###lt.toString(); } function setSrc(el, url) { try { // https://github.com/eight04/ptt-imgur-fix/issues/22 el.contentWindow.location.replace(url); } catch (err) { el.src = url; } }