V2ex supper helper

Make v2ex easyer to use

  1. // ==UserScript==
  2. // @name V2ex supper helper
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1
  5. // @description Make v2ex easyer to use
  6. // @author You
  7. // @match https://v2ex.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=v2ex.com
  9. // @grant GM_addStyle
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @run-at document-start
  13. // @license MIT
  14. // ==/UserScript==
  15. class PageManager {
  16. constructor() {
  17. this.conditionalWorks = [];
  18. this.activeConditionWork = null;
  19. setInterval(() => {
  20. this.conditionalWorks.forEach(cw => {
  21. cw.conditionWork.report();
  22. });
  23. }, 1000);
  24. }
  25.  
  26. stage(name) {
  27. var conditionalWork = new ConditionalWork(name);
  28. this.conditionalWorks.push({
  29. name: name,
  30. conditionWork: conditionalWork,
  31. });
  32. return conditionalWork;
  33. }
  34.  
  35. off(name) {
  36. var check = this.conditionalWorks.find(cw => cw.name === name);
  37. if (check) {
  38. check.conditionWork.off();
  39. }
  40. }
  41. }
  42.  
  43. class ConditionalWork {
  44.  
  45. constructor(name) {
  46. this.__name = name;
  47. this.__conditionFn = null;
  48. this.__actionFn = null;
  49. this.__running = false;
  50. this.__timeDuration = 0;
  51. this.__intervalMethod = 'interval';
  52. this.__handlerId = null;
  53. this.__logLevel = 0;
  54. this.__log = console.log;
  55. }
  56.  
  57. log = (message, level) => {
  58. if (isNaN(level)) level = 1;
  59. if (this.__logLevel > 0 && level >= this.__logLevel) {
  60. this.__log(message);
  61. }
  62. }
  63.  
  64. __checkBeforeRun() {
  65. if (!this.__conditionFn) {
  66. throw new Error("Condition function must be defined");
  67. }
  68. if (!this.__timeDuration) {
  69. throw new Error(`${this.__intervalMethod} must be defined`);
  70. }
  71. }
  72.  
  73. __complete() {
  74. this.__running = false;
  75. this.__handlerId = null;
  76. }
  77.  
  78. logLevel(level) {
  79. this.__logLevel = level;
  80. return this;
  81. }
  82.  
  83. asTimeout(timeout) {
  84. if (this.__intervalMethod === 'interval') {
  85. // initial set for timeout
  86. if (isNaN(timeout)) {
  87. throw new Error("Timeout must be a number");
  88. } else {
  89. if (timeout < 1) {
  90. throw new Error("timeout must be greater than 0");
  91. } else {
  92. timeout = Math.floor(timeout);
  93. }
  94. }
  95. } else {
  96. throw new Error("Interval already set");
  97. }
  98. this.__timeDuration = timeout;
  99. this.__intervalMethod = 'timeout';
  100. return this;
  101. }
  102.  
  103. on(conditionFn) {
  104. if (typeof conditionFn !== "function") {
  105. throw new Error("Condition function must be a function");
  106. }
  107. if (typeof this.__conditionFn === "function") {
  108. throw new Error("Condition function already defined");
  109. }
  110. this.__conditionFn = conditionFn;
  111. return this;
  112. }
  113.  
  114. act(actionFn) {
  115. // on must be called before act
  116. if (typeof this.__conditionFn !== "function") {
  117. throw new Error("Condition function must be defined first");
  118. }
  119. if (typeof actionFn !== "function") {
  120. throw new Error("Action function must be a function");
  121. }
  122. this.__actionFn = actionFn;
  123. return this;
  124. }
  125.  
  126. every(interval) {
  127. if (!isNaN(this.__timeDuration) && this.__timeDuration !== 0) {
  128. throw new Error("Interval already set");
  129. }
  130.  
  131. if (isNaN(interval)) {
  132. throw new Error("Interval must be a number");
  133. } else {
  134. if (interval < 1) {
  135. throw new Error("Interval must be greater than 0");
  136. } else {
  137. interval = Math.floor(interval);
  138. }
  139. }
  140.  
  141. this.__timeDuration = interval;
  142. return this;
  143. }
  144.  
  145. __hanlder = (cancelFn) => {
  146. let shouldContinue = false;
  147. if (this.__conditionFn()) {
  148. shouldContinue = this.__actionFn(this.log);
  149. } else {
  150. shouldContinue = true;
  151. }
  152. if (shouldContinue === false) {
  153. cancelFn(this.__handlerId);
  154. this.__complete();
  155. }
  156. }
  157.  
  158. __timeoutHanlder = () => {
  159. this.__hanlder(clearTimeout);
  160. setTimeout(() => {
  161. this.__timeoutHanlder();
  162. }, this.__timeDuration);
  163. }
  164.  
  165. __intervalHandler = () => {
  166. if (!this.__handlerId) {
  167. this.__handlerId = setInterval(this.__intervalHandler, this.__timeDuration);
  168. } else {
  169. this.__hanlder(clearInterval);
  170. }
  171. }
  172.  
  173. run = () => {
  174. // check basic configurations: interval, conditionFn, isRunning
  175. this.__checkBeforeRun();
  176. if (this.__running) {
  177. this.log("Work is already running", 1);
  178. return;
  179. }
  180. switch (this.__intervalMethod) {
  181. case 'interval':
  182. this.__intervalHandler();
  183. this.__running = true;
  184. break;
  185. case 'timeout':
  186. this.__timeoutHanlder();
  187. this.__running = true;
  188. default:
  189. throw new Error("Interval method not defined");
  190. }
  191. }
  192.  
  193. report() {
  194. if (this.__running) {
  195. this.log(`${this.__name} is running`, 1);
  196. } else {
  197. this.log(`${this.__name} is not running`, 2);
  198. }
  199. }
  200. }
  201.  
  202. (function () {
  203. // 'use strict';
  204. GM_addStyle(`
  205. .cell.read {
  206. background-color: antiquewhite;
  207. }
  208. `);
  209.  
  210. function getWebsite() {
  211. // Get website domain
  212. return window.location.hostname;
  213. }
  214.  
  215. function getJsonDataObject() {
  216. return JSON.parse(GM_getValue(getWebsite(), '{}'));
  217. }
  218.  
  219. function setUrlRead(url) {
  220. // Set url as read
  221. var data = getJsonDataObject();
  222. data[url] = true;
  223. GM_setValue(getWebsite(), JSON.stringify(data));
  224. }
  225.  
  226. function isUrlRead(url) {
  227. // Check if url is read
  228. var data = getJsonDataObject();
  229. return data[url] === true;
  230. }
  231.  
  232. function getMainPageCells() {
  233. // Get main page cells
  234. return document.querySelectorAll('#Main div.cell.item');
  235. }
  236.  
  237. function getTopicsPageCells() {
  238. // Get topics page cells
  239. return document.querySelectorAll('#TopicsNode .cell');
  240. }
  241.  
  242. function getCells() {
  243. const mainCells = getMainPageCells();
  244. const topicsCells = getTopicsPageCells();
  245. if (mainCells.length > 0) {
  246. return mainCells;
  247. }
  248. if (topicsCells.length > 0) {
  249. return topicsCells;
  250. }
  251. return [];
  252. }
  253.  
  254. function findLinks() {
  255. return Array.from(getCells()).map((cell) => { return cell.querySelector('.item_title a').href; });
  256. }
  257.  
  258. var pm = new PageManager();
  259.  
  260. const condition = () => {
  261. return findLinks().length > 0;
  262. }
  263.  
  264. // record current page as read
  265. setUrlRead(window.location.href);
  266.  
  267. pm.stage('Add color to viewd topics')
  268. .on(condition)
  269. .logLevel(1)
  270. .act((logger) => {
  271. logger('Add color to viewd topics');
  272. // find all cells
  273. var cells = getCells();
  274. // add color to read topics
  275. Array.from(cells).forEach((cell) => {
  276. const link = cell.querySelector('.item_title a').href;
  277. logger(`${link} is ${isUrlRead(link) ? 'read' : 'unread'}`);
  278. if (isUrlRead(link)) {
  279. cell.classList.add('read');
  280. }
  281. });
  282. logger('Done', 2);
  283. return false;
  284. })
  285. .every(1000)
  286. .run();
  287.  
  288. })();