8chan Reports Page Enhancer

Enhances the usability and appearance of the 8chan board moderation reports page with a dynamic, compact grid layout, smaller image thumbnails, combined report labels in a single div, styled Moderate buttons, scrollable post content, and an orange-themed Ban button to open the ban modal.

  1. // ==UserScript==
  2. // @name 8chan Reports Page Enhancer
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.12
  5. // @description Enhances the usability and appearance of the 8chan board moderation reports page with a dynamic, compact grid layout, smaller image thumbnails, combined report labels in a single div, styled Moderate buttons, scrollable post content, and an orange-themed Ban button to open the ban modal.
  6. // @author Grok
  7. // @match *://8chan.moe/openReports.js*
  8. // @grant GM_addStyle
  9. // @license MIT
  10. // @run-at document-start
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // Inject CSS styles
  17. GM_addStyle(`
  18. /* General Layout */
  19. body {
  20. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
  21. background-color: #f5f6f5;
  22. color: #333;
  23. line-height: 1.4;
  24. margin: 0;
  25. font-size: 0.9em;
  26. }
  27.  
  28. .titleWrapper {
  29. max-width: 100%;
  30. margin: 15px auto;
  31. padding: 0 10px;
  32. box-sizing: border-box;
  33. }
  34.  
  35. .titleFieldset {
  36. background: #fff;
  37. border: 1px solid #ddd;
  38. border-radius: 6px;
  39. padding: 15px;
  40. box-shadow: 0 1px 3px rgba(0,0,0,0.05);
  41. }
  42.  
  43. legend {
  44. font-size: 1.3em;
  45. font-weight: 600;
  46. color: #222;
  47. padding: 0 8px;
  48. }
  49.  
  50. /* Report Cells */
  51. #reportDiv {
  52. display: flex !important;
  53. flex-wrap: wrap !important;
  54. gap: 10px;
  55. margin-top: 15px;
  56. justify-content: space-between;
  57. width: 100%;
  58. box-sizing: border-box;
  59. }
  60.  
  61. .reportCell {
  62. background: #fff;
  63. border: 1px solid #e0e0e0;
  64. border-radius: 5px;
  65. padding: 10px;
  66. transition: box-shadow 0.2s;
  67. flex: 1 1 calc(50% - 8px);
  68. box-sizing: border-box;
  69. min-width: 250px;
  70. font-size: 0.85em;
  71. display: flex;
  72. flex-direction: column;
  73. }
  74.  
  75. .reportCell:hover {
  76. box-shadow: 0 3px 6px rgba(0,0,0,0.1);
  77. }
  78.  
  79. /* Combined Div */
  80. .reportCell .combined-div {
  81. display: flex;
  82. flex-wrap: nowrap;
  83. gap: 12px;
  84. align-items: center;
  85. font-size: 0.9em;
  86. margin-bottom: 5px;
  87. white-space: nowrap;
  88. }
  89.  
  90. .reportCell .combined-div span {
  91. display: inline-flex;
  92. align-items: center;
  93. }
  94.  
  95. /* Hide Reports and Category labels as fallback */
  96. .reportCell label:has(.totalLabel),
  97. .reportCell label.categoryDiv {
  98. display: none !important;
  99. }
  100.  
  101. .boardLabel, .totalLabel, .categoryLabel {
  102. font-weight: 500;
  103. color: #1a73e8;
  104. }
  105.  
  106. .reasonLabel {
  107. background: #f1f3f4;
  108. padding: 3px 6px;
  109. border-radius: 3px;
  110. display: inline-block;
  111. font-size: 0.9em;
  112. }
  113.  
  114. /* Checkbox Styles */
  115. .closureCheckbox {
  116. margin-right: 6px;
  117. vertical-align: middle;
  118. width: 16px;
  119. height: 16px;
  120. cursor: pointer;
  121. accent-color: #1a73e8;
  122. box-shadow: 0 0 6px rgba(26, 115, 232, 0.6);
  123. }
  124.  
  125. .deletionCheckBox {
  126. margin-right: 6px;
  127. vertical-align: middle;
  128. width: 14px;
  129. height: 14px;
  130. cursor: pointer;
  131. accent-color: #d32f2f;
  132. }
  133.  
  134. /* Moderate and Ban Buttons */
  135. .reportCell label {
  136. display: flex;
  137. align-items: center;
  138. gap: 8px;
  139. flex-wrap: wrap;
  140. }
  141.  
  142. .link {
  143. color: #d32f2f;
  144. font-weight: 500;
  145. text-decoration: none;
  146. padding: 3px 6px;
  147. border-radius: 3px;
  148. font-size: 0.9em;
  149. background-color: #f0e0e0;
  150. border: 1px solid #e03030;
  151. transition: background-color 0.2s, border-color 0.2s;
  152. }
  153.  
  154. .banButton {
  155. color: #f57c00;
  156. font-weight: 500;
  157. text-decoration: none;
  158. padding: 3px 6px;
  159. border-radius: 3px;
  160. font-size: 0.9em;
  161. background-color: #ffe0b2;
  162. border: 1px solid #f57c00;
  163. transition: background-color 0.2s, border-color 0.2s;
  164. }
  165.  
  166. .link:hover, .banButton:hover {
  167. background-color: #fff3e0;
  168. border-color: #888;
  169. text-decoration: none;
  170. }
  171.  
  172. /* Post Content */
  173. .postingDiv {
  174. flex: 1;
  175. min-width: 0;
  176. max-height: 25rem;
  177. overflow-y: auto;
  178. }
  179.  
  180. .postingDiv .innerPost {
  181. background: #fafafa;
  182. padding: 8px;
  183. border-radius: 3px;
  184. margin-top: 8px;
  185. font-size: 0.9em;
  186. min-width: 150px;
  187. padding-bottom: 12px;
  188. }
  189.  
  190. .labelBoard {
  191. font-size: 1em;
  192. color: #0288d1;
  193. margin: 0 0 8px;
  194. }
  195.  
  196. .postInfo {
  197. font-size: 0.85em;
  198. color: #555;
  199. white-space: normal;
  200. }
  201.  
  202. .labelId {
  203. padding: 2px 5px;
  204. border-radius: 3px;
  205. color: #fff;
  206. font-size: 0.85em;
  207. }
  208.  
  209. .panelIp, .panelASN, .panelBypassId {
  210. font-size: 0.8em;
  211. color: #666;
  212. margin-top: 4px;
  213. word-break: break-all;
  214. }
  215.  
  216. .divMessage {
  217. margin-top: 8px;
  218. font-size: 0.9em;
  219. color: #333;
  220. padding: 6px;
  221. background: #f5f5f5;
  222. border-radius: 3px;
  223. min-width: 150px;
  224. word-wrap: break-word;
  225. }
  226.  
  227. .quoteLink {
  228. color: #388e3c;
  229. text-decoration: none;
  230. font-weight: 500;
  231. font-size: 0.9em;
  232. }
  233.  
  234. .quoteLink:hover {
  235. text-decoration: underline;
  236. }
  237.  
  238. /* Uploads */
  239. .panelUploads {
  240. margin-top: 8px;
  241. display: flex;
  242. flex-wrap: wrap;
  243. }
  244.  
  245. .uploadCell {
  246. margin-bottom: 8px;
  247. flex: 0 0 auto;
  248. }
  249.  
  250. .imgLink img {
  251. border-radius: 3px;
  252. max-width: 60px !important;
  253. max-height: 60px !important;
  254. width: auto !important;
  255. height: auto !important;
  256. object-fit: contain;
  257. display: block;
  258. margin: 0;
  259. }
  260.  
  261. .originalNameLink, .nameLink {
  262. color: #0288d1;
  263. text-decoration: none;
  264. font-size: 0.85em;
  265. }
  266.  
  267. .originalNameLink:hover, .nameLink:hover {
  268. text-decoration: underline;
  269. }
  270.  
  271. .uploadDetails, .divHash, .sizeLabel, .dimensionLabel {
  272. font-size: 0.8em;
  273. color: #555;
  274. }
  275.  
  276. .unlinkLink, .unlinkAndDeleteLink {
  277. font-size: 0.85em;
  278. }
  279.  
  280. /* Forms */
  281. #filterForm, form[action="/closeReports.js"] {
  282. background: #f9f9f9;
  283. padding: 10px;
  284. border-radius: 5px;
  285. margin-bottom: 15px;
  286. }
  287.  
  288. #filterForm label, form[action="/closeReports.js"] label {
  289. display: block;
  290. margin-bottom: 8px;
  291. font-weight: 500;
  292. color: #444;
  293. font-size: 0.9em;
  294. }
  295.  
  296. #filterCategoriesDiv {
  297. margin: 8px 0;
  298. }
  299.  
  300. .categoryCheckbox {
  301. margin-right: 6px;
  302. width: 14px;
  303. height: 14px;
  304. }
  305.  
  306. input[type="text"], select {
  307. padding: 6px;
  308. border: 1px solid #ccc;
  309. border-radius: 3px;
  310. font-size: 0.9em;
  311. width: 180px;
  312. margin-left: 8px;
  313. }
  314.  
  315. input[type="text"]:focus, select:focus {
  316. border-color: #1a73e8;
  317. outline: none;
  318. box-shadow: 0 0 0 2px rgba(26,115,232,0.2);
  319. }
  320.  
  321. button, #filterSubmitButton, #closeReportsFormButton {
  322. background: #1a73e8;
  323. color: #fff;
  324. border: none;
  325. padding: 6px 12px;
  326. border-radius: 3px;
  327. font-size: 0.9em;
  328. cursor: pointer;
  329. transition: background 0.2s;
  330. }
  331.  
  332. button:hover, #filterSubmitButton:hover, #closeReportsFormButton:hover {
  333. background: #1565c0;
  334. }
  335.  
  336. /* Checkboxes */
  337. .closeReportsField {
  338. margin-right: 6px;
  339. vertical-align: middle;
  340. width: 14px;
  341. height: 14px;
  342. }
  343.  
  344. /* Navigation */
  345. .navHeader {
  346. background: #fff;
  347. border-bottom: 1px solid #ddd;
  348. padding: 8px 15px;
  349. position: sticky;
  350. top: 0;
  351. z-index: 1000;
  352. }
  353.  
  354. .navLinkSpan a {
  355. color: #0288d1;
  356. text-decoration: none;
  357. margin: 0 4px;
  358. font-size: 0.9em;
  359. }
  360.  
  361. .navLinkSpan a:hover {
  362. text-decoration: underline;
  363. }
  364.  
  365. /* Responsive Adjustments for More Columns */
  366. @media (min-width: 900px) {
  367. .reportCell {
  368. flex: 1 1 calc(33.33% - 8px);
  369. }
  370. }
  371.  
  372. @media (min-width: 1200px) {
  373. .reportCell {
  374. flex: 1 1 calc(25% - 8px);
  375. }
  376. }
  377.  
  378. @media (max-width: 600px) {
  379. .reportCell {
  380. flex: 1 1 100% !important;
  381. min-width: 100% !important;
  382. max-width: 100% !important;
  383. }
  384.  
  385. input[type="text"], select {
  386. width: 100%;
  387. }
  388.  
  389. .divMessage, .innerPost {
  390. min-width: 100% !important;
  391. }
  392.  
  393. .reportCell .combined-div {
  394. flex-direction: column;
  395. gap: 5px;
  396. align-items: flex-start;
  397. white-space: normal;
  398. }
  399.  
  400. .reportCell label {
  401. flex-direction: column;
  402. align-items: flex-start;
  403. gap: 4px;
  404. }
  405. }
  406. `);
  407.  
  408. // JavaScript to combine labels and add Ban button
  409. function processReportCells() {
  410. const cells = document.querySelectorAll('.reportCell:not(.processed)');
  411. if (cells.length) {
  412. console.log(`Processing ${cells.length} reportCell(s) at`, Date.now());
  413. }
  414. cells.forEach(cell => {
  415. // Mark as processed
  416. cell.classList.add('processed');
  417.  
  418. // Combine labels into a single div
  419. const labels = cell.querySelectorAll('label:not([for])');
  420. if (labels.length >= 3) {
  421. const boardSpan = labels[0].querySelector('.boardLabel');
  422. const totalSpan = labels[1].querySelector('.totalLabel');
  423. const categorySpan = labels[2].querySelector('.categoryLabel');
  424.  
  425. if (boardSpan && totalSpan && categorySpan) {
  426. const combinedDiv = document.createElement('div');
  427. combinedDiv.className = 'combined-div';
  428. combinedDiv.innerHTML = `
  429. <span class="boardLabel">/${boardSpan.textContent}/</span>
  430. Reports: <span class="totalLabel">${totalSpan.textContent}</span>
  431. Category: <span class="categoryLabel">${categorySpan.textContent}</span>
  432. `;
  433. cell.insertBefore(combinedDiv, labels[0]);
  434. labels[0].remove();
  435. labels[1].remove();
  436. labels[2].remove();
  437. }
  438. }
  439.  
  440. // Add Ban button
  441. const reportLabel = cell.querySelector('label');
  442. const deletionCheckBox = cell.querySelector('.deletionCheckBox');
  443. if (reportLabel && deletionCheckBox) {
  444. const banButton = document.createElement('a');
  445. banButton.className = 'banButton';
  446. banButton.textContent = 'Ban';
  447. banButton.href = '#';
  448. banButton.setAttribute('data-ban-id', deletionCheckBox.name);
  449. banButton.setAttribute('aria-label', `Ban post ${deletionCheckBox.name}`);
  450. banButton.addEventListener('click', (e) => {
  451. e.preventDefault();
  452. e.stopPropagation(); // Prevent bubbling to label/checkbox
  453. console.log('Ban button clicked for post:', deletionCheckBox.name);
  454.  
  455. // Find extraMenuButton
  456. const extraMenuButton = cell.querySelector('.extraMenuButton');
  457. if (extraMenuButton) {
  458. console.log('Clicking extraMenuButton for:', deletionCheckBox.name);
  459. extraMenuButton.click();
  460.  
  461. // Wait for extraMenu to appear
  462. setTimeout(() => {
  463. const extraMenu = document.querySelector('.extraMenu');
  464. if (extraMenu) {
  465. const banOption = Array.from(extraMenu.querySelectorAll('li')).find(li => li.textContent === 'Ban');
  466. if (banOption) {
  467. console.log('Found Ban option, triggering click for:', deletionCheckBox.name);
  468. const originalDisplay = extraMenu.style.display;
  469. extraMenu.style.display = 'block';
  470. banOption.click();
  471. setTimeout(() => {
  472. extraMenu.style.display = originalDisplay;
  473. }, 0);
  474. } else {
  475. console.error('Ban option not found in .extraMenu');
  476. }
  477. } else {
  478. console.error('extraMenu not found after clicking extraMenuButton');
  479. }
  480. }, 50); // Wait 50ms for extraMenu to appear
  481. } else {
  482. console.error('extraMenuButton not found in reportCell');
  483. }
  484. });
  485. reportLabel.appendChild(banButton);
  486. console.log('Added Ban button for post:', deletionCheckBox.name);
  487. }
  488. });
  489. }
  490.  
  491. // Debounce function to batch processReportCells calls
  492. function debounce(fn, wait) {
  493. let timeout;
  494. return function(...args) {
  495. clearTimeout(timeout);
  496. timeout = setTimeout(() => fn(...args), wait);
  497. };
  498. }
  499.  
  500. // Process cells with debounce
  501. const debouncedProcessReportCells = debounce(processReportCells, 100);
  502.  
  503. // Observe #reportDiv for .reportCell additions
  504. function observeReportDiv() {
  505. const reportDiv = document.querySelector('#reportDiv');
  506. if (reportDiv) {
  507. console.log('Found #reportDiv, starting observer at', Date.now());
  508. const observer = new MutationObserver((mutations) => {
  509. if (mutations.some(m => Array.from(m.addedNodes).some(node => node.nodeType === 1 && node.matches('.reportCell')))) {
  510. debouncedProcessReportCells();
  511. }
  512. });
  513. observer.observe(reportDiv, { childList: true, subtree: true });
  514. // Check existing cells immediately
  515. processReportCells();
  516. } else {
  517. // Retry if #reportDiv not found
  518. setTimeout(observeReportDiv, 100);
  519. }
  520. }
  521.  
  522. // Start observing as early as possible
  523. observeReportDiv();
  524. })();