[GMT] Ignored forums and threads

Hide threads not interested in from unread forum posts listing by one click

  1. // ==UserScript==
  2. // @name [GMT] Ignored forums and threads
  3. // @namespace https://greasyfork.org/users/321857-anakunda
  4. // @version 1.01.0
  5. // @description Hide threads not interested in from unread forum posts listing by one click
  6. // @author Anakunda
  7. // @copyright 2021, Anakunda (https://greasyfork.org/users/321857-anakunda)
  8. // @license GPL-3.0-or-later
  9. // @match https://*/forums.php
  10. // @match https://*/forums.php?action=unread
  11. // @match https://*/forums.php?action=unread&page=*
  12. // @match https://*/forums.php?page=*&action=unread
  13. // @match https://*/forums.php?action=viewforum&*
  14. // @match https://*/forums.php?page=*&action=viewforum&*
  15. // @grant GM_getValue
  16. // @grant GM_setValue
  17. // @grant GM_deleteValue
  18. // @grant GM_registerMenuCommand
  19. // ==/UserScript==
  20.  
  21. 'use strict';
  22.  
  23. function hasStyleSheet(name) {
  24. if (name) name = name.toLowerCase(); else throw 'Invalid argument';
  25. const hrefRx = new RegExp('\\/' + name + '\\b', 'i');
  26. if (document.styleSheets) for (let styleSheet of document.styleSheets)
  27. if (styleSheet.title && styleSheet.title.toLowerCase() == name) return true;
  28. else if (styleSheet.href && hrefRx.test(styleSheet.href)) return true;
  29. return false;
  30. }
  31. const isLightTheme = ['postmod', 'shiro', 'layer_cake', 'proton', 'red_light'].some(hasStyleSheet);
  32. if (isLightTheme) console.log('Light Gazelle theme detected');
  33. const isDarkTheme = ['kuro', 'minimal', 'red_dark'].some(hasStyleSheet);
  34. if (isDarkTheme) console.log('Dark Gazelle theme detected');
  35.  
  36. try { var ignoredThreads = GM_getValue('ignored_threads', { }) } catch(e) { ignoredThreads = { } }
  37. if (!Array.isArray(ignoredThreads[document.location.hostname])) ignoredThreads[document.location.hostname] = [ ];
  38. try { var ignoredForums = GM_getValue('ignored_forums', { }) } catch(e) { ignoredForums = { } }
  39. if (!Array.isArray(ignoredForums[document.location.hostname])) ignoredForums[document.location.hostname] = [ ];
  40.  
  41. console.info(ignoredThreads[document.location.hostname].length, 'ignored threads in total');
  42. console.info(ignoredForums[document.location.hostname].length, 'ignored forums in total');
  43. const ignOpacity = GM_getValue('ignored_opacity', 0.5);
  44. const ignColor = 'grey';
  45. const params = new URLSearchParams(document.location.search);
  46. let action = params.get('action');
  47. if (!action) {
  48. for (let tr of document.body.querySelectorAll('table.forum_index > tbody > tr:not(.colhead)')) {
  49. let forumId = tr.querySelector('td > h4 > a');
  50. if (forumId != null) forumId = new URLSearchParams(forumId.search); else continue; // assertion failed!
  51. if (!(forumId = parseInt(forumId.get('forumid')))) continue; // assertion failed!
  52. if (ignoredForums[document.location.hostname].includes(forumId)) tr.style.opacity = ignOpacity;
  53. }
  54. return;
  55. } else action = action.toLowerCase();
  56.  
  57. function getThreadId(tr) {
  58. if (!(tr instanceof HTMLTableRowElement)) return undefined;
  59. let a = tr.children[1].getElementsByTagName('a');
  60. return a.length > 0 ? parseInt(new URLSearchParams(a[0].search).get('threadid')) : undefined;
  61. }
  62. function getForumId(tr) {
  63. if (!(tr instanceof HTMLTableRowElement)) return undefined;
  64. let a = tr.children[0].getElementsByTagName('a');
  65. return a.length > 0 ? parseInt(new URLSearchParams(a[0].search).get('forumid')) : undefined;
  66. }
  67.  
  68. function threadClickHandler(evt) {
  69. const tr = evt.currentTarget.parentNode.parentNode, threadId = getThreadId(tr);
  70. if (!threadId) throw 'invalid page structure';
  71. const index = ignoredThreads[document.location.hostname].indexOf(threadId);
  72. if (index < 0) ignoredThreads[document.location.hostname].push(threadId);
  73. else ignoredThreads[document.location.hostname].splice(index, 1);
  74. GM_setValue('ignored_threads', ignoredThreads);
  75. evt.currentTarget.textContent = index < 0 ? '+' : 'X';
  76. evt.currentTarget.style.color = index < 0 ? 'green' : 'red';
  77. if (action == 'unread') tr.style.visibility = index < 0 ? 'collapse' : 'visible';
  78. tr.style.opacity = index < 0 ? ignOpacity : null;
  79. return false;
  80. }
  81.  
  82. function forumClickHandler(evt) {
  83. const index = ignoredForums[document.location.hostname].indexOf(_forumId);
  84. if (index < 0) ignoredForums[document.location.hostname].push(_forumId);
  85. else ignoredForums[document.location.hostname].splice(index, 1);
  86. GM_setValue('ignored_forums', ignoredForums);
  87. evt.currentTarget.textContent = index < 0 ? '+' : 'X';
  88. evt.currentTarget.style.color = index < 0 ? 'green' : 'red';
  89. updateView();
  90. return false;
  91. }
  92.  
  93. switch (action) {
  94. case 'unread':
  95. var selector = 'table#unread_posts_table';
  96. var hdrCallback = function(tr) {
  97. tr.children[0].width = '16%';
  98. tr.children[2].removeAttribute('width');
  99. };
  100. break;
  101. case 'viewforum': {
  102. selector = 'table.forum_index';
  103. hdrCallback = tr => { tr.children[3].style.width = '11%' };
  104. var _forumId = parseInt(new URLSearchParams(document.location.search).get('forumid'));
  105. const h2 = document.body.querySelector('div#content h2');
  106. if (h2 == null) break; // assertion failed
  107. const span = document.createElement('SPAN'), a = document.createElement('A');
  108. span.className = 'forum-ignore';
  109. span.style = 'float: right; margin-left: 1em;';
  110. a.href = '#';
  111. a.className = 'brackets';
  112. if (ignoredForums[document.location.hostname].includes(_forumId)) {
  113. a.textContent = '+';
  114. a.style.color = 'green';
  115. } else {
  116. a.textContent = 'X';
  117. a.style.color = 'red';
  118. }
  119. a.onclick = forumClickHandler;
  120. a.title = 'Ignore/unignore this subforum from unread forum threads view';
  121. span.append(a);
  122. h2.append(span);
  123. break;
  124. }
  125. default: throw 'invalid location';
  126. }
  127. selector += ' > tbody > tr';
  128.  
  129. function updateView() {
  130. for (let tr of document.body.querySelectorAll(selector)) {
  131. let td = tr.querySelector('td.thread-ignore');
  132. if (tr.classList.contains('colhead')) {
  133. if (td == null) {
  134. td = document.createElement('td');
  135. if (typeof hdrCallback == 'function') hdrCallback(tr);
  136. td.className = 'thread-ignore';
  137. td.textContent = 'Ignore';
  138. td.style.width = '6%';
  139. tr.append(td);
  140. }
  141. td.hidden = _forumId > 0 && ignoredForums[document.location.hostname].includes(_forumId);
  142. continue;
  143. }
  144. const forumId = _forumId || getForumId(tr), threadId = getThreadId(tr);
  145. let a = td && td.querySelector('a.ignore');
  146. if (a == null) {
  147. a = document.createElement('a');
  148. a.href = '#';
  149. a.className = 'brackets ignore';
  150. a.onclick = threadClickHandler;
  151. a.title = 'Ignore/unignore this thread from unread forum threads view';
  152. }
  153. if (forumId > 0 && ignoredForums[document.location.hostname].includes(forumId)
  154. || threadId > 0 && ignoredThreads[document.location.hostname].includes(threadId)) {
  155. if (action == 'unread') tr.style.visibility = 'collapse';
  156. tr.style.opacity = ignOpacity;
  157. a.textContent = '+';
  158. a.style.color = 'green';
  159. } else {
  160. if (action == 'unread') tr.style.visibility = 'visible';
  161. tr.style.opacity = null;
  162. a.textContent = 'X';
  163. a.style.color = 'red';
  164. }
  165. if (td == null) {
  166. td = document.createElement('TD');
  167. td.className = 'thread-ignore';
  168. td.style.textAlign = 'center';
  169. td.append(a);
  170. tr.append(td);
  171. }
  172. td.hidden = ignoredForums[document.location.hostname].includes(forumId);
  173. }
  174. }
  175. updateView();
  176.  
  177. if (action != 'unread' || typeof GM_registerMenuCommand != 'function') return;
  178. GM_registerMenuCommand('Temporarily show ignored threads', function() {
  179. document.body.querySelectorAll(selector)
  180. .forEach(tr => { if (tr.style.visibility == 'collapse') tr.style.visibility = 'visible' });
  181. }, 'S');