Greasy Fork is available in English.

Reddit expand media and comments

Shows pictures and some videos right after the link, loads and expands comment threads.

Från och med 2019-02-17. Se den senaste versionen.

// ==UserScript==
// @name           Reddit expand media and comments
// @description    Shows pictures and some videos right after the link, loads and expands comment threads.
// @version        0.1.7
// @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
// @connect        ibb.co
// @connect        prntscr.com
// @connect        prnt.sc
// ==/UserScript==

const CLASS = 'reddit-inline-media';
const CLASS_ALBUM = CLASS + '-album';
const MORE_SELECTOR = '[id^="moreComments-"] p, .morecomments a';
const RULES = [
  {r:/^https?:\/\/(i\.)?imgur\.com\/(?:a|gallery)\/(\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:/^https?:\/\/ibb\.co\/\w+$/i, q:'meta[property="og:image"]'},
  {r:/^https?:\/\/prntscr\.com\/(\w+)$/i, s:'https://prnt.sc/$1', q:'meta[property="og:image"]', xhr:true},
  {r:/^https?:\/\/prnt\.sc\/(\w+)$/i, q:'meta[property="og:image"]', xhr:true},
  {r:/\.gifv(\?.*)?$/i, s:'.mp4'},
  {r:/\.(jpe?g|png|gif|webm|mp4)(\?.*)?$/i},
];

GM_addStyle(`
  .${CLASS} {
    max-width: 100%;
    display: block;
  }
  .${CLASS}[data-src] {
    padding-top: 300px;
  }
  .${CLASS}:hover {
    outline: 2px solid #3bbb62;
  }
  .${CLASS_ALBUM} {
    overflow-y: auto;
    max-height: calc(100vh - 100px);
    margin: .5em 0;
    -webkit-mask-image: linear-gradient(white, transparent);
    mask-image: linear-gradient(white, transparent);
  }
  .${CLASS_ALBUM}:hover {
    -webkit-mask-image: none;
    mask-image: none;
  }
  .${CLASS_ALBUM} > :nth-child(n + 2) {
    margin-top: 1em;
  }
`);

const isChrome = navigator.userAgent.includes('Chrom');

const REQUEST_THROTTLE_MS = location.hostname.startsWith('old.') ? 500 : 250;
const ME = Symbol(GM_info.script.name);
const more = [];

new MutationObserver(onMutation)
  .observe(document.body, {subtree: true, childList: true});

onMutation([{
  addedNodes: [document.body]
}]);

const scrollObserver = new IntersectionObserver(onScroll, {
  rootMargin: '200% 0px',
});

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 onScroll(entries, observer) {
  for (const e of entries) {
    if (!e.isIntersecting)
      continue;
    let el = e.target;
    if (el.localName === 'img' || el.localName === 'video') {
      observer.unobserve(el);
      el.src = el.dataset.src;
      delete el.dataset.src;
      continue;
    }
    if (el.localName === 'a') {
      // switch to an unfocusable element to prevent the link
      // from stealing focus and scrolling the view
      const el2 = document.createElement('span');
      el2.setAttribute('onclick', el.getAttribute('onclick'));
      el2.setAttribute('id', el.id);
      el.parentNode.replaceChild(el2, el);
      el = el2;
    }
    expandNextComment(el);
  }
}

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 {href} = a;
    const text = a.textContent.trim();
    if (
      text &&
      !a.getElementsByTagName('img')[0] &&
      !/^https?:\/\/\S+?\.{3}$/.test(text) &&
      !a.closest('.scrollerItem,' +
                 '[contenteditable="true"],' +
                 '[data-test-id="post-content"],' +
                 `a[href="${href}"] + * a[href="${href}"],` +
                 `img[src="${href}"] + * a[href="${href}"]`)
    ) {
      (q ? expandRemote : expand)(item);
    }
  }
}

function expandRemote({a, url, q, xhr}) {
  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 imageUrl = el && (el.href || el.src || el.content);
        if (imageUrl) {
          if (xhr) {
            GM_xmlhttpRequest({
              imageUrl,
              method: 'GET',
              headers: {Referer: url},
              responseType: 'arraybuffer',
              onload: r => {
                expand({a, url: arrayBufferToBase64(r.response, getMimeType(imageUrl))});
              },
            });
          } else {
            expand({a, url: imageUrl});
          }
        }
        return;
      }
      let urls;
      if (typeof q === 'function') {
        try {
          urls = q(doc, r.response);
        } catch (e) {}
      }
      if (!urls || !urls.length) return;
      urls = Array.isArray(urls) ? urls : [urls];
      let observer;
      if (urls.length > 1) {
        observer = new IntersectionObserver(onScroll, {rootMargin: '200px 0px'});
        a = a.insertAdjacentElement('afterEnd', document.createElement('div'));
        a.className = CLASS_ALBUM;
      }
      for (const url of urls) {
        if (url)
          a = expand({a, url}, observer);
      }
    },
  });
}

function expand({a, url = a.href}, observer = scrollObserver) {
  const isVideo = /(webm|gifv|mp4)(\?.*)?$/i.test(url);
  const el = document.createElement(isVideo ? 'video' : 'img');
  el.dataset.src = url;
  el.className = CLASS;
  a.insertAdjacentElement(a.classList.contains(CLASS_ALBUM) ? 'beforeEnd' : 'afterEnd', el);
  if (isVideo) {
    el.controls = true;
    el.preload = 'metadata';
    if (isChrome) el.addEventListener('click', playOnClick);
  }
  observer.observe(el);
  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 expandNextComment(el) {
  if (el)
    more.push(el);
  else
    more.shift();
  if (more.length === 1 || !el && more.length) {
    more[0].dispatchEvent(new MouseEvent('click', {bubbles: true}));
    setTimeout(expandNextComment, REQUEST_THROTTLE_MS);
  }
}

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;
  }
}

function arrayBufferToBase64(buffer, type) {
  return new Promise(resolve => {
    Object.assign(new FileReader(), {
      onload: e => resolve(e.target.result)
    }).readAsDataURL(new Blob([buffer], {type}));
  });
}

function getMimeType(url) {
  const ext = (url.match(/\.(\w+)(\?.*)?$|$/)[1] || '').toLowerCase();
  return 'image/' + (ext === 'jpg' ? 'jpeg' : ext);
}