Pawoo Always Show Spoilers

Automatically shows spoilers on media when browsing Pawoo through the web client.

  1. // ==UserScript==
  2. // @name Pawoo Always Show Spoilers
  3. // @namespace https://github.com/TaleirOfDeynai/
  4. // @version 1.0
  5. // @description Automatically shows spoilers on media when browsing Pawoo through the web client.
  6. // @author Taleir
  7. // @match https://pawoo.net/*
  8. // @grant none
  9. // @run-at document-start
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // Helper functions.
  16. function logError(ex) {
  17. console.error(`[UserScript:Pawoo Always Show Spoilers] Encountered an error; ${ex.message}`);
  18. console.error(ex);
  19. }
  20.  
  21. function isDetached(node) {
  22. while (node) {
  23. if (node === document.documentElement) return false;
  24. node = node.parentNode;
  25. }
  26. return true;
  27. }
  28.  
  29. function createTrial(fn) {
  30. return function(arg) {
  31. try { fn(arg); }
  32. catch (ex) { logError(ex); }
  33. };
  34. }
  35.  
  36. // Helper classes.
  37. class Observable {
  38. constructor(source) {
  39. this._source = Observable.toSource(source);
  40. }
  41.  
  42. static toSource(obj) {
  43. switch (true) {
  44. case typeof obj.forEach === "function":
  45. return obj.forEach.bind(obj);
  46. case typeof obj === "function":
  47. return obj;
  48. default:
  49. return (iterator) => iterator(obj);
  50. }
  51. }
  52.  
  53. static join(...args) {
  54. args = args.map(Observable.toSource);
  55. return new Observable(iterator => args.forEach(source => source(iterator)));
  56. }
  57.  
  58. forEach(fn) {
  59. try { this._source(fn); }
  60. catch (ex) { logError(ex); }
  61. return this;
  62. }
  63.  
  64. map(fn) {
  65. var mappedSource = (iterator) => this._source(val => iterator(fn(val)));
  66. return new Observable(mappedSource);
  67. }
  68.  
  69. flat() {
  70. var flattenedSource = (iterator) => this._source(val => {
  71. Observable.toSource(val)(iterator);
  72. });
  73. return new Observable(flattenedSource);
  74. }
  75.  
  76. flatMap(fn) {
  77. var flatMappedSource = (iterator) => this._source(val => {
  78. Observable.toSource(fn(val))(iterator);
  79. });
  80. return new Observable(flatMappedSource);
  81. }
  82.  
  83. collect(fn) {
  84. var collectedSource = (iterator) => this._source(val => {
  85. var result = fn(val);
  86. if (typeof result !== "undefined") iterator(result);
  87. });
  88. return new Observable(collectedSource);
  89. }
  90.  
  91. filter(fn) {
  92. var filteredSource = (iterator) => this._source(val => fn(val) && iterator(val));
  93. return new Observable(filteredSource);
  94. }
  95.  
  96. zip(fn) {
  97. var zippedSource = (iterator) => this._source(left => {
  98. Observable.toSource(fn(left))(right => {
  99. iterator(Array.isArray(left) ? [...left, right] : [left, right]);
  100. });
  101. });
  102. return new Observable(zippedSource);
  103. }
  104. }
  105.  
  106. class NodeObserver {
  107. constructor(parent, selector) {
  108. this.disconnected = false;
  109. this._callbacks = [];
  110. this._nodes = new Set();
  111.  
  112. var addNode = (added) => {
  113. if (this._nodes.has(added)) return;
  114. this._callbacks.forEach(fn => fn(added));
  115. this._nodes.add(added);
  116. };
  117.  
  118. var tryAddNode = (added) => {
  119. if (!added.matches) return;
  120. if (added.matches(selector)) return addNode(added);
  121. added.querySelectorAll(selector).forEach(addNode);
  122. };
  123.  
  124. // Attach to all existing nodes that match.
  125. parent.querySelectorAll(selector).forEach(addNode);
  126.  
  127. this._observer = new MutationObserver(mutations => {
  128. mutations.forEach(mutation => {
  129. mutation.addedNodes.forEach(tryAddNode);
  130. if (mutation.removedNodes.length > 0)
  131. for (var node of this._nodes)
  132. if (isDetached(node))
  133. this._nodes.delete(node);
  134. });
  135. });
  136.  
  137. this._observer.observe(parent, { childList: true, subtree: true });
  138. }
  139.  
  140. disconnect() {
  141. if (this.disconnected) return;
  142. this._observer.disconnect();
  143. this._callbacks = [];
  144. this._nodes = new Set();
  145. this.disconnected = true;
  146. }
  147.  
  148. forEach(fn) {
  149. if (this.disconnected) return;
  150. fn = createTrial(fn);
  151. this._nodes.forEach(fn);
  152. this._callbacks.push(fn);
  153. }
  154.  
  155. toObservable() {
  156. return new Observable(this);
  157. }
  158. }
  159.  
  160. // Inject style for revealed articles.
  161. var styleNode = document.createElement("style");
  162. styleNode.innerHTML = `
  163. .app-body > .app-holder article .status__wrapper.PASS_ext__revealed,
  164. .app-body > .app-holder .detailed-status__wrapper .status.PASS_ext__revealed,
  165. .app-body > .app-holder .detailed-status__wrapper .detailed-status.PASS_ext__revealed,
  166. .container.pawoo-wide .entry.PASS_ext__revealed {
  167. position: relative;
  168. }
  169.  
  170. .PASS_ext__revealed > * {
  171. background-color: transparent !important;
  172. }
  173.  
  174. .PASS_ext__revealed::before {
  175. background-color: rgba(255, 0, 0, 0.05);
  176. content: '';
  177. display: block;
  178. width: 100%;
  179. height: 100%;
  180. position: absolute;
  181. left: 0;
  182. top: 0;
  183. pointer-events: none;
  184. }
  185. `;
  186. document.head.appendChild(styleNode);
  187.  
  188. // Actual work of the script starts here.
  189. var applyModifications = (tuple) => {
  190. tuple[0].setAttribute("data-pass-ext-visited", "");
  191. tuple[0].classList.add("PASS_ext__revealed");
  192. tuple[1].click();
  193. };
  194.  
  195. // Observe on the main application entry points.
  196. var app = new NodeObserver(document.documentElement, `.app-body > .app-holder`).toObservable();
  197. var act = new NodeObserver(document.documentElement, `.container.pawoo-wide`).toObservable();
  198.  
  199. // Observe on the main app's articles list.
  200. var appArticles = app.flatMap(node => new NodeObserver(node, `.item-list[role="feed"] article .status__wrapper`));
  201.  
  202. // Observe on the main app's detailed view.
  203. var appView = app.flatMap(node => new NodeObserver(node, `.detailed-status__wrapper`));
  204. var appViewStatus = appView.flatMap(node => new NodeObserver(node, `.status`));
  205. var appViewDetail = appView.flatMap(node => new NodeObserver(node, `.detailed-status`));
  206.  
  207. // Observe on the activity stream entries and detailed views.
  208. var actEntries = act.flatMap(node => new NodeObserver(node, `.h-feed .entry`));
  209. var actDetail = act.flatMap(node => new NodeObserver(node, `.h-entry .entry`));
  210.  
  211. // Apply the modifications.
  212. Observable.join(appArticles, appViewStatus, appViewDetail, actEntries, actDetail)
  213. .zip(node => new NodeObserver(node, `button.media-spoiler`))
  214. .filter(tuple => !tuple[0].hasAttribute("data-pass-ext-visited"))
  215. .forEach(applyModifications);
  216. })();