Reddit expand media and comments

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

目前为 2018-10-19 提交的版本,查看 最新版本

  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.9
  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. // @connect instagram.com
  15. // ==/UserScript==
  16.  
  17. const CLASS = 'reddit-inline-media';
  18. const MORE_SELECTOR = '[id^="moreComments-"] p';
  19. const RULES = [
  20. {r:/^https?:\/\/(i\.)?imgur\.com\/a\/(\w+)$/i,
  21. s:'https://imgur.com/ajaxalbums/getimages/$2/hit.json?all=true',
  22. q:(json, text) =>
  23. Array.from(((json || {}).data || {}).images || [])
  24. .map(img => img && `https://i.imgur.com/${img.hash}${img.ext}`),
  25. },
  26. {r:/^https?:\/\/(i\.)?imgur\.com\/\w+$/i, q:'link[rel="image_src"], meta[name="twitter:player:stream"]'},
  27. {r:/^https?:\/\/streamable\.com\/.+/i, q:'video'},
  28. {r:/^https?:\/\/gfycat\.com\/.+/i, q:'source[src*=".webm"]'},
  29. {r:/^https?:\/\/(www\.)?instagram\.com\/p\/[^/]+\/?$/i, q:'meta[property="og:image"]'},
  30. {r:/\.gifv$/i, s:'.mp4'},
  31. {r:/\.(jpe?g|png|gif|webm|mp4)$/i},
  32. ];
  33.  
  34. GM_addStyle(`
  35. .${CLASS} {
  36. max-width: 100%;
  37. display: block;
  38. }
  39. .${CLASS}:hover {
  40. outline: 2px solid #3bbb62;
  41. }
  42. `);
  43.  
  44. const isChrome = navigator.userAgent.includes('Chrom');
  45.  
  46. new MutationObserver(onMutation)
  47. .observe(document.body, {subtree: true, childList: true});
  48.  
  49. onMutation([{
  50. addedNodes: [document.body]
  51. }]);
  52.  
  53. const scrollObserver = new IntersectionObserver(expandComments, {
  54. rootMargin: window.innerHeight + 'px',
  55. });
  56.  
  57. function onMutation(mutations) {
  58. const items = [];
  59. let someElementsAdded = false;
  60. for (var i = 0, m; (m = mutations[i++]);) {
  61. for (var j = 0, added = m.addedNodes, node; (node = added[j++]);) {
  62. if (node.nodeType !== 1) continue; // Node.ELEMENT_NODE
  63. someElementsAdded = true;
  64. if (node.localName === 'a') {
  65. const data = preprocess(node);
  66. if (data) items.push(data);
  67. continue;
  68. }
  69. if (!node.children[0]) continue;
  70. var aa = node.getElementsByTagName('a');
  71. for (var k = 0, a; (a = aa[k++]);) {
  72. const data = preprocess(a);
  73. if (data) items.push(data);
  74. }
  75. }
  76. }
  77. if (someElementsAdded) debounce(observeShowMore);
  78. if (items.length) setTimeout(process, 0, items);
  79. }
  80.  
  81. function preprocess(a) {
  82. let url = a.href;
  83. for (const {r, s, q} of RULES) {
  84. if (typeof r === 'string') {
  85. if (!url.includes(r)) continue;
  86. } else {
  87. if (!r.test(url)) continue;
  88. if (s) url = url.replace(r, s);
  89. }
  90. return {a, url, q};
  91. }
  92. }
  93.  
  94. function process(items) {
  95. for (const item of items) {
  96. const {a, url, q} = item;
  97. const text = a.textContent.trim();
  98. if (
  99. !/^https?:\/\/\S+?\.{3}$/.test(text) && (
  100. a.parentNode.localName === 'p' &&
  101. a.parentNode.textContent.trim().length > text.length ||
  102. !a.closest('.scrollerItem,' +
  103. '[data-test-id="post-content"],' +
  104. `img[src="${url}"] + * a[href="${url}"]`)
  105. )
  106. ) {
  107. (q ? expandRemote : expand)(item);
  108. }
  109. }
  110. }
  111.  
  112. function expandRemote({a, url, q}) {
  113. GM_xmlhttpRequest({
  114. url,
  115. method: 'GET',
  116. onload: r => {
  117. const isJSON = /^content-type:.*?json\s*$/mi.test(r.responseHeaders);
  118. const doc = isJSON ?
  119. tryJSONparse(r.response) :
  120. new DOMParser().parseFromString(r.response, 'text/html');
  121. if (typeof q === 'string') {
  122. if (isJSON) return;
  123. const el = doc && doc.querySelector(q);
  124. const url = el && (el.href || el.src || el.content);
  125. if (url) expand({a, url});
  126. return;
  127. }
  128. let urls;
  129. if (typeof q === 'function') {
  130. try {
  131. urls = q(doc, r.response);
  132. } catch (e) {}
  133. }
  134. if (!urls || !urls.length) return;
  135. for (const url of Array.isArray(urls) ? urls : [urls]) {
  136. if (!url) continue;
  137. a = expand({a, url});
  138. }
  139. },
  140. });
  141. }
  142.  
  143. function expand({a, url = a.href}) {
  144. const isVideo = /(webm|gifv|mp4)(\?.*)?$/i.test(url);
  145. const el = document.createElement(isVideo ? 'video' : 'img');
  146. el.src = url;
  147. el.className = CLASS;
  148. a.insertAdjacentElement('afterend', el);
  149. if (isVideo) {
  150. el.controls = true;
  151. el.preload = 'metadata';
  152. if (isChrome) el.addEventListener('click', playOnClick);
  153. }
  154. return el;
  155. }
  156.  
  157. function observeShowMore() {
  158. const more = document.querySelector(MORE_SELECTOR);
  159. if (!more) return;
  160. for (const el of document.querySelectorAll(MORE_SELECTOR)) {
  161. scrollObserver.observe(el);
  162. }
  163. }
  164.  
  165. function expandComments(entries) {
  166. for (const e of entries) {
  167. if (!e.isIntersecting) continue;
  168. e.target.dispatchEvent(new MouseEvent('click', {bubbles: true}));
  169. }
  170. }
  171.  
  172. function playOnClick(event, el, wasPaused) {
  173. if (!el) {
  174. setTimeout(playOnClick, 0, event, this, this.paused);
  175. } else if (el.paused === wasPaused) {
  176. wasPaused ? el.play() : el.pause();
  177. }
  178. }
  179.  
  180. function debounce(fn, timeout = 0, ...args) {
  181. clearTimeout(fn.__timeout);
  182. fn.__timeout = setTimeout(fn, timeout, ...args);
  183. }
  184.  
  185. function tryJSONparse(str) {
  186. try {
  187. return JSON.parse(str);
  188. } catch (e) {
  189. return undefined;
  190. }
  191. }