Greasy Fork is available in English.

Better Sturdy

Easy image filtering and post marking + hide your own posts

  1. // ==UserScript==
  2. // @name Better Sturdy
  3. // @namespace dunkydonut
  4. // @version 1.1
  5. // @description Easy image filtering and post marking + hide your own posts
  6. // @author ILoveS10
  7. // @license MIT
  8. // @match https://sturdychan.help/*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. 'use strict';
  14.  
  15. let hiddenCount = 0;
  16. const hiddenDisplay = document.querySelector('#hidden');
  17. const hiddenPosts = new Map();
  18.  
  19. function updateHiddenDisplay() {
  20. if (hiddenDisplay) hiddenDisplay.textContent = `Hidden: ${hiddenPosts.size}`;
  21. }
  22.  
  23. function hashFiltering(postElement) {
  24. const image = postElement.querySelector('figure img');
  25. const filterArea = document.querySelector('#filterArea');
  26. if (!image || !filterArea) return null;
  27.  
  28. const imageURL = image.src;
  29. const filenameWithExt = imageURL.split('/').pop();
  30. const filenameWithoutExt = filenameWithExt.split('.')[0];
  31. const hashLine = `hash:${filenameWithoutExt}`;
  32.  
  33. filterArea.value += `\n${hashLine}`;
  34. return hashLine;
  35. }
  36.  
  37. function showMessage(text, undoCallback) {
  38. const message = document.createElement('div');
  39. message.id = 'sturdy-message';
  40. message.style.position = 'fixed';
  41. message.style.top = '30px';
  42. message.style.left = '50%';
  43. message.style.transform = 'translateX(-50%)';
  44. message.style.backgroundColor = '#000';
  45. message.style.color = '#fff';
  46. message.style.padding = '10px 20px';
  47. message.style.borderRadius = '8px';
  48. message.style.zIndex = '10000';
  49. message.style.fontSize = '16px';
  50. message.style.boxShadow = '0 2px 10px rgba(0,0,0,0.5)';
  51. message.style.display = 'flex';
  52. message.style.alignItems = 'center';
  53. message.style.gap = '10px';
  54.  
  55. const textNode = document.createElement('span');
  56. textNode.textContent = text;
  57.  
  58. const showButton = document.createElement('button');
  59. showButton.textContent = '[Show]';
  60. showButton.style.background = 'none';
  61. showButton.style.color = '#0af';
  62. showButton.style.border = 'none';
  63. showButton.style.cursor = 'pointer';
  64. showButton.style.fontSize = '16px';
  65.  
  66. const undoButton = document.createElement('button');
  67. undoButton.textContent = '[Undo]';
  68. undoButton.style.background = 'none';
  69. undoButton.style.color = '#0af';
  70. undoButton.style.border = 'none';
  71. undoButton.style.cursor = 'pointer';
  72. undoButton.style.fontSize = '16px';
  73.  
  74. showButton.addEventListener('click', () => {
  75. const filterBox = document.getElementById('megukascript-options');
  76. const filterTabs = filterBox?.querySelectorAll('.tab-link');
  77. const filtersTab = Array.from(filterTabs || []).find(tab => tab.dataset.id === '1');
  78. const filterArea = document.getElementById('filterArea');
  79.  
  80. if (filterBox) filterBox.style.display = 'block';
  81. if (filtersTab) filtersTab.click();
  82. if (filterArea) filterArea.scrollIntoView({ behavior: 'smooth', block: 'center' });
  83. });
  84.  
  85. undoButton.addEventListener('click', () => {
  86. undoCallback();
  87. message.remove();
  88. });
  89.  
  90. message.appendChild(textNode);
  91. message.appendChild(showButton);
  92. message.appendChild(undoButton);
  93. document.body.appendChild(message);
  94.  
  95. const removeMessage = () => {
  96. message.remove();
  97. document.removeEventListener('keydown', onKeyDown);
  98. };
  99.  
  100. const onKeyDown = (e) => {
  101. if (e.key === 'Escape') removeMessage();
  102. };
  103.  
  104. document.addEventListener('keydown', onKeyDown);
  105. setTimeout(removeMessage, 10000);
  106. }
  107.  
  108. function hideReplies(post) {
  109. const backlinks = post.querySelectorAll('.backlinks a.post-link[data-id]');
  110. backlinks.forEach(link => {
  111. const replyId = link.getAttribute('data-id');
  112. const replyPost = document.getElementById(`p${replyId}`);
  113. if (replyPost && !hiddenPosts.has(replyPost)) {
  114. const originalClass = replyPost.className;
  115. replyPost.classList.add('hidden');
  116. hiddenPosts.set(replyPost, originalClass);
  117. }
  118. });
  119. }
  120.  
  121. function addButtons() {
  122. const dropdowns = document.querySelectorAll('ul.popup-menu.glass');
  123.  
  124. dropdowns.forEach(dropdown => {
  125. const post = dropdown.closest('article');
  126. if (!post) return;
  127.  
  128. if (!dropdown.querySelector('li[data-id="hide"]')) {
  129. const hideItem = document.createElement('li');
  130. hideItem.setAttribute('data-id', 'hide');
  131. hideItem.textContent = 'Hide';
  132. hideItem.addEventListener('click', e => {
  133. e.stopPropagation();
  134. const originalClass = post.classList.contains('postMine') ? 'glass postMine' : 'glass';
  135. post.classList.remove('postMine');
  136. post.classList.add('hidden');
  137. hiddenPosts.set(post, originalClass);
  138. hideReplies(post);
  139. updateHiddenDisplay();
  140. });
  141. dropdown.insertBefore(hideItem, dropdown.firstChild);
  142. }
  143.  
  144. if (!dropdown.querySelector('li[data-id="addHashToFilter"]')) {
  145. const hasImage = post.querySelector('figcaption a[download]');
  146. if (hasImage) {
  147. const hashFilter = document.createElement('li');
  148. hashFilter.setAttribute('data-id', 'addHashToFilter');
  149. hashFilter.textContent = 'Filter Hash';
  150.  
  151. hashFilter.addEventListener('click', e => {
  152. e.stopPropagation();
  153. const hashLine = hashFiltering(post);
  154. if (!hashLine) return;
  155.  
  156. const saveButton = document.getElementById('filterArea_button');
  157. if (saveButton) saveButton.click();
  158.  
  159. showMessage('Image Filtered. Refresh page.', () => {
  160. const filterArea = document.querySelector('#filterArea');
  161. if (!filterArea) return;
  162. const lines = filterArea.value.split('\n').filter(line => line.trim() !== hashLine);
  163. filterArea.value = lines.join('\n');
  164. if (saveButton) saveButton.click();
  165. });
  166. });
  167.  
  168. dropdown.appendChild(hashFilter);
  169. }
  170. }
  171. });
  172. }
  173.  
  174. addButtons();
  175. const observer = new MutationObserver(addButtons);
  176. observer.observe(document.body, { childList: true, subtree: true });
  177.  
  178. function addToggleMarkButtons() {
  179. document.querySelectorAll('article.glass .popup-menu.glass').forEach(menu => {
  180. const article = menu.closest('article');
  181. if (!article) return;
  182.  
  183. let existingToggle = menu.querySelector('li[data-id="toggle-own"]');
  184. if (!existingToggle) {
  185. const toggleItem = document.createElement('li');
  186. toggleItem.setAttribute('data-id', 'toggle-own');
  187.  
  188. function updateToggleText() {
  189. toggleItem.textContent = article.classList.contains('postMine') ?
  190. 'Unmark post as your own' : 'Mark post as your own';
  191. }
  192.  
  193. toggleItem.addEventListener('click', () => {
  194. const postId = article.id.replace('p', '');
  195. const replySelectors = `a.post-link[data-id="${postId}"]`;
  196.  
  197. if (article.classList.contains('postMine')) {
  198. article.classList.remove('postMine');
  199. const author = article.querySelector('b.name i');
  200. if (author && author.textContent.trim() === '(You)') author.remove();
  201. document.querySelectorAll(replySelectors).forEach(link => {
  202. const replyArticle = link.closest('article');
  203. if (replyArticle) {
  204. link.innerHTML = link.innerHTML.replace(' (You)', '');
  205. replyArticle.classList.remove('postReply');
  206. }
  207. });
  208. } else {
  209. article.classList.add('postMine');
  210. const nameSpan = article.querySelector('b.name span');
  211. if (nameSpan && !article.querySelector('b.name i')) {
  212. const youMarker = document.createElement('i');
  213. youMarker.textContent = ' (You)';
  214. nameSpan.insertAdjacentElement('afterend', youMarker);
  215. }
  216. document.querySelectorAll(replySelectors).forEach(link => {
  217. const replyArticle = link.closest('article');
  218. if (replyArticle && !link.innerHTML.includes(' (You)')) {
  219. link.innerHTML = link.innerHTML.replace(/(>>\d+)/, '$1 (You)');
  220. replyArticle.classList.add('postReply');
  221. }
  222. });
  223. }
  224. updateToggleText();
  225. });
  226.  
  227. updateToggleText();
  228. menu.appendChild(toggleItem);
  229. }
  230. });
  231. }
  232.  
  233. addToggleMarkButtons();
  234. const toggleObserver = new MutationObserver(addToggleMarkButtons);
  235. toggleObserver.observe(document.body, { childList: true, subtree: true });
  236.  
  237. if (hiddenDisplay) {
  238. hiddenDisplay.addEventListener('click', () => {
  239. hiddenPosts.forEach((originalClass, post) => {
  240. post.className = originalClass;
  241. });
  242. hiddenPosts.clear();
  243. updateHiddenDisplay();
  244. });
  245. }
  246. })();