Reddit expand media and comments

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

当前为 2018-08-23 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Reddit expand media and comments
  3. // @description Shows pictures and some videos right after the link, loads and expands comment threads.
  4. // @version 0.0.3
  5. // @author wOxxOm
  6. // @namespace wOxxOm.scripts
  7. // @license MIT License
  8. // @match *://*.reddit.com/*
  9. // @grant GM_addStyle
  10. // @grant GM_xmlhttpRequest
  11. // @connect imgur.com
  12. // @connect gfycat.com
  13. // @connect streamable.com
  14. // ==/UserScript==
  15.  
  16. const CLASS = 'reddit-inline-media';
  17. const MORE_SELECTOR = '[id^="moreComments-"] p';
  18. const RULES = [
  19. {r:/^https?:\/\/imgur\.com\/a\/.+/i, q:'link[rel="image_src"]'},
  20. {r:/^https?:\/\/streamable\.com\/.+/i, q:'video'},
  21. {r:/^https?:\/\/gfycat\.com\/.+/i, q:'#webmSource'},
  22. {r:/\.gifv$/i, s:'.mp4'},
  23. {r:/\.(jpe?g|png|gif|webm|mp4)$/i},
  24. ];
  25.  
  26. GM_addStyle(`
  27. .${CLASS} {
  28. width: 100%;
  29. display: block;
  30. }
  31. .${CLASS}:hover {
  32. outline: 2px solid #3bbb62;
  33. }
  34. `);
  35.  
  36. const isChrome = navigator.userAgent.includes('Chrom');
  37.  
  38. new MutationObserver(onMutation)
  39. .observe(document.body, {subtree: true, childList: true});
  40.  
  41. onMutation([{
  42. addedNodes: [document.body]
  43. }]);
  44.  
  45. const scrollObserver = new IntersectionObserver(expandComments, {
  46. rootMargin: window.innerHeight + 'px',
  47. });
  48.  
  49. function onMutation(mutations) {
  50. const items = [];
  51. let someElementsAdded = false;
  52. for (var i = 0, m; (m = mutations[i++]);) {
  53. for (var j = 0, added = m.addedNodes, node; (node = added[j++]);) {
  54. if (node.nodeType !== 1) continue; // Node.ELEMENT_NODE
  55. someElementsAdded = true;
  56. if (node.localName === 'a') {
  57. const data = preprocess(node);
  58. if (data) items.push(data);
  59. continue;
  60. }
  61. if (!node.children[0]) continue;
  62. var aa = node.getElementsByTagName('a');
  63. for (var k = 0, a; (a = aa[k++]);) {
  64. const data = preprocess(a);
  65. if (data) items.push(data);
  66. }
  67. }
  68. }
  69. if (someElementsAdded) debounce(observeShowMore);
  70. if (items.length) setTimeout(process, 0, items);
  71. }
  72.  
  73. function preprocess(a) {
  74. let url = a.href;
  75. for (const {r, s, q} of RULES) {
  76. if (typeof r === 'string') {
  77. if (!url.includes(r)) continue;
  78. } else {
  79. if (!r.test(url)) continue;
  80. if (s) url = url.replace(r, s);
  81. }
  82. return {a, url, q};
  83. }
  84. }
  85.  
  86. function process(items) {
  87. for (const item of items) {
  88. const {a, url, q} = item;
  89. if (!/^https?:\/\/\S+?\.{3}$/.test(a.textContent) &&
  90. !a.closest('[data-test-id="post-content"], .scrollerItem') &&
  91. !a.closest(`img[src="${url}"] + * a[href="${url}"]`)) {
  92. q ? expandRemote(item) : expand(item);
  93. }
  94. }
  95. }
  96.  
  97. function expandRemote({a, url, q}) {
  98. GM_xmlhttpRequest({
  99. url,
  100. method: 'GET',
  101. onload: r => {
  102. const doc = new DOMParser().parseFromString(r.response, 'text/html');
  103. const el = doc && doc.querySelector(q);
  104. if (el) expand({a, url: el.href || el.src});
  105. },
  106. });
  107. }
  108.  
  109. function expand({a, url = a.href}) {
  110. const isVideo = /(webm|gifv|mp4)(\?.*)?$/i.test(url);
  111. const el = document.createElement(isVideo ? 'video' : 'img');
  112. el.src = url;
  113. el.className = CLASS;
  114. a.insertAdjacentElement('afterend', el);
  115. if (isVideo) {
  116. el.controls = true;
  117. el.preload = 'metadata';
  118. if (isChrome) el.addEventListener('click', playOnClick);
  119. }
  120. return el;
  121. }
  122.  
  123. function observeShowMore() {
  124. const more = document.querySelector(MORE_SELECTOR);
  125. if (!more) return;
  126. for (const el of document.querySelectorAll(MORE_SELECTOR)) {
  127. scrollObserver.observe(el);
  128. }
  129. }
  130.  
  131. function expandComments(entries) {
  132. for (const e of entries) {
  133. if (!e.isIntersecting) continue;
  134. e.target.dispatchEvent(new MouseEvent('click', {bubbles: true}));
  135. }
  136. }
  137.  
  138. function playOnClick(event, el, wasPaused) {
  139. if (!el) {
  140. setTimeout(playOnClick, 0, event, this, this.paused);
  141. } else if (el.paused === wasPaused) {
  142. wasPaused ? el.play() : el.pause();
  143. }
  144. }
  145.  
  146. function debounce(fn, timeout = 0, ...args) {
  147. clearTimeout(fn.__timeout);
  148. fn.__timeout = setTimeout(fn, timeout, ...args);
  149. }