- // ==UserScript==
- // @name Reddit expand media and comments
- // @description Shows pictures and some videos right after the link, loads and expands comment threads.
- // @version 0.0.9
- // @author wOxxOm
- // @namespace wOxxOm.scripts
- // @license MIT License
- // @match *://*.reddit.com/*
- // @grant GM_addStyle
- // @grant GM_xmlhttpRequest
- // @connect imgur.com
- // @connect gfycat.com
- // @connect streamable.com
- // @connect instagram.com
- // ==/UserScript==
-
- const CLASS = 'reddit-inline-media';
- const MORE_SELECTOR = '[id^="moreComments-"] p';
- const RULES = [
- {r:/^https?:\/\/(i\.)?imgur\.com\/a\/(\w+)$/i,
- s:'https://imgur.com/ajaxalbums/getimages/$2/hit.json?all=true',
- q:(json, text) =>
- Array.from(((json || {}).data || {}).images || [])
- .map(img => img && `https://i.imgur.com/${img.hash}${img.ext}`),
- },
- {r:/^https?:\/\/(i\.)?imgur\.com\/\w+$/i, q:'link[rel="image_src"], meta[name="twitter:player:stream"]'},
- {r:/^https?:\/\/streamable\.com\/.+/i, q:'video'},
- {r:/^https?:\/\/gfycat\.com\/.+/i, q:'source[src*=".webm"]'},
- {r:/^https?:\/\/(www\.)?instagram\.com\/p\/[^/]+\/?$/i, q:'meta[property="og:image"]'},
- {r:/\.gifv$/i, s:'.mp4'},
- {r:/\.(jpe?g|png|gif|webm|mp4)$/i},
- ];
-
- GM_addStyle(`
- .${CLASS} {
- max-width: 100%;
- display: block;
- }
- .${CLASS}:hover {
- outline: 2px solid #3bbb62;
- }
- `);
-
- const isChrome = navigator.userAgent.includes('Chrom');
-
- new MutationObserver(onMutation)
- .observe(document.body, {subtree: true, childList: true});
-
- onMutation([{
- addedNodes: [document.body]
- }]);
-
- const scrollObserver = new IntersectionObserver(expandComments, {
- rootMargin: window.innerHeight + 'px',
- });
-
- function onMutation(mutations) {
- const items = [];
- let someElementsAdded = false;
- for (var i = 0, m; (m = mutations[i++]);) {
- for (var j = 0, added = m.addedNodes, node; (node = added[j++]);) {
- if (node.nodeType !== 1) continue; // Node.ELEMENT_NODE
- someElementsAdded = true;
- if (node.localName === 'a') {
- const data = preprocess(node);
- if (data) items.push(data);
- continue;
- }
- if (!node.children[0]) continue;
- var aa = node.getElementsByTagName('a');
- for (var k = 0, a; (a = aa[k++]);) {
- const data = preprocess(a);
- if (data) items.push(data);
- }
- }
- }
- if (someElementsAdded) debounce(observeShowMore);
- if (items.length) setTimeout(process, 0, items);
- }
-
- function preprocess(a) {
- let url = a.href;
- for (const {r, s, q} of RULES) {
- if (typeof r === 'string') {
- if (!url.includes(r)) continue;
- } else {
- if (!r.test(url)) continue;
- if (s) url = url.replace(r, s);
- }
- return {a, url, q};
- }
- }
-
- function process(items) {
- for (const item of items) {
- const {a, url, q} = item;
- const text = a.textContent.trim();
- if (
- !/^https?:\/\/\S+?\.{3}$/.test(text) && (
- a.parentNode.localName === 'p' &&
- a.parentNode.textContent.trim().length > text.length ||
- !a.closest('.scrollerItem,' +
- '[data-test-id="post-content"],' +
- `img[src="${url}"] + * a[href="${url}"]`)
- )
- ) {
- (q ? expandRemote : expand)(item);
- }
- }
- }
-
- function expandRemote({a, url, q}) {
- GM_xmlhttpRequest({
- url,
- method: 'GET',
- onload: r => {
- const isJSON = /^content-type:.*?json\s*$/mi.test(r.responseHeaders);
- const doc = isJSON ?
- tryJSONparse(r.response) :
- new DOMParser().parseFromString(r.response, 'text/html');
- if (typeof q === 'string') {
- if (isJSON) return;
- const el = doc && doc.querySelector(q);
- const url = el && (el.href || el.src || el.content);
- if (url) expand({a, url});
- return;
- }
- let urls;
- if (typeof q === 'function') {
- try {
- urls = q(doc, r.response);
- } catch (e) {}
- }
- if (!urls || !urls.length) return;
- for (const url of Array.isArray(urls) ? urls : [urls]) {
- if (!url) continue;
- a = expand({a, url});
- }
- },
- });
- }
-
- function expand({a, url = a.href}) {
- const isVideo = /(webm|gifv|mp4)(\?.*)?$/i.test(url);
- const el = document.createElement(isVideo ? 'video' : 'img');
- el.src = url;
- el.className = CLASS;
- a.insertAdjacentElement('afterend', el);
- if (isVideo) {
- el.controls = true;
- el.preload = 'metadata';
- if (isChrome) el.addEventListener('click', playOnClick);
- }
- return el;
- }
-
- function observeShowMore() {
- const more = document.querySelector(MORE_SELECTOR);
- if (!more) return;
- for (const el of document.querySelectorAll(MORE_SELECTOR)) {
- scrollObserver.observe(el);
- }
- }
-
- function expandComments(entries) {
- for (const e of entries) {
- if (!e.isIntersecting) continue;
- e.target.dispatchEvent(new MouseEvent('click', {bubbles: true}));
- }
- }
-
- function playOnClick(event, el, wasPaused) {
- if (!el) {
- setTimeout(playOnClick, 0, event, this, this.paused);
- } else if (el.paused === wasPaused) {
- wasPaused ? el.play() : el.pause();
- }
- }
-
- function debounce(fn, timeout = 0, ...args) {
- clearTimeout(fn.__timeout);
- fn.__timeout = setTimeout(fn, timeout, ...args);
- }
-
- function tryJSONparse(str) {
- try {
- return JSON.parse(str);
- } catch (e) {
- return undefined;
- }
- }