redditmod2

Dark themes, inline posts/comments, endless scrolling, subreddit filters, and other improvements for reddit.com

  1. // ==UserScript==
  2. // @name redditmod2
  3. // @namespace derv82
  4. // @description Dark themes, inline posts/comments, endless scrolling, subreddit filters, and other improvements for reddit.com
  5. // @include http://*.reddit.com/*
  6. // @include https://*.reddit.com/*
  7. // @version 2.2.5
  8. // @grant GM.getValue
  9. // @grant GM.setValue
  10. // @grant GM.xmlHttpRequest
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_xmlhttpRequest
  14. // @connect reddit.com
  15. // @connect www.reddit.com
  16. // @connect imgur.com
  17. // @connect mercury.postlight.com
  18. // @connect gfycat.com
  19. // @connect redgifs.com
  20. // @connect soundcloud.com
  21. // @connect explosm.net
  22. // @connect imgflip.com
  23. // @connect streamable.com
  24. // @connect instagram.com
  25. // @connect deviantart.com
  26. // @connect xkcd.com
  27. // @run-at document-start
  28. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  29. // ==/UserScript==
  30.  
  31. /** TODO
  32. * Delete filter button.
  33. * Support domains: giphy, flickr, prnt.sc, questionablecontent
  34. * Can't collapse/expand comments provided by RedditPromise for some reason... need to hijack onclick?
  35. * Can't click links in inline-comments (should be target=_blank).
  36. * Clicking bottom of inline-comment (marked as hovered) doesn't expand/collapse.
  37. * Voting on posts expands/collapses.
  38. * Can't vote on expanded comments.
  39. * Setting toggle: Shrink images to screen height
  40. * "Options" bar @ top: Fit to window, overflow-y:scroll ?
  41. */
  42.  
  43. (() => {
  44. 'use strict';
  45.  
  46. const DEBUG = false; // TODO: Change to false for release.
  47.  
  48. /** Helper/Wrapper around console.log -- includes stack trace. */
  49. function debug() {
  50. if (!DEBUG) return;
  51. const args = ['[Redditmod@'
  52. + (new Error()).stack.replace(/.*\?id=[a-f0-9\-]*:/g, '').replace(/@.*[^\r\n]/g, '').replace(/\n\n/g, '').replace(/\n$/g, '').replace(/\n/g, ' <- ')
  53. + ']'];
  54. for (let i = 0; i < arguments.length; i++) {
  55. args.push(arguments[i]);
  56. }
  57. console.log.apply(console.log, args);
  58. }
  59.  
  60. /** Helper function for preventing event bubbling. */
  61. function stopEvent(event) {
  62. if (event && event.stopPropagation) event.stopPropagation();
  63. if (event && event.preventDefault) event.preventDefault();
  64. }
  65.  
  66.  
  67. /**
  68. * Promises accessors to a loaded configuration.
  69. */
  70. const CONFIG = (() => {
  71. const BASE_STYLE = '.thing.gilded:hover,.thing:hover{background:linear-gradient(to right,var(--bg-2),var(--bg) 50%,var(--bg-2));box-shadow:0 0 5px rgba(0,0,0,.5)}body>.content .link .rank{width:auto!important}.thing{padding:3px 10px;border-radius:10px;transition:height .2s ease-in-out}#header-img.default-header{width:30px}.redditmod-filter-subreddit-button{border:none;background-color:var(--fg-link);color:var(--bg)!important;font-weight:900;border-radius:15px;padding:0 1.9px .5px;margin-left:5px;box-shadow:0 0 5px rgba(0,0,0,.5)}.redditmod-filter-subreddit-button:hover{text-decoration:none!important;box-shadow:0 0 2px rgba(0,0,0,.5)}';
  72. const THEMED_STYLE = '#header-bottom-right,.ProfileSidebar__titleContainer,.ProfileTemplate.m-updated,.content,.link,.listing-chooser li.selected,.side,.tabmenu li.selected a,.user-subreddit-banner,body,body.with-listing-chooser .listing-chooser .grippy,html{background-color:var(--bg)}#header,#sr-drop-down,#sr-header-area,.BlueBar,.Media.m-profile.m-image,.Post,.Post__pinInfo,.Post__stickiedDivider,.ProfileBarDropdown__list,.SubscriptionBar,.author-tooltip__head,.authorized-app,.crosspost-preview,.drop-choices,.explore-comment .explore-label,.explore-discovery .explore-label,.explore-hot .explore-label,.explore-rising .explore-label,.gold-accent,.hover-bubble,.infobar,.link .usertext-body .md,.linkinfo,.listing-chooser li,.liveupdate-listing li.liveupdate time::before,.liveupdate-listing li.separator time,.message .subject .correspondent,.message.new>.entry,.modal-content,.nextprev a:hover,.raisedbox,.report-modal .modal-content,.roundfield,.searchpane,.server-seconds,.sr-interest-bar,.sr-interest-bar .bubble,.subreddit .usertext .md,.tabmenu li a,.trophy-area .content,body.with-listing-chooser .listing-chooser,div.link.self:hover,main[role=main]{background-color:var(--bg-2)}#newlink-with-image-upload #new-link-preview,#newlink-with-image-upload .image-upload-drop-target,#sr-more-link,.BlueBar__account,.goldvertisement a,.infobar.profilebetabar,.link .md :not(pre)>code,.link .md pre,.link.promotedlink.promoted,.md code,.md pre,.nextprev a,.tabmenu li a:hover,.usertable tr:hover,.usertext.border .usertext-body,.white-field,oddrow{background-color:var(--bg-odd)}.form-control,.hover-bubble.multi-selector .create-multi input[type=text],.morelink,.morelink .nub,.morelink a,button.expand-summary,button.report-button,input,input[type=password],input[type=text],select,textarea{background-color:var(--bg-input);color:var(--fg-input)}.form-control:focus,.goldvertisement a:hover,.hover-bubble.multi-selector .create-multi input[type=text]:focus,.listing-chooser li:hover,.morelink:hover,.morelink:hover .nub,.morelink:hover a,body.with-listing-chooser .listing-chooser .grippy:hover::after,body.with-listing-chooser.listing-chooser-collapsed .listing-chooser .grippy::before,button.expand-summary:hover,input:focus,input[type=password]:focus,input[type=text]:focus,select:focus,textarea:focus{background-color:var(--bg-input-hover);color:var(--fg-input-hover)}.explore-comment .comment-fade,.form-control,.infobar.onboardingbar,.listing-chooser li.selected,.morelink,.morelink .nub,.morelink:hover,.morelink:hover .nub,.navbar,.search-expando.collapsed::before,.sidebox.hohoho .morelink,body,body.with-listing-chooser .listing-chooser .grippy:hover::after,body.with-listing-chooser.listing-chooser-collapsed .listing-chooser .grippy::before,input[type=text],select,textarea{background-image:none}.hover-bubble.multi-selector label:hover,.icon-menu a,.morelink .nub,.side ul.content,.sidebox .spacer,.titlebox form.toggle,aside.sidebar #discussions li,div.link.self{background-color:transparent}.ProfileBarDropdown__listItem:hover,.drop-choices a.choice:hover,.flair,.linkflairlabel,.wiki-page .pageactions .wikiaction:hover,a.message-count{background-color:var(--fg-link);color:var(--bg)!important}.tagline .flair{background-color:var(--fg-muted);color:var(--bg)}.md hr,.tabmenu.formtab .selected a{background-color:var(--border-color)}.Post__pinInfo,.TabMenu__tab.m-active,.card a,.gold-accent,.goldvertisement,.hover-bubble,.liveupdate-listing li.separator time,.md,.message.message-parent>.entry .md,.message.recipient>.entry,.report-modal .modal-header-title,.report-modal .report-form-bottom-panel .report-form-content-policy p,.report-modal .report-form-last-step-text-custom-rules,.report-modal .report-reason-item .report-reason-display,.search-result-group footer .info,.side .titlebox .md ul li,.titlebox,.titlebox h1 a,.trophy-name,.user-subreddit-public-description,body,html,main[role=main] h1,main[role=main] h2,main[role=main] p{color:var(--fg)}.Post__score,.TrendingProfilesSidebar__header,.TrophyCaseSidebar__header,.UserModeratedSubredditsSidebar__header,.UserSpecialsListSidebar__header,.author-tooltip__credentials-list strong,.brand,.dropdown.srdrop .selected,.gold-accent th,.gold-expiration-info .karma,.goldvertisement .progress p,.hover-bubble.multi-selector strong,.link .md :not(pre)>code,.link .rank,.listing-chooser h3,.search-result :link>mark,.search-result-group-header,.server-seconds em,.sidecontentbox .title h1,.titlebox h1 a:first-child,.wiki-page .wikititle,.wiki-page-content .md h1,.wiki-page-content .md h2,.wiki-page-content .md h3,.wiki-page-content .md h4,.wiki-page-content .md h5,aside.sidebar .md h2,aside.sidebar section,dl>dt,h1,h2,h3,h4,h5{color:var(--fg-2)}#sr-header-area .selected a,#sr-more-link,.Post__authorLink,.Post__authorLink:visited,.Post__flatListItem:link,.Post__source .Post__sourceLink,.Post__tagline,.ProfileSidebar__counters,.ProfileSidebar__description,.ProfileSidebar__displayName,.SubredditListItem__subscribers,.TrophyCaseSidebar__trophyDescription,.TrophyCaseSidebar__trophyTitle,.UserSpecialsListSidebar__date,.UserSpecialsListSidebar__title,.bottommenu,.dropdown.lightdrop .selected,.entry .buttons li a,.explore-comment .comment-link,.explore-item .explore-sr-details,.flat-list li.selected a,.flat-vert.title,.footer,.gray,.gray-buttons button,.listing-chooser li a .description,.liveupdate-listing li.liveupdate time,.md blockquote,.md del,.message .search-result-meta,.message .tagline,.message.message-parent.recipient>.entry .head,.message.message-parent>.entry .head,.message.message-reply.recipient>.entry .head,.multi-details h3,.muted,.nextprev,.preftable .details,.report-modal .report-form-block-user .report-form-action-desc p,.search-expando-button,.search-result .author:link,.search-result .search-subreddit-link:link,.search-result-meta,.separator,.sidebox .subtitle,.sidecontentbox .more a,.sr-list a:link,.sr-list a:link:hover,.sr-list a:visited,.subscription-box .title:visited,.tagline,.tagline .expand,.tagline>a.author,.titlebox .bottom,.titlebox form.toggle,.trophy-description,.user,.redditmod-visited a.title,aside.sidebar #discussions li,dl>dd,label.disabled,span.domain>a{color:var(--fg-muted)}.BlueBar__logout,.ProfileBarDropdown__title,.listing-chooser .create button,.liveupdate-listing li.liveupdate a.author,.liveupdate-listing li.liveupdate:hover time,.report-modal .report-form-bottom-panel .report-form-content-policy a,.search-result :link,.subscription-box .title,.tabmenu li.selected a,.tagline a.subreddit,.thing .title,a,a:link{color:var(--fg-link)}#sr-header-area .selected a:hover,.BlueBar__logout:hover,.Post__authorLink:hover,.Post__stickied-title:hover,.Post__subredditLink:hover,.ProfileBarDropdown__title.m-opened,.author-tooltip__head a:hover,.author-tooltip__link-list a:hover,.flat-list li.selected a:hover,.liveupdate-listing li.liveupdate:hover a.author,.report-modal .report-form-bottom-panel .report-form-content-policy a:hover,.search-result :link:hover,.sidecontentbox .more a:hover,.subscription-box .title:hover,.tabmenu li a:hover,.tagline a.subreddit,.thing .title:hover,a:hover,a:visited:hover{color:var(--fg-link-hover)}.search-result :link:visited,.tabmenu li a,.tagline a.subreddit:visited,.thing .title:visited,a:visited{color:var(--fg-link-visited)}.morelink>a,.spoiler-stamp,a.TrendingProfilesSidebar__follow,a.c-btn-primary{color:#fff}.tagline .submitter{color:#4af}.Post__moderatorFlair,.green,.tagline .moderator,.tagline .pinned-tagline,.tagline .stickied-tagline,.thing.stickied.link a.title,.thing.sticky-pinned.link a.title{color:#3a3}input[style*="color:black"]{color:var(--fg-input)!important}.trophy-name{color:var(--fg-2)!important}hr{color:var(--border-color)}#header,#liveupdate-statusbar.complete,#sr-header-area,.Post.m-profile,.Post__scoreDisplay,.content,.crosspost-preview,.explore-comment .explore-label,.explore-discovery .explore-label,.explore-hot .explore-label,.explore-rising .explore-label,.flair,.footer,.infobar.profilebetabar,.link,.linkflairlabel,.linkinfo,.navbar,.organic-listing,.permission-summary,.reddit-infobar,div.link.self,noborder{border:none}button.expand-summary,button.report-button,custom-border,hr,input,select{border:var(--border)}#compose-message .roundfield select,#header,#header hr,#sr-header-area,.BlueBar,.author-tooltip__credentials-list li,.author-tooltip__head,.author-tooltip_self .author-tooltip__head,.authorized-app,.c-form-control,.drop-choices,.explore-comment .comment,.filtered-details form.add-sr .sr-name,.footer .col,.formtabs-content,.hover-bubble,.hover-bubble.multi-selector .create-multi input[type=text],.infobar,.link .md pre,.link .usertext-body .md,.listing-chooser li,.listing-chooser li.selected,.liveupdate-listing li.separator::before,.md code,.md pre,.md td,.md th,.menuarea,.message.new>.entry,.morelink,.multi-details form.add-sr .sr-name,.nextprev a,.raisedbox,.report-modal .modal-content,.roundfield input[type=text],.roundfield textarea,.search-result-group-header,.sidebox.hohoho .morelink,.sidecontentbox .content,.subreddit .usertext .md,.subscription-box .box-separator,.titlebox .bottom,.wiki-page .pageactions,.wiki-page .wiki-page-content .wiki>.toc>ul,.wiki-page .wiki-page-content .wiki>.toc>ul ul,body.with-listing-chooser .listing-chooser .grippy,body.with-listing-chooser .listing-chooser .grippy::after,body.with-listing-chooser.listing-chooser-collapsed .listing-chooser .grippy::after,hr,input,select,textarea{border-color:var(--border-color)}.tabmenu li.selected a{border-color:transparent}#header-bottom-right{border-bottom:solid 1px var(--bg)}.goldvertisement a,.listing-chooser .create button{border-color:var(--fg-link)}.nextprev .separator{border-color:var(--fg-muted)}.author-tooltip.hover-bubble.anchor-bottom-left::before,.author-tooltip.hover-bubble.anchor-bottom-right::before{border-top-color:var(--bg-2)}.ProfileBarDropdown__title.m-arrow.m-opened::after,.ProfileBarDropdown__title.m-arrow::after,.TabMenu__tab.m-active::after{border-top-color:var(--fg-muted)}.comment .child{border-left:solid 5px var(--border-color)}.hover-bubble.anchor-right::after{border-left-color:var(--bg-2)}#liveupdate-statusbar.complete,success,success a{background-color:var(--bg-success);color:var(--fg-success)}.archived-infobar.with-icon::before,.content.submit .info-notice,.reddit-infobar,.reddit-infobar .md,info,info a{background-color:var(--bg-info);color:var(--fg-info)}.app-scope,warning{background-color:var(--bg-warning);color:var(--fg-warning)}error,error a{background-color:var(--bg-error);color:var(--fg-error)}disabled{background-color:var(--bg-disabled);color:var(--fg-disabled)}pre{background-color:var(--syntax-bg);color:var(--syntax-fg)}.infobar.profilebetabar,.organic-listing,.toc>ul{box-shadow:0 0 5px rgba(0,0,0,1)}.crosspost-preview,.listing-chooser li.selected{box-shadow:30px 0 30px -15px rgba(255,255,255,.1) inset,0 2px 6px -1px rgba(0,0,0,.2)}.goldvertisement,.report-flow-form.thing:hover{box-shadow:none;background-image:none}label.disabled{cursor:not-allowed}.subreddit-subscribe:before{filter:saturate(0) brightness(150%)}.subreddit-subscribe:hover:before{filter:saturate(100%)}.add-sr button.add,.multi-details button.add,.multi-details button.remove-sr.remove-sr,img[src*="start_chat.png"]{filter:invert(200%)}';
  73.  
  74. const CSS_NO_ADS = '.goldvertisement, .thing.promoted, [id^="ad_"], #siteTable_organic {display:none!important}';
  75. const CSS_NO_CHAT = '#chat-app, #chat, #chat + span {display:none!important}';
  76. const CSS_NO_SUBLIST = '.sr-list > *:not(:first-child) {display:none!important}';
  77. const CSS_NO_FOOTER = '.footer, .footer-parent {display:none!important}';
  78. const CSS_NO_HEADER_IMAGE = '#header-img, .hohoho {display:none!important} body.with-listing-chooser #header .pagename{position:relative; bottom:0}';
  79. const CSS_COLLAPSE_SIDEBAR = '.side{position:absolute;transition:right .2s ease-in-out;box-shadow:-5px 0 3px 0 rgba(0,0,0,.1);padding-left:10px;right:-305px;width:300px;z-index:1}' +
  80. 'body{overflow-x:hidden}' +
  81. '.side:hover{right:-5px}' +
  82. '.side .nub{opacity:0}' +
  83. '.side:hover .nub{opacity:1}';
  84. const CSS_DIM_GALLERY_NAV = '.gallery-nav-disabled{visibility:hidden}.media-gallery .gallery-nav-back{float:left;margin-left:20px;padding-left:20px}';
  85.  
  86. // Existing values are the *default*
  87. const config = {
  88. subreddits: [],
  89. filteredSubreddits: {},
  90. texts: [],
  91. filteredTexts: {},
  92. theme: 'obsidian',
  93. nsfwFilterState: 'show',
  94. themes: {
  95. none: '',
  96. obsidian: ':root{--fg:#E0E2E4;--bg:#293134;--bg-odd:#303a3d;--fg-2:#678CB1;--bg-2:#3F4B4E;--fg-muted:#999;--fg-input:#A7CCF1;--bg-input:#293134;--fg-input-hover:#E0E2E4;--bg-input-hover:#536364;--border:solid 0.5px #678CB1;--border-color:#678CB1;--fg-link:#EC7600;--fg-link-hover:#FC8610;--fg-link-visited:#BA4400;--fg-disabled:#ddd;--bg-disabled:#777;--fg-error:#fff;--fg-warning:#000;--fg-info:#fff;--fg-success:#fff;--bg-error:#600;--bg-warning:#fa4;--bg-info:#14a;--bg-success:#151;--syntax-fg:#F1F2F3;--syntax-fg-2:#E8E2B7;--syntax-bg:#293134;--syntax-bg-2:#3F4B4E;--syntax-comment:#66747B;--syntax-builtin:#93C763;--syntax-function:#678CB1;--syntax-keyword:#7CCADD;--syntax-number:#FFCD22;--syntax-string:#EC7600;--syntax-font-family:"Menlo","Liberation Mono","Consolas","DejaVu Sans Mono","Ubuntu Mono","Courier New","andale mono","lucida console",monospace;--selection-fg:#000}',
  97. monokai: ':root{--fg:#f8f8f2;--bg:#29281E;--bg-odd:#2f2f24;--fg-2:#66d9ef;--bg-2:#39382E;--fg-muted:#999;--fg-input:#fd971f;--bg-input:#2f2f24;--fg-input-hover:#fd971f;--bg-input-hover:#252520;--border:solid 0.5px #777;--border-color:#777;--fg-link:#f92672;--fg-link-hover:#d90652;--fg-link-visited:#fd971f;--fg-disabled:#ddd;--bg-disabled:#777;--fg-error:#fff;--fg-warning:#000;--fg-info:#fff;--fg-success:#fff;--bg-error:#600;--bg-warning:#fa4;--bg-info:#14a;--bg-success:#151;--syntax-fg:#f8f8f2;--syntax-fg-2:#f8f8f2;--syntax-bg:#272822;--syntax-bg-2:#373832;--syntax-comment:#75715e;--syntax-builtin:#f92672;--syntax-function:#a6e22e;--syntax-keyword:#76d9e6;--syntax-number:#ae81ff;--syntax-string:#e6db74;--syntax-font-family:"Menlo","Liberation Mono","Consolas","DejaVu Sans Mono","Ubuntu Mono","Courier New","andale mono","lucida console",monospace;--selection-fg:#000}',
  98. blue: ':root{--fg:#ccc;--bg:#192833;--bg-odd:#243237;--fg-2:#F39200;--bg-2:#293843;--fg-muted:#999;--fg-input:#fff;--bg-input:#334247;--fg-input-hover:#fff;--bg-input-hover:#394853;--border:solid 0.5px #495863;--border-color:#495863;--fg-link:#28c;--fg-link-hover:#6cf;--fg-link-visited:#8af;--fg-disabled:#ddd;--bg-disabled:#777;--fg-error:#fff;--fg-warning:#000;--fg-info:#fff;--fg-success:#fff;--bg-error:#600;--bg-warning:#fa4;--bg-info:#14a;--bg-success:#151;--syntax-fg:#839496;--syntax-fg-2:#eee8d5;--syntax-bg:#002b36;--syntax-bg-2:#073642;--syntax-comment:#75715e;--syntax-builtin:#268bd2;--syntax-function:#cb4b16;--syntax-keyword:#b58900;--syntax-number:#6c71c4;--syntax-string:#2aa198;--syntax-font-family:"Menlo","Liberation Mono","Consolas","DejaVu Sans Mono","Ubuntu Mono","Courier New","andale mono","lucida console",monospace;--selection-fg:#000}',
  99. black: ':root{--fg:#ccc;--bg:#101010;--bg-odd:#191919;--fg-2:#aab;--bg-2:#202020;--fg-muted:#999;--fg-input:#aaa;--bg-input:#000;--fg-input-hover:#fff;--bg-input-hover:#151515;--border:solid 0.5px #353535;--border-color:#353535;--fg-link:#369;--fg-link-hover:#47a;--fg-link-visited:#96a;--fg-disabled:#ddd;--bg-disabled:#777;--fg-error:#fff;--fg-warning:#000;--fg-info:#fff;--fg-success:#fff;--bg-error:#600;--bg-warning:#fa4;--bg-info:#14a;--bg-success:#151;--syntax-fg:#f5f5f5;--syntax-fg-2:#ffffff;--syntax-bg:#000000;--syntax-bg-2:#202020;--syntax-comment:#b0b0b0;--syntax-builtin:#fb0120;--syntax-function:#6fb3d2;--syntax-keyword:#d381c3;--syntax-number:#fc6d24;--syntax-string:#a1c659;--syntax-font-family:"Menlo","Liberation Mono","Consolas","DejaVu Sans Mono","Ubuntu Mono","Courier New","andale mono","lucida console",monospace;--selection-fg:#000}',
  100. dark: ':root{--fg:#E0E2E4;--bg:#121212;--bg-odd:#303a3d;--fg-2:#678CB1;--bg-2:#232323;--fg-muted:#999;--fg-input:#A7CCF1;--bg-input:#293134;--fg-input-hover:#E0E2E4;--bg-input-hover:#536364;--border:solid 0.5px #678CB1;--border-color:#678CB1;--fg-link:hsla(0,0%,100%,0.8);--fg-link-hover:hsla(0,0%,100%,0.9);--fg-link-visited:hsla(0,0%,100%,0.7);--fg-disabled:#ddd;--bg-disabled:#777;--fg-error:#fff;--fg-warning:#000;--fg-info:#fff;--fg-success:#fff;--bg-error:#600;--bg-warning:#fa4;--bg-info:#14a;--bg-success:#151;--syntax-fg:#F1F2F3;--syntax-fg-2:#E8E2B7;--syntax-bg:#293134;--syntax-bg-2:#3F4B4E;--syntax-comment:#66747B;--syntax-builtin:#93C763;--syntax-function:#678CB1;--syntax-keyword:#7CCADD;--syntax-number:#FFCD22;--syntax-string:#EC7600;--syntax-font-family:"Menlo","Liberation Mono","Consolas","DejaVu Sans Mono","Ubuntu Mono","Courier New","andale mono","lucida console",monospace;--selection-fg:#000}'
  101. },
  102. tweaks: {
  103. autoAlign: true,
  104. infiniteScrolling: true,
  105. loadPostsInline: true,
  106. noAds: true,
  107. noChat: true,
  108. noSublist: true,
  109. noFooter: true,
  110. noHeaderImage: true,
  111. collapseSidebar:true
  112. },
  113. visitedThings: {}
  114. };
  115.  
  116. return new Promise(resolve => {
  117. const keys = Object.keys(config);
  118. Promise.all(keys.map(key => GM.getValue('redditmod-' + key)))
  119. .then(values => {
  120. // Load config
  121. values.forEach((value, keyIndex) => {
  122. if (value !== undefined) {
  123. config[keys[keyIndex]] = JSON.parse(value);
  124. }
  125. });
  126.  
  127. // Clear visited for the past day
  128. flushVisited();
  129.  
  130. // Expose public methods
  131. resolve({
  132. getThemes, setTheme, getThemeCSS, getTheme,
  133. getTweaks, setTweak, getTweakCSS, isTweakEnabled,
  134. getSubreddits, setSubredditFilter, setAllSubredditFilters, isSubredditFiltered, removeSubreddit,
  135. getNsfwState, setNsfwState,
  136. getTexts, setTextFilter, isTextFiltered, removeText, shouldFilterTextblock,
  137. addVisitedThing, isVisited, flushVisited
  138. });
  139. });
  140. });
  141.  
  142. /* optionalKey: if given, only saves that key of the config. */
  143. function _save(optionalKey) {
  144. const keys = optionalKey !== undefined ? [optionalKey] : Object.keys(config);
  145. return new Promise(resolve => {
  146. Promise.all(keys.map(key => GM.setValue('redditmod-' + key, JSON.stringify(config[key]))))
  147. .then(resolve);
  148. });
  149. }
  150.  
  151. // ==============================================
  152. // Themes
  153. function getTheme() { return config.theme; }
  154. function getThemes() { return Object.keys(config.themes); }
  155.  
  156. function setTheme(theme) {
  157. const promises = [];
  158. theme = (theme === undefined) ? 'none' : theme;
  159. if (config.theme !== theme) {
  160. config.theme = theme;
  161. promises.push(_save('theme'));
  162. }
  163. return Promise.all(promises);
  164. }
  165.  
  166. function getThemeCSS() {
  167. if (config.theme === 'none' || config.themes[config.theme] === undefined) {
  168. return BASE_STYLE;
  169. } else {
  170. return config.themes[config.theme] + BASE_STYLE + THEMED_STYLE;
  171. }
  172. }
  173.  
  174. // =========================================
  175. // Tweaks
  176. function isTweakEnabled(tweakID) { return (config.tweaks[tweakID] === true); }
  177.  
  178. function getTweaks() {
  179. return [
  180. { name: 'Infinite Scrolling', id: 'infiniteScrolling', enabled: config.tweaks.infiniteScrolling, title: "Load next page when you reach the bottom" },
  181. { name: 'Load Pages Inline', id: 'loadPostsInline', enabled: config.tweaks.loadPostsInline, title: "Add the next page of posts to the bottom of the current page (ignored when 'Infinite Scrolling' is enabled)." },
  182. { name: 'Auto-align on expand', id: 'autoAlign', enabled: config.tweaks.autoAlign, title: "Scroll so the clicked post is at the top of the screen." },
  183. { name: 'No Ads', id: 'noAds', enabled: config.tweaks.noAds, title: "Don't display ads disguised as posts" },
  184. { name: 'No Chat', id: 'noChat', enabled: config.tweaks.noChat, title: "Don't show anything related to Reddit's Chat feature" },
  185. { name: 'No Sublist', id: 'noSublist', enabled: config.tweaks.noSublist, title: "Don't show subreddit list at top of page" },
  186. { name: 'No Footer', id: 'noFooter', enabled: config.tweaks.noFooter, title: "Don't show footer at bottom of page" },
  187. { name: 'No Header Image', id: 'noHeaderImage', enabled: config.tweaks.noHeaderImage, title: "Don't show reddit icon at top of page" },
  188. { name: 'Collapse Sidebar', id: 'collapseSidebar', enabled: config.tweaks.collapseSidebar, title: "Hide the sidebar -- visible on hover" }
  189. ];
  190. }
  191.  
  192. function getTweakCSS() {
  193. const results = [];
  194. if (config.tweaks.noAds) { results.push(CSS_NO_ADS); }
  195. if (config.tweaks.noChat) { results.push(CSS_NO_CHAT); }
  196. if (config.tweaks.noSublist) { results.push(CSS_NO_SUBLIST); }
  197. if (config.tweaks.noFooter) { results.push(CSS_NO_FOOTER); }
  198. if (config.tweaks.noHeaderImage) { results.push(CSS_NO_HEADER_IMAGE); }
  199. if (config.tweaks.collapseSidebar) { results.push(CSS_COLLAPSE_SIDEBAR); }
  200. results.push(CSS_DIM_GALLERY_NAV);
  201. return results.join('');
  202. }
  203.  
  204. function setTweak(tweakID, enabled) {
  205. const promises = [];
  206. config.tweaks[tweakID] = enabled;
  207. promises.push(_save('tweaks'));
  208. return Promise.all(promises);
  209. }
  210.  
  211. // ==============================================
  212. // Subreddit Filters
  213. function getSubreddits() { return config.subreddits; }
  214. function isSubredditFiltered(subreddit) { return (config.filteredSubreddits[subreddit] === true); }
  215.  
  216. function setAllSubredditFilters(enabled) {
  217. config.subreddits.forEach(subreddit => {
  218. config.filteredSubreddits[subreddit] = enabled;
  219. });
  220. return _save('filteredSubreddits');
  221. }
  222.  
  223. function setSubredditFilter(subreddit, enabled) {
  224. const promises = [];
  225. if (!config.subreddits.includes(subreddit)) {
  226. config.subreddits.push(subreddit);
  227. promises.push(_save('subreddits'));
  228. }
  229. if (config.filteredSubreddits[subreddit] !== enabled) {
  230. config.filteredSubreddits[subreddit] = enabled;
  231. promises.push(_save('filteredSubreddits'));
  232. }
  233. return Promise.all(promises);
  234. }
  235.  
  236. function removeSubreddit(subreddit) {
  237. const promises = [];
  238. if (config.subreddits.includes(subreddit)) {
  239. config.subreddits = config.subreddits.filter(sub => subreddit !== sub);
  240. promises.push(_save('subreddits'));
  241. }
  242. Promise.all(promises);
  243. }
  244.  
  245. // ==============================================
  246. // Text Filters
  247. function getTexts() { return config.texts; }
  248. function isTextFiltered(text) { return config.filteredTexts[text] === true; }
  249.  
  250. function shouldFilterTextblock(textblock) {
  251. const reasons = [];
  252. Object.keys(config.filteredTexts).forEach(filteredText => {
  253. if (isTextFiltered(filteredText) && textblock.toLowerCase().includes(filteredText.toLowerCase())) {
  254. reasons.push('TEXT("' + filteredText + '")');
  255. }
  256. });
  257. return reasons.length === 0 ? false : reasons.join(', ');
  258. }
  259.  
  260. function setTextFilter(text, enabled) {
  261. const promises = [];
  262. if (!config.texts.includes(text)) {
  263. config.texts.push(text);
  264. promises.push(_save('texts'));
  265. }
  266. if (config.filteredTexts[text] !== enabled) {
  267. config.filteredTexts[text] = enabled;
  268. promises.push(_save('filteredTexts'));
  269. }
  270. return Promise.all(promises);
  271. }
  272.  
  273. function removeText(text) {
  274. const promises = [];
  275. if (config.texts.includes(text)) {
  276. config.texts = config.texts.filter(txt => text !== txt);
  277. promises.push(_save('texts'));
  278. }
  279. Promise.all(promises);
  280. }
  281.  
  282. // ==============================================
  283. // Nsfw Filter
  284. function getNsfwState() {
  285. return config.nsfwFilterState;
  286. }
  287.  
  288. function setNsfwState(state) {
  289. config.nsfwFilterState = state;
  290. return _save('nsfwFilterState');
  291. }
  292.  
  293. // ==============================================
  294. // Visited
  295. function addVisitedThing(thingId) {
  296. config.visitedThings[thingId] = new Date().getTime();
  297. return _save('visitedThings');
  298. }
  299.  
  300. function isVisited(thingId) {
  301. return config.visitedThings[thingId] > 0;
  302. }
  303.  
  304. function flushVisited() {
  305. const yesterday = new Date().getTime() - 86400000;
  306. Object.entries(config.visitedThings)
  307. .filter(([thingId, timestamp]) => (timestamp < yesterday))
  308. .forEach(([thingId, timestamp]) => {
  309. delete config.visitedThings[thingId];
  310. });
  311. return _save('visitedThings');
  312. }
  313.  
  314. })();
  315.  
  316. /**
  317. * All things CSS & DOM (applying/removing CSS rules, waiting for DOM elements);
  318. */
  319. const CSS = (() => {
  320. const onHead = _onNode('head'); /* Wait for <head> to appear. */
  321. const onBody = _onNode('body'); /* Wait for <body> to appear. */
  322.  
  323. return {onDOM, onHead, applyStyle, removeStyle};
  324.  
  325. /** Promises a child node immediately under the root `document`. Waits if node is not found. */
  326. function _onNode(documentKey) {
  327. return new Promise(resolve => {
  328. const timer = setInterval(promiseNode, 50);
  329. function promiseNode() {
  330. if (document[documentKey]) {
  331. clearInterval(timer);
  332. resolve(document[documentKey]);
  333. }
  334. }
  335. });
  336. }
  337.  
  338. /** Promises /when/ the DOM is loaded & ready. */
  339. function onDOM() {
  340. return new Promise(resolve => {
  341. if (document.readyState === 'interactive' || document.readyState === 'complete') {
  342. resolve();
  343. } else {
  344. document.addEventListener('DOMContentLoaded', function(event) {
  345. document.removeEventListener('DOMContentLoaded', event.callee);
  346. resolve();
  347. });
  348. }
  349. });
  350. }
  351.  
  352. /**
  353. * Waits for <head> to appear, then applies CSS rules to the page.
  354. * Removes existing CSS Rules that match the given ID.
  355. * @param {String} id The identifier for this style.
  356. * @param {String} css The CSS rules in text/string format.
  357. * @return Promise for when style is applied.
  358. */
  359. function applyStyle(id, css) {
  360. return new Promise(resolve => {
  361. onHead.then(head => {
  362. let style = head.querySelector('.redditmod-style#' + id);
  363. if (style !== null && style.firstChild) {
  364. style.removeChild(style.firstChild);
  365. } else {
  366. style = document.createElement('style');
  367. Object.assign(style, { id:id, type:'text/css', className:'redditmod-style' });
  368. }
  369. style.appendChild(document.createTextNode(css));
  370. head.appendChild(style);
  371. resolve(head);
  372. });
  373. });
  374. }
  375.  
  376. /** @param {String} id The style ID, set when `applyStyle` is called. */
  377. function removeStyle(id) {
  378. const style = document.head.querySelector('style#' + id);
  379. if (style) style.parentNode.removeChild(style);
  380. }
  381. })();
  382.  
  383.  
  384. /** */
  385. const MENU = (() => {
  386. const MENU_STYLE_ID = 'redditmod-menu-style';
  387. const MENU_STYLE = '.redditmod-menu-header{margin:3px 5px 3px 5px}';
  388.  
  389. let isInitialized = false;
  390. let config;
  391. return {init};
  392.  
  393. function init(theConfig) {
  394. config = theConfig;
  395. return new Promise(resolve => {
  396. if (isInitialized) {
  397. resolve({updateMenuSection});
  398. } else {
  399. CSS.onDOM().then(() => {
  400. resetMenu();
  401. CSS.applyStyle(MENU_STYLE_ID, MENU_STYLE);
  402. isInitialized = true;
  403. resolve({updateMenuSection});
  404. });
  405. }
  406. });
  407. }
  408.  
  409. function resetMenu() {
  410. const menu = document.querySelector('.dropdown.srdrop .selected');
  411. if (menu) menu.textContent = 'OPTIONS';
  412. const dropdown = document.querySelector('.drop-choices.srdrop');
  413. if (dropdown) dropdown.innerHTML = '';
  414. }
  415.  
  416. function create(tag, props, parentToAppendTo) {
  417. const node = document.createElement(tag);
  418. if (props) Object.assign(node, props);
  419. if (parentToAppendTo) parentToAppendTo.appendChild(node);
  420. return node;
  421. }
  422.  
  423. function updateMenuSection(title, id, anchorsProps) {
  424. const dropdown = document.querySelector('.drop-choices.srdrop');
  425. if (!dropdown) return;
  426.  
  427. if (document.querySelector('.drop-choices.srdrop .redditmod-menu-header') !== null && document.querySelector('#' + id) === null) {
  428. create('hr', {className:'redditmod-menu-spacer'}, dropdown);
  429. }
  430.  
  431. const section = document.querySelector('#' + id) || create('div', {id}, dropdown);
  432. const header = section.querySelector('.redditmod-menu-header') || create('h3', {className:'redditmod-menu-header'}, section);
  433. const links = section.querySelector('.redditmod-menu-links') || create('div', {className:'redditmod-menu-links'}, section);
  434.  
  435. header.textContent = title;
  436. links.innerHTML = '';
  437. anchorsProps.forEach(props => {
  438. if (!props.href) props.href = '#';
  439. create('a', props, links).className = 'choice';
  440. });
  441. }
  442. })();
  443.  
  444. const MEDIA = (() => {
  445. const CLASS_MEDIA_EXPANDED = 'redditmod-media-expanded',
  446. CLASS_COMMENTS_EXPANDED = 'redditmod-comments-expanded',
  447. CLASS_MEDIABOX = 'redditmod-media-box',
  448. CLASS_COMMENTS = 'redditmod-comments-box',
  449. CLASS_MEDIA = 'redditmod-media',
  450. CLASS_SPINNER = 'redditmod-media-spinner',
  451. CLASS_ERROR = 'redditmod-media-error',
  452. MEDIA_STYLE_ID = 'redditmod-media-style',
  453. MEDIA_STYLE_CSS = '.redditmod-media-box, .redditmod-comments-box{max-height:0; height:0; overflow:hidden; transition: height linear 0.2s, max-height linear 0.2s}' +
  454. '.redditmod-media{}' + // ???
  455. '.redditmod-media-expanded .redditmod-media-box, .redditmod-comments-expanded .redditmod-comments-box{max-height:10000px; height:auto}' +
  456. '.redditmod-media-spinner,.redditmod-media-spinner:after{border-radius:50%;width:10em;height:10em}.redditmod-media-spinner{margin:60px auto;font-size:10px;position:relative;text-indent:-9999em;border-top:1.1em solid rgba(255,255,255,.2);border-right:1.1em solid rgba(255,255,255,.2);border-bottom:1.1em solid rgba(255,255,255,.2);border-left:1.1em solid #fff;-webkit-transform:translateZ(0);-ms-transform:translateZ(0);transform:translateZ(0);-webkit-animation:load8 1.1s infinite linear;animation:load8 1.1s infinite linear}@-webkit-keyframes load8{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes load8{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}' +
  457. '.redditmod-media-error{background-color:rgba(255,0,0,0.3); color:#fff; text-shadow:1px 1px #000}';
  458.  
  459. CSS.applyStyle(MEDIA_STYLE_ID, MEDIA_STYLE_CSS);
  460.  
  461. return {postClick};
  462.  
  463. function postClick(thing, event) {
  464. const target = event.target;
  465. const thingAnchor = thing.querySelector('a.title');
  466. const thingHref = thing.getAttribute('data-url');
  467. const thingUrl = thingHref || thingAnchor && thingAnchor.href;
  468.  
  469. if (!thing.classList.contains('redditmod-visited')) {
  470. thing.classList.add('redditmod-visited');
  471. CONFIG.then(config => config.addVisitedThing(thing.dataset.fullname));
  472. }
  473.  
  474. if (target.classList.contains('gallery-navigation')) {
  475. return true;
  476. } else if (target.tagName === 'IMG') {
  477. if (target.parentNode.tagName === 'A' && target.parentNode.classList.contains('gallery-item-thumbnail-link')) {
  478. if (target.hasAttribute('oldwidth')) {
  479. return true;
  480. } else {
  481. var href = target.parentNode.getAttribute('href');
  482. href = href.replace('preview.redd.it', 'i.redd.it');
  483. href = href.replace(/\?.*/g, '');
  484. target.parentNode.setAttribute('href', href);
  485. target.src = href;
  486. target.setAttribute('oldwidth', target.width);
  487. target.setAttribute('oldheight', target.height);
  488. target.removeAttribute('width');
  489. target.removeAttribute('height');
  490. }
  491. }
  492. event.stopPropagation();
  493. event.preventDefault();
  494. return false;
  495. } else if (target.tagName === 'A' && target.classList.contains('comments')) {
  496. stopEvent(event);
  497. _fetchCommentsbox(thing, target.href)._toggle();
  498. return false;
  499.  
  500. } else if (_shouldUseExpando(thing, thingUrl)) {
  501. if (target.classList.contains('expando-button')) {
  502. return true;
  503. }
  504. const expandoButton = thing.querySelector('.expando-button');
  505. if (expandoButton) {
  506. expandoButton.click(event);
  507. return false;
  508. }
  509. else {
  510. const commentTarget = thing.querySelector('.comments');
  511. if (commentTarget) {
  512. _fetchCommentsbox(thing, commentTarget.href)._toggle();
  513. }
  514. }
  515. return true;
  516.  
  517. } else if ((target.tagName === 'A') ||
  518. (target.tagName === 'VIDEO' && target.controls === 'true') ||
  519. (target.classList.contains('expando-button'))) {
  520. return true; // Pass-through
  521.  
  522. } else {
  523. _fetchMediabox(thing, thingUrl).click(event); // Fetch/Contruct mediabox & click it.
  524. return false;
  525. }
  526. }
  527.  
  528. function _shouldUseExpando(thing, thingUrl) {
  529. var result = false;
  530. if (thing.classList.contains('self')) {
  531. result = true;
  532. } else if (/https?:\/\/(\w+\.)*(v\.redd\.it|clips\.twitch\.tv|reddituploads\.com|vimeo\.com|youtube\.com|youtu\.be|old\.reddit\.com|reddit.com)\/.*$/.test(thingUrl)) {
  533. result = true;
  534. } else if (/$\/r\/.*$/.test(thingUrl)) {
  535. result = true;
  536. }
  537. debug('_shouldUseExpando(thing=' + thing + ', thingUrl=' + thingUrl + ') = ' + result);
  538. return result;
  539. }
  540.  
  541. function _fetchCommentsbox(thing, thingUrl) {
  542. const existing = thing.querySelector('.' + CLASS_COMMENTS);
  543. if (existing) {
  544. return existing;
  545. }
  546.  
  547. const commentsBox = _create('div', {className:CLASS_COMMENTS}, thing);
  548. commentsBox.onclick = stopEvent;
  549. commentsBox._toggle = () => {
  550. thing.classList.remove(CLASS_MEDIA_EXPANDED);
  551. thing.classList.toggle(CLASS_COMMENTS_EXPANDED);
  552. };
  553.  
  554. const spinner = _create('div', {className:CLASS_SPINNER}, commentsBox);
  555. RedditCommentsPromise(thingUrl)
  556. .then(container => {
  557. commentsBox.removeChild(spinner);
  558. commentsBox.appendChild(container);
  559. }).catch(reason => {
  560. debug('RedditCommentsPromise rejected, reason:', reason);
  561. });
  562.  
  563. return commentsBox;
  564. }
  565.  
  566. function _fetchMediabox(thing, thingUrl) {
  567. // Use existing mediabox
  568. const existingMediabox = thing.querySelector('.' + CLASS_MEDIABOX);
  569. if (existingMediabox) return existingMediabox;
  570.  
  571. // Create a custom mediabox
  572. const mediaBox = _create('div', {className:CLASS_MEDIABOX}, thing);
  573. mediaBox.onclick = event => {
  574. stopEvent(event);
  575. thing.classList.remove(CLASS_COMMENTS_EXPANDED);
  576. thing.classList.toggle(CLASS_MEDIA_EXPANDED);
  577. const media = thing.querySelector('.' + CLASS_MEDIA);
  578. if (thing.classList.contains(CLASS_MEDIA_EXPANDED)) {
  579. if (media && media._onshow) media._onshow();
  580. } else {
  581. if (media && media._onhide) media._onhide();
  582. }
  583. CONFIG.then(config => {
  584. if (config.isTweakEnabled('autoAlign')) {
  585. thing.scrollIntoView({block: 'start', inline: 'nearest', behavior:'smooth'});
  586. }
  587. })
  588. };
  589.  
  590. const spinner = _create('div', {className:CLASS_SPINNER}, mediaBox);
  591.  
  592. mediaBoxPromise(thingUrl)
  593. .then(media => {
  594. media.classList.add(CLASS_MEDIA);
  595. mediaBox.removeChild(spinner);
  596. mediaBox.appendChild(media);
  597. }).catch(reason => {
  598. debug('mediaboxPromise rejected, reason:', reason);
  599. mediaBox.removeChild(spinner);
  600. const errorBox = _create('div', {className:CLASS_ERROR}, mediaBox);
  601. errorBox.textContent = reason;
  602. });
  603. return mediaBox;
  604. }
  605.  
  606. function mediaBoxPromise(urlText) {
  607. return new Promise((resolve, reject) => {
  608. const url = new URL(urlText);
  609. const domain = url.hostname.replace(/^(?:.*\.)?(\w+\.\w+)$/, '$1');
  610. let promise;
  611. switch(domain) {
  612. case 'imgur.com': promise = ImgurMediaPromise(url); break;
  613. case 'gfycat.com': promise = GfycatMediaPromise(url); break;
  614. case 'soundcloud.com': promise = SoundcloudPromise(url); break;
  615. case 'explosm.net': promise = ExplosmPromise(url); break;
  616. case 'imgflip.com': promise = ImgflipPromise(url); break;
  617. case 'streamable.com': promise = StreamablePromise(url); break;
  618. case 'instagram.com': promise = InstagramPromise(url); break;
  619. case 'deviantart.com': promise = DeviantartPromise(url); break;
  620. case 'xkcd.com': promise = XkcdPromise(url); break;
  621. case 'redgifs.com': promise = RedgifsPromise(url); break;
  622. case 'reddit.com': promise = RedditPromise(url); break;
  623. default: promise = GenericMediaPromise(url); break;
  624. }
  625. promise.then(resolve).catch(reject);
  626. });
  627. }
  628.  
  629. function _create(tag, props, parentNode) {
  630. const node = document.createElement(tag);
  631. if (props) {
  632. Object.assign(node, props);
  633. }
  634. if (parentNode) {
  635. parentNode.appendChild(node);
  636. }
  637. return node;
  638. }
  639.  
  640. function _getJSON(url, headers, use_GM_XHR) {
  641. return new Promise((resolve, reject) => {
  642. if (!headers) {
  643. headers = {'Accept': 'application/json'};
  644. } else if (!headers.Accept) {
  645. headers.Accept = 'application/json';
  646. }
  647. _getWEB(url, headers, use_GM_XHR)
  648. .then(responseText => {
  649. try {
  650. resolve(JSON.parse(responseText));
  651. } catch (err) {
  652. debug('_getJSON error. url:', url.toString(), 'responseText', responseText);
  653. reject('[MEDIA._getJSON] Error parsing JSON text from ' + url.toString() + ' : ' + responseText);
  654. }
  655. })
  656. .catch(reason => {
  657. reject('[Media._getJSON] Error fetching JSON from url ' + url.toString() + ' reason: ' + reason);
  658. });
  659. });
  660. }
  661.  
  662. function _getWEB(url, headers, use_GM_XHR) {
  663. return new Promise(function(resolve, reject) {
  664. if (!headers) {
  665. headers = {};
  666. }
  667. if (use_GM_XHR) {
  668. debug('MEDIA._getWEB() via GM.xmlHttpRequest(), fetching url:', url, 'with headers', headers);
  669. GM.xmlHttpRequest({
  670. method: 'GET',
  671. url: url,
  672. headers: headers,
  673. onabort: onError,
  674. onerror: onError,
  675. onload: function(response) {
  676. debug('MEDIA._getWEB() got GM.xmlHttpRequest response (' + response.responseText.length.toLocaleString() + ' bytes)');
  677. resolve(response.responseText);
  678. }
  679. });
  680. } else {
  681. debug('MEDIA._getWEB() via XMLHttpRequest(), fetching url:', url, 'with headers', headers);
  682. const xhr = new XMLHttpRequest();
  683. xhr.onload = function(response) {
  684. debug('MEDIA._getWEB() got response (' + response.target.responseText.length.toLocaleString() + ' bytes)');
  685. resolve(response.target.responseText);
  686. };
  687. xhr.onerror = onError;
  688. xhr.onabort = onError;
  689. xhr.open('GET', url, true);
  690. Object.keys(headers).forEach(key => {
  691. xhr.setRequestHeader(key, headers[key]);
  692. });
  693. xhr.send(null);
  694. }
  695.  
  696. function onError(reason) {
  697. debug('MEDIA._getWEB error. url:', url.toString(), 'reason:', reason);
  698. reject('[MEDIA._getWEB] Error fetching data from ' + url + ' Reason: ' + reason.error);
  699. }
  700. });
  701. }
  702.  
  703. function _getDocument(url, headers, use_GM_XHR) {
  704. return new Promise((resolve, reject) => {
  705. _getWEB(url, headers, use_GM_XHR)
  706. .then(responseText => {
  707. const html = document.createElement('html');
  708. html.innerHTML = responseText;
  709. resolve(html);
  710. })
  711. .catch(reject);
  712. });
  713. }
  714.  
  715. function GenericMediaPromise(url) {
  716. return new Promise((resolve, reject) => {
  717. const urlText = url.toString();
  718. if (/\.(jpe?g|gif|png|mp4)(\?.*)?$/i.test(urlText)) {
  719. GenericImagePromise([urlText]).then(resolve).catch(reject);
  720. } else {
  721. PostlightMediaPromise(urlText).then(resolve).catch(reject);
  722. }
  723. });
  724. }
  725.  
  726. function PostlightMediaPromise(url) {
  727. return new Promise((resolve, reject) => {
  728. const postlightUrl = 'https://mercury.postlight.com/parser?url=' + encodeURIComponent(url);
  729. const headers = {'X-api-key': 'NtFdFjTYzQXF4WUWBivfsnTj0zXZyvwCKbSQeuAB'};
  730. _getJSON(postlightUrl, headers, true)
  731. .then(resp => {
  732. if (resp.content === '<body></body>') {
  733. reject('[PostlightMediaProimse] Postlight returned empty content for ' + url);
  734. } else {
  735. resolve(_create('div', {innerHTML:resp.content, className:'redditmod-media-other'}));
  736. }
  737. })
  738. .catch(reject);
  739. });
  740. }
  741.  
  742. function GenericImagePromise(urls) {
  743. return new Promise((resolve, reject) => {
  744. if (urls.length === 0) {
  745. debug('GenericImagePromise error. No URLs given:', urls);
  746. reject('[GenericImagePromise] No URLs given');
  747. return;
  748. }
  749. let index = 0;
  750. const container = _create('div');
  751. const nav = _create('div', {}, container);
  752. const navPrev = _create('a', {textContent:'<',href:'#'}, nav);
  753. const navCurrent = _create('span', {textContent:'1'}, nav);
  754. const navSep = _create('span', {textContent:'/'}, nav);
  755. const navTotal = _create('span', {textContent:urls.length}, nav);
  756. const navNext = _create('a', {textContent:'>',href:'#'}, nav);
  757. const imageDiv = _create('div', {}, container);
  758. const image = _create('img', {
  759. src: urls[0],
  760. style: 'max-width:100%; max-height:100%; object-fit:scale-down'
  761. }, imageDiv);
  762. if (urls.length === 1) {
  763. nav.style.display = 'none';
  764. } else {
  765. container._click = navNext.click;
  766. }
  767. navPrev.onclick = event => {
  768. stopEvent(event);
  769. if (--index < 0) index = urls.length - 1;
  770. navCurrent.textContent = (index + 1).toString().padStart(urls.length.toString().length, '0');
  771. image.removeAttribute('src');
  772. image.src = urls[index];
  773. return false;
  774. };
  775. navNext.onclick = event => {
  776. stopEvent(event);
  777. if (++index === urls.length) index = 0;
  778. navCurrent.textContent = (index + 1).toString().padStart(urls.length.toString().length, ' ');
  779. image.removeAttribute('src');
  780. setTimeout(() => {
  781. image.src = urls[index];
  782. }, 10);
  783. return false;
  784. };
  785. resolve(container);
  786. });
  787. }
  788.  
  789. function GenericVideoPromise(urls) {
  790. return new Promise(resolve => {
  791. const video = _create('video', {
  792. controls: false,
  793. autoplay: true,
  794. loop: true,
  795. style: 'display:block; width:' + (window.innerWidth-75) + 'px; height:' + (window.innerHeight-75) + 'px',
  796. });
  797. video._onhide = video.pause;
  798. video._onshow = video.load;
  799. urls.forEach(url => _create('source', {src:url}, video));
  800. resolve(video);
  801. });
  802. }
  803.  
  804. function ImgurMediaPromise(url) {
  805. return new Promise((resolve, reject) => {
  806. let urlText = url.toString();
  807. const albumIdMatches = urlText.match(/imgur\.com\/(?:gallery|a|r\/[^\/]+)\/(\w+)/i);
  808. debug('ImgurMediaPromise: url: ' + url + ', albumIdMatches:', albumIdMatches);
  809. if (albumIdMatches) {
  810. // Album: Extract images
  811. _getJSON('https://imgur.com/ajaxalbums/getimages/' + albumIdMatches[1] + '/hit.json', {}, true)
  812. .then(resp => {
  813. if (!resp.data.images) {
  814. debug('ImgurMediaPromise: No resp.data.images, assuming single image:', albumIdMatches[1] + '.jpg');
  815. GenericImagePromise(['https://i.imgur.com/' + albumIdMatches[1] + '.jpg']).then(resolve).catch(reject);
  816. } else {
  817. debug('ImgurMediaPromise: Got ' + resp.data.images.length + ' images from album');
  818. GenericImagePromise(resp.data.images.map(image => 'https://i.imgur.com/' + image.hash + image.ext)).then(resolve).catch(reject);
  819. }
  820. });
  821. } else if (/\.(gifv?|mp4)$/.test(url)) {
  822. // Video
  823. debug('ImgurMediaPromise: gifv/mp4');
  824. urlText = urlText.replace(/\.gifv?$/, '.mp4');
  825. GenericVideoPromise([urlText]).then(resolve).catch(reject);
  826. } else {
  827. // Image?
  828. urlText = urlText.replace(/[^/]*\.imgur\.com/, 'i.imgur.com');
  829. urlText = urlText.replace(/\/\w+\//, '');
  830. urlText = urlText.replace(/_[a-z]\./, '.');
  831. urlText = urlText.replace(/\.(gif|jpg|jpeg|png)$/i, '');
  832. urlText = urlText + '.jpg';
  833. debug('ImgurMediaPromise: Unknown type, generated/mangled URL:', urlText);
  834. GenericImagePromise([urlText]).then(resolve).catch(reject);
  835. }
  836. });
  837. }
  838.  
  839. function GfycatMediaPromise(url) {
  840. return new Promise((resolve, reject) => {
  841. let gfycatUrl = url.href;
  842. const shortCode = url.href.match(/gfycat\.com\/(?:.*\/)?([a-z0-9]*)/i);
  843. if (shortCode) {
  844. gfycatUrl = 'https://gfycat.com/' + shortCode[1];
  845. }
  846. _getDocument(gfycatUrl, {}, true)
  847. .then(doc => {
  848. let video = doc.querySelector('#mp4Source');
  849. if (!video) {
  850. video = doc.querySelector('.video.media source[type="video/mp4"]');
  851. }
  852. if (!video || !video.src) {
  853. reject('[GfycatMediaPromise] Could not find video at ' + gfycatUrl + ' body: ' + doc.innerHTML);
  854. } else {
  855. GenericVideoPromise([video.src]).then(resolve);
  856. }
  857. })
  858. .catch(reject);
  859. });
  860. }
  861.  
  862. function RedgifsPromise(url) {
  863. debug('RedgifsPromise(url);', url);
  864. return new Promise((resolve, reject) => {
  865. let redgifsUrl = url.href;
  866. _getDocument(redgifsUrl, {}, true)
  867. .then(doc => {
  868. let source = doc.querySelector('meta[property="og:video"]');
  869. debug('[RedgifsPromise] source =', source);
  870. if (!source) {
  871. reject('[RedgifsPromise] Could not find video at ' + redgifsUrl + ' body: ' + doc.innerHTML);
  872. } else {
  873. debug('[RedgifPromise] source.content =', source.content);
  874. GenericVideoPromise([source.content]).then(resolve);
  875. }
  876. })
  877. .catch(reject);
  878. });
  879. }
  880.  
  881. function RedditPromise(url) {
  882. const urlText = url.toString();
  883. return new Promise((resolve, reject) => {
  884. if (/reddit\.com.*\/comments\/.*/.test(urlText)) {
  885. RedditCommentsPromise(urlText).then(resolve).catch(reject);
  886. } else {
  887. debug('RedditPromise error. Unable to load non-comments Reddit page', urlText);
  888. reject('[RedditPromise] Unable to load non-comments Reddit page ' + urlText);
  889. }
  890. });
  891. }
  892.  
  893. function RedditCommentsPromise(url) {
  894. return new Promise((resolve, reject) => {
  895. _getDocument(url, {}, false)
  896. .then(doc => {
  897. const commentArea = doc.querySelector('.commentarea .sitetable');
  898. const container = _create('div', {className:'redditmod-media-comments-area'});
  899. commentArea.querySelectorAll('.entry').forEach(entry => {
  900. entry.onclick = event => {
  901. if (event && event.target && event.target.tagName === 'A') {
  902. return true; // Pass-through
  903. } else {
  904. // Expand/collapse comment tree
  905. stopEvent(event);
  906. entry.querySelector('a.expand').click();
  907. return false;
  908. }
  909. };
  910. });
  911. container.appendChild(commentArea);
  912. container._click = () => {};
  913. resolve(container);
  914. })
  915. .catch(reject);
  916. });
  917. }
  918.  
  919. function SoundcloudPromise(url) {
  920. return new Promise((resolve, reject) => {
  921. _getWEB(url.toString(), {}, true)
  922. .then(responseText => {
  923. let matches = responseText.match(/meta itemprop="embedUrl" content="([^"]*)"/);
  924. if (!matches) {
  925. matches = responseText.match(/meta property="twitter:player" content="([^"]*)"/);
  926. }
  927. if (matches && matches.length > 0) {
  928. const iframe = _create('iframe', {
  929. style: 'width:100%; height:' + (window.innerHeight / 2) + 'px',
  930. src: matches[1]
  931. });
  932. resolve(iframe);
  933. } else {
  934. reject('[SoundcloudPromise] No soundcloud data found at ' + url.toString());
  935. }
  936. })
  937. .catch(reject);
  938. });
  939. }
  940.  
  941. function ExplosmPromise(url) {
  942. return new Promise(function(resolve, reject) {
  943. _getDocument(url.toString(), {}, true)
  944. .then(doc => {
  945. const imageMeta = doc.querySelector('img#main-comic');
  946. if (imageMeta) {
  947. GenericImagePromise([imageMeta.src]).then(resolve, reject);
  948. } else {
  949. reject('[ExplosmPromise] No images found at ' + url.toString());
  950. }
  951. })
  952. .catch(reject);
  953. });
  954. }
  955.  
  956. function ImgflipPromise(url) {
  957. return new Promise(function(resolve, reject) {
  958. const urlText = url.toString();
  959. if (/\.(jpg|jpeg|png)$/i.test(urlText)) {
  960. GenericImagePromise([urlText]).then(resolve).catch(reject);
  961. }
  962. _getDocument(urlText, {}, true)
  963. .then(doc => {
  964. const imageMeta = doc.querySelector('img#im');
  965. if (imageMeta) {
  966. GenericImagePromise([imageMeta.src]).then(resolve, reject);
  967. } else {
  968. reject('[ImgflipPromise] No images found at ' + urlText);
  969. }
  970. })
  971. .catch(reject);
  972. });
  973. }
  974.  
  975. function StreamablePromise(url) {
  976. return new Promise((resolve, reject) => {
  977. const matches = url.href.match(/streamable\.com\/([a-zA-Z0-9]*)/);
  978. if (!matches) reject('[StreamablePromise] No Streamable ID found in url ' + url.toString());
  979. const apiUrl = 'https://api.streamable.com/videos/' + matches[1];
  980. _getJSON(apiUrl, {}, true)
  981. .then(json => {
  982. GenericVideoPromise([json.files.mp4.url]).then(resolve, reject);
  983. }).catch(reject);
  984. });
  985. }
  986.  
  987. function InstagramPromise(url) {
  988. const urlText = url.toString();
  989. const matches = urlText.match(/instagram\.com\/p\/([a-zA-Z0-9_\-]*)/);
  990. const apiUrl = 'https://instagram.com/p/' + matches[1] + '/';
  991. return new Promise((resolve, reject) => {
  992. if (!matches) {
  993. reject('[InstagramPromise] InstagramPromise error. No images found at', urlText);
  994. return;
  995. }
  996. _getDocument(apiUrl, {}, true).then(doc => {
  997. const videoMeta = doc.querySelector('meta[property="og:video"]');
  998. const imageMeta = doc.querySelector('meta[property="og:image"]');
  999. if (videoMeta) {
  1000. GenericVideoPromise([videoMeta.content]).then(resolve, reject);
  1001. } else if (imageMeta) {
  1002. GenericImagePromise([imageMeta.content]).then(resolve, reject);
  1003. } else {
  1004. reject('[InstagramPromise] Error: No images found at ' + apiUrl);
  1005. }
  1006. }).catch(reject);
  1007. });
  1008. }
  1009.  
  1010. function DeviantartPromise(url) {
  1011. return new Promise(function(resolve, reject) {
  1012. _getDocument(url.toString(), {}, true).then(doc => {
  1013. const fullImg = doc.querySelector('img[dev-content-full]');
  1014. const smallImg = doc.querySelector('meta[property="og:image"]');
  1015. if (fullImg) {
  1016. GenericImagePromise([fullImg.src]).then(resolve, reject);
  1017. } else if (smallImg) {
  1018. GenericImagePromise([smallImg.content]).then(resolve, reject);
  1019. } else {
  1020. reject('[DeviantartPromise] Error: No images found at ' + url.toString());
  1021. }
  1022. }).catch(reject);
  1023. });
  1024. }
  1025.  
  1026. function XkcdPromise(url) {
  1027. return new Promise((resolve, reject) => {
  1028. const matches = url.href.match(/xkcd\.com\/([0-9]+)/);
  1029. if (matches) {
  1030. _getJSON("https://xkcd.com/" + matches[1] + "/info.0.json", {}, true).then(json => {
  1031. const xkcdDiv = document.createElement("div");
  1032. const h3 = document.createElement("h3");
  1033. const img = document.createElement("img");
  1034. const h5 = document.createElement("h5");
  1035. h3.textContent = json.title;
  1036. img.src = json.img;
  1037. img.title = json.alt;
  1038. h5.textContent = json.alt;
  1039. xkcdDiv.appendChild(h3);
  1040. xkcdDiv.appendChild(img);
  1041. xkcdDiv.appendChild(h5);
  1042. resolve(xkcdDiv);
  1043. }).catch(reject)
  1044. } else if (/\.(png|gif|jpe?g)$/i.test(url.href)) {
  1045. GenericImagePromise([url.toString()]).then(resolve).catch(reject);
  1046. } else {
  1047. reject('Failed to find XKCD metadata from ' + url.toString());
  1048. }
  1049. });
  1050. }
  1051. })();
  1052.  
  1053. function Filters(config, menuPromise) {
  1054. const CLASS_PROCESSED = 'redditmod-thing-processed';
  1055. const CLASS_FILTERED = 'redditmod-thing-filtered';
  1056.  
  1057. // CSS specific to filtering
  1058. const CSS_FILTER = '' +
  1059. '.redditmod-thing-filtered {' +
  1060. 'margin:0!important;' +
  1061. 'padding:0!important;' +
  1062. 'max-height:0!important;' +
  1063. 'overflow:hidden!important;' +
  1064. 'background-color:var(--bg)' +
  1065. '}' +
  1066. '.thing {' +
  1067. 'transition:background-color linear 0.2s' +
  1068. '}' +
  1069. '.redditmod-filter-subreddit-button {' +
  1070. 'border:none;' +
  1071. 'background-color:var(--fg-link);' +
  1072. 'color:var(--bg)!important;' +
  1073. 'font-weight:900;' +
  1074. 'border-radius:15px;' +
  1075. 'padding:0 1.9px .5px;' +
  1076. 'margin-left:5px' +
  1077. '}' +
  1078. '.redditmod-filter-subreddit-button:hover {' +
  1079. 'text-decoration:none!important;' +
  1080. 'box-shadow: 0 0 5px rgba(0,0,0,0.5)' +
  1081. '}';
  1082.  
  1083. // Only apply filters on URLs that match:
  1084. const URL_FILTER_REGEX = /^.*\/r\/(all|popular)\/.*$/i;
  1085. const filters = {};
  1086.  
  1087. return new Promise(resolve => {
  1088. CSS.applyStyle('redditmod-thing-style', CSS_FILTER);
  1089. menuPromise.then(menu => {
  1090. filters.nsfw = NsfwFilter(menu);
  1091. filters.text = TextFilter(menu);
  1092. filters.subreddit = SubredditFilter(menu);
  1093. processPosts();
  1094. resolve({filters, processPost, processPosts});
  1095. });
  1096. });
  1097.  
  1098. function processPosts() {
  1099. document.querySelectorAll('#siteTable .thing').forEach(thing => processPost(thing));
  1100. }
  1101.  
  1102. function processPost(thing) {
  1103. if (!thing.classList.contains(CLASS_PROCESSED)) {
  1104. thing.classList.add(CLASS_PROCESSED);
  1105. filters.subreddit.addFilterButton(thing);
  1106.  
  1107. // Hide/Show media when post is clicked
  1108. thing.addEventListener('click', event => MEDIA.postClick(thing, event));
  1109.  
  1110. // Visited
  1111. if (config.isVisited(thing.dataset.fullname)) {
  1112. thing.classList.add('redditmod-visited');
  1113. }
  1114.  
  1115. // Remove tracking from links if user did not specify in preferences.
  1116. thing.querySelectorAll('a[data-outbound-url], a[data-outbound-expiration], a[data-inbound-url]')
  1117. .forEach(anchor => {
  1118. if (anchor.dataset) {
  1119. if (anchor.dataset.outboundUrl) delete anchor.dataset.outboundUrl;
  1120. if (anchor.dataset.outboundExpiration) delete anchor.dataset.outboundExpiration;
  1121. if (anchor.dataset.inboundUrl) delete anchor.dataset.inboundUrl;
  1122. }
  1123. });
  1124. }
  1125.  
  1126. if (_shouldFilterThisPage()) {
  1127. const reasons = [];
  1128. const activeFilters = Object.entries(filters).map(
  1129. ([name, filter]) => {
  1130. const filterReason = filter.shouldFilter(thing);
  1131. if (filterReason !== false) {
  1132. reasons.push(filterReason);
  1133. }
  1134. });
  1135.  
  1136. if (reasons.length > 0 && !thing.classList.contains(CLASS_FILTERED)) {
  1137. thing.classList.add(CLASS_FILTERED);
  1138. } else if (reasons.length === 0 && thing.classList.contains(CLASS_FILTERED)) {
  1139. thing.classList.remove(CLASS_FILTERED);
  1140. }
  1141. }
  1142. }
  1143.  
  1144. function _shouldFilterThisPage() {
  1145. return URL_FILTER_REGEX.test(window.location.pathname);
  1146. }
  1147.  
  1148. ///////////////////////////////////////////
  1149. function NsfwFilter(menu) {
  1150. updateMenu();
  1151. return {shouldFilter};
  1152.  
  1153. function shouldFilter(thing) {
  1154. const state = config.getNsfwState();
  1155. const isOver18 = thing.classList.contains('over18');
  1156. let reason = false;
  1157. if (state === 'hide' && isOver18) {
  1158. reason = 'NSFW';
  1159. } else if (state === 'always' && !isOver18) {
  1160. reason = 'Not NSFW';
  1161. }
  1162. return reason;
  1163. }
  1164.  
  1165. function updateMenu() {
  1166. const submenus = [_nsfwMenu('hide'), _nsfwMenu('show'), _nsfwMenu('always')];
  1167. menu.updateMenuSection('nsfw filter', 'redditmod-menu-filter-nsfw', submenus);
  1168. }
  1169.  
  1170. // `verb` is either "hide" or "show"
  1171. function _nsfwMenu(state) {
  1172. const currentState = config.getNsfwState();
  1173. const isChecked = currentState === state;
  1174.  
  1175. let msg;
  1176. switch (state) {
  1177. case 'hide':
  1178. msg = 'hide all nsfw posts';
  1179. break;
  1180. case 'show':
  1181. msg = 'allow nsfw posts';
  1182. break;
  1183. case 'always':
  1184. msg = 'only show nsfw posts';
  1185. break;
  1186. }
  1187. return {
  1188. href:'#',
  1189. innerHTML: (isChecked ? '&#9745; ' : '&#9744; ') + msg,
  1190. onclick: _onNsfwMenuClick
  1191. };
  1192. function _onNsfwMenuClick(event) {
  1193. stopEvent(event);
  1194. config.setNsfwState(state);
  1195. updateMenu();
  1196. processPosts();
  1197. }
  1198. }
  1199. }
  1200.  
  1201. ///////////////////////////////////////////
  1202. function SubredditFilter(menu) {
  1203. updateMenu();
  1204. return {shouldFilter, addFilterButton};
  1205.  
  1206. function addFilterButton(thing) {
  1207. const subreddit = thing && thing.dataset && thing.dataset.subreddit;
  1208. const subredditLink = thing.querySelector('a.subreddit');
  1209. if (!subreddit || !subredditLink) return;
  1210. const filterButton = document.createElement('a');
  1211. Object.assign(filterButton, {
  1212. innerHTML: '&times;',
  1213. href: '#',
  1214. className: 'redditmod-filter-subreddit-button',
  1215. title: 'Filter /r/' + subreddit + ' from appearing',
  1216. onclick: event => _toggleSubreddit(subreddit, event)
  1217. });
  1218. subredditLink.parentNode.appendChild(filterButton);
  1219. }
  1220.  
  1221. function shouldFilter(thing) {
  1222. let reason = false;
  1223. if (config.isSubredditFiltered(thing.dataset.subreddit)) {
  1224. reason = 'SUBREDDIT';
  1225. }
  1226. return reason;
  1227. }
  1228.  
  1229. function updateMenu() {
  1230. const subreddits = config.getSubreddits();
  1231. const submenus = subreddits.map(subreddit => _subredditMenuProps(subreddit));
  1232. submenus.unshift({
  1233. textContent: 'select none',
  1234. onclick: event => _toggleAllSubreddits(false, event)
  1235. });
  1236. submenus.unshift({
  1237. textContent: 'select all',
  1238. onclick: event => _toggleAllSubreddits(true, event)
  1239. });
  1240. menu.updateMenuSection(
  1241. 'filtered subreddits (' + subreddits.length + ')',
  1242. 'redditmod-menu-filter-subreddits',
  1243. submenus);
  1244. }
  1245.  
  1246. function refresh() {
  1247. updateMenu();
  1248. processPosts();
  1249. }
  1250.  
  1251. function _toggleSubreddit(subreddit, event) {
  1252. stopEvent(event);
  1253. const enabled = !config.isSubredditFiltered(subreddit);
  1254. config.setSubredditFilter(subreddit, enabled).then(refresh);
  1255. }
  1256.  
  1257. function _toggleAllSubreddits(enabled, event) {
  1258. stopEvent(event);
  1259. config.setAllSubredditFilters(enabled).then(refresh);
  1260. }
  1261.  
  1262. function _subredditMenuProps(subreddit) {
  1263. return {
  1264. innerHTML: (config.isSubredditFiltered(subreddit) ? '&#9745; ' : '&#9744; ') + subreddit,
  1265. onclick: event => _toggleSubreddit(subreddit, event)
  1266. };
  1267. }
  1268. }
  1269.  
  1270. ///////////////////////////////////////////
  1271. function TextFilter(menu) {
  1272. updateMenu();
  1273. return {shouldFilter};
  1274.  
  1275. function shouldFilter(thing) {
  1276. let reason = false;
  1277. const title = thing.querySelector('a.title');
  1278. if (title) {
  1279. reason = config.shouldFilterTextblock(title.textContent);
  1280. }
  1281. return reason;
  1282. }
  1283.  
  1284. function updateMenu() {
  1285. const texts = config.getTexts();
  1286. const submenus = texts.map(text => _textMenuProps(text));
  1287. submenus.unshift({
  1288. textContent: 'add a text filter',
  1289. onclick: _addTextFilter
  1290. });
  1291. menu.updateMenuSection('filtered text (' + texts.length + ')', 'redditmod-menu-filter-texts', submenus);
  1292. }
  1293.  
  1294. function refresh() {
  1295. updateMenu();
  1296. processPosts();
  1297. }
  1298.  
  1299. function _addTextFilter(event) {
  1300. stopEvent(event);
  1301. const text = prompt(
  1302. '"Text Filter" will hide reddit posts that contain a word or phrase in the post title.' +
  1303. '\n\nYou can enter multiple words/phrases by separating them with a Comma ( , )' +
  1304. '\n\n(Text-matching is case-insensitive)' +
  1305. '\n\nEnter text to filter:', 'Example: trump, sanders, clinton');
  1306. if (text === null) return;
  1307. text.split(/\s*,\s/).forEach(word => {
  1308. config.setTextFilter(word.toLowerCase(), true);
  1309. });
  1310. refresh();
  1311. }
  1312.  
  1313. function _textMenuProps(text) {
  1314. return {
  1315. innerHTML: (config.isTextFiltered(text) ? '&#9745; ' : '&#9744; ') + text,
  1316. onclick: event => {
  1317. stopEvent(event);
  1318. _toggleText(text);
  1319. }
  1320. };
  1321. }
  1322.  
  1323. function _toggleText(text) {
  1324. const enabled = !config.isTextFiltered(text);
  1325. config.setTextFilter(text, enabled);
  1326. refresh();
  1327. }
  1328. }
  1329. }
  1330.  
  1331. function Navigation(filters) {
  1332. let loading = false;
  1333.  
  1334. CSS.onDOM().then(() => {
  1335. overrideNextButton();
  1336. scrollListener();
  1337. addScrollListener();
  1338. });
  1339.  
  1340. return {scrollListener, addScrollListener};
  1341.  
  1342. function scrollListener(event) {
  1343. const evt = event || {pageY:0};
  1344. if (document.body.clientHeight - (window.scrollY + window.innerHeight) < 200) {
  1345. CONFIG.then(config => {
  1346. if (config.isTweakEnabled('infiniteScrolling')) {
  1347. loadMorePosts();
  1348. }
  1349. });
  1350. }
  1351. }
  1352.  
  1353. function addScrollListener() { window.addEventListener('scroll', scrollListener); }
  1354. function removeScrollListener() { window.removeEventListener('scroll', scrollListener); }
  1355.  
  1356. function loadMorePosts() {
  1357. const nextButton = document.querySelector('.next-button a');
  1358. if (loading || !nextButton) {
  1359. return;
  1360. }
  1361.  
  1362. loading = true;
  1363. removeScrollListener();
  1364.  
  1365. debug('[Navigation.loadMorePosts] Fetching URL (via nextButton.href):', nextButton.href);
  1366. const xhr = new XMLHttpRequest();
  1367. xhr.onload = function(response) {
  1368. _injectPosts(response.target);
  1369. };
  1370. xhr.onerror = _onError;
  1371. xhr.onabort = _onError;
  1372. xhr.open('GET', nextButton.href, true);
  1373. xhr.send(null);
  1374. /*GM.xmlHttpRequest({
  1375. method: "GET",
  1376. url: nextButton.href,
  1377. onabort: _onError,
  1378. onerror: _onError,
  1379. onload: _injectPosts
  1380. });*/
  1381.  
  1382. const parentNode = nextButton.parentNode.parentNode;
  1383. parentNode.style.backgroundColor = '#aaa';
  1384. parentNode.opacity = '0.5';
  1385. parentNode.cursor = 'not-allowed';
  1386. parentNode.childNodes.forEach(child => {
  1387. if (child.style) {
  1388. child.style.display = 'none';
  1389. }
  1390. });
  1391.  
  1392. function _onError(response) {
  1393. debug('Navigation.loadMorePosts() error. url:', nextButton.href, 'response:', response);
  1394. const errDiv = document.createElement('div');
  1395. errDiv.className = 'redditmod-media-error';
  1396. errDiv.textContent = 'Navigation.loadMorePosts() error. URL: ' + nextButton.href + ' Reason: ' + (response.error || response.responseText);
  1397. nextButton.parentNode.parentNode.appendChild(errDiv);
  1398. }
  1399. }
  1400.  
  1401. function _injectPosts(response) {
  1402. debug('[Navigation.loadMorePosts._injectPosts] Got response (' + response.responseText.length.toLocaleString() + ' bytes)');
  1403. const previousNav = document.querySelector('.nav-buttons');
  1404. const html = document.createElement('html');
  1405. html.innerHTML = response.responseText;
  1406. html.querySelectorAll('#siteTable > *').forEach(thing => {
  1407. if (thing.classList.contains('clearleft')) return;
  1408. if (!thing.id) {
  1409. previousNav.parentNode.insertBefore(thing, previousNav);
  1410. } else if (!document.querySelector('#' + thing.id)) {
  1411. filters.processPost(thing);
  1412. previousNav.parentNode.insertBefore(thing, previousNav);
  1413. } else {
  1414. debug('[Navigation.loadMorePosts._injectPosts] Ignoring duplicate post. thing.id:', thing.id);
  1415. }
  1416. });
  1417. previousNav.parentNode.removeChild(previousNav);
  1418.  
  1419. // Re-enable features on the "new page".
  1420. overrideNextButton();
  1421. addScrollListener();
  1422. loading = false;
  1423. setTimeout(scrollListener, 250);
  1424. }
  1425.  
  1426. function overrideNextButton() {
  1427. const nextButton = document.querySelector('.next-button a');
  1428. if (!nextButton) return;
  1429. nextButton.addEventListener('click', event => {
  1430. CONFIG.then(config => {
  1431. if (config.isTweakEnabled('loadPostsInline')) {
  1432. stopEvent(event);
  1433. loadMorePosts();
  1434. }
  1435. });
  1436. });
  1437. }
  1438. }
  1439.  
  1440. function Tweaks(config, menuPromise) {
  1441. const TWEAK_STYLE_ID = 'redditmod-tweak-style';
  1442.  
  1443. return new Promise(resolve => {
  1444. // Apply tweaks CSS before MENU loads
  1445. _applyTweakCSS();
  1446.  
  1447. menuPromise.then(menu => {
  1448. updateMenu(menu);
  1449. resolve({
  1450. updateMenu: () => updateMenu(menu)
  1451. });
  1452. });
  1453. });
  1454.  
  1455. function _applyTweakCSS() {
  1456. CSS.applyStyle(TWEAK_STYLE_ID, config.getTweakCSS());
  1457. }
  1458.  
  1459. function _tweakLinkProps(tweak, menu) {
  1460. return {
  1461. innerHTML: (tweak.enabled ? '&#9745; ' : '&#9744; ') + tweak.name,
  1462. title: tweak.title,
  1463. onclick: event => {
  1464. stopEvent(event);
  1465. config.setTweak(tweak.id, !config.isTweakEnabled(tweak.id))
  1466. .then(() => {
  1467. _applyTweakCSS();
  1468. updateMenu(menu);
  1469. });
  1470. }
  1471. };
  1472. }
  1473.  
  1474. function updateMenu(menu) {
  1475. if (!menu) return;
  1476. menu.updateMenuSection(
  1477. 'tweaks', 'redditmod-menu-tweaks',
  1478. config.getTweaks().map(tweak => _tweakLinkProps(tweak, menu))
  1479. );
  1480. }
  1481. }
  1482.  
  1483. function Themes(config, menuPromise) {
  1484. const THEME_STYLE_ID = 'redditmod-theme-style';
  1485.  
  1486. return new Promise(resolve => {
  1487. // Apply theme CSS before MENU loads
  1488. applyThemeCSS();
  1489.  
  1490. menuPromise.then(menu => {
  1491. updateMenu(menu);
  1492. resolve({
  1493. applyThemeCSS,
  1494. updateMenu: () => updateMenu(menu)
  1495. });
  1496. });
  1497. });
  1498.  
  1499. function applyThemeCSS() {
  1500. CSS.applyStyle(THEME_STYLE_ID, config.getThemeCSS());
  1501. }
  1502.  
  1503. function updateMenu(menu) {
  1504. if (!menu) return;
  1505. const submenus = config.getThemes().map(themeName => ({
  1506. innerHTML: (config.getTheme() === themeName ? '&#9745; ' : '&#9744; ') + themeName,
  1507. onclick: event => {
  1508. stopEvent(event);
  1509. config.setTheme(themeName).then(() => {
  1510. applyThemeCSS();
  1511. updateMenu(menu);
  1512. });
  1513. }
  1514. }));
  1515. menu.updateMenuSection('themes', 'redditmod-menu-themes', submenus);
  1516. }
  1517. }
  1518.  
  1519. //debug('Startup: config');
  1520. CONFIG.then(config => {
  1521. //debug('Startup: menuPromise (config:', config, ')');
  1522. const menuPromise = MENU.init(config);
  1523.  
  1524. //debug('Startup: theme, tweak, & filters (menuPromise:', menuPromise, ')');
  1525. Promise.all([
  1526. Themes(config, menuPromise),
  1527. Tweaks(config, menuPromise),
  1528. Filters(config, menuPromise)
  1529. ]).then(([themes, tweaks, filters]) => {
  1530. //debug('Startup: navigation (themes:', themes, 'tweaks:', tweaks, 'filters', filters, ')');
  1531. const navigation = Navigation(filters);
  1532. //debug('Startup: COMPLETE (navigation:', navigation, ')');
  1533. });
  1534.  
  1535. });
  1536. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址