Guest User

Untitled

a guest
Nov 25th, 2025
61
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         4chan Deleted Post Viewer
  3. // @namespace    http://tampermonkey.net/
  4. // @version      10.1
  5. // @description  Fixes the "Deleted: 0" bug by updating all Top/Bottom buttons simultaneously.
  6. // @author       Anonymous
  7. // @match        *://boards.4chan.org/*/thread/*
  8. // @match        *://boards.4channel.org/*/thread/*
  9. // @connect      *
  10. // @grant        GM_openInTab
  11. // @grant        GM_xmlhttpRequest
  12. // ==/UserScript==
  13.  
  14. (function() {
  15.     'use strict';
  16.  
  17.     // Class name to identify our buttons
  18.     const BTN_CLASS = 'deleted-tracker-btn';
  19.  
  20.     function init() {
  21.         const navLinks = document.querySelectorAll('.navLinks');
  22.         navLinks.forEach(nav => {
  23.             // Create a button for THIS nav container
  24.             const localBtn = document.createElement('a');
  25.             localBtn.href = 'javascript:;';
  26.             localBtn.textContent = 'Deleted: 0';
  27.             localBtn.className = BTN_CLASS; // Tag it so we can find it later
  28.             localBtn.style.fontWeight = 'bold';
  29.             localBtn.style.color = '#888';
  30.             localBtn.addEventListener('click', startProcess);
  31.  
  32.             nav.appendChild(document.createTextNode(' ['));
  33.             nav.appendChild(localBtn);
  34.             nav.appendChild(document.createTextNode(']'));
  35.         });
  36.  
  37.         // Initial Count
  38.         updateButtonCount();
  39.  
  40.         // Live Monitoring
  41.         const observer = new MutationObserver(debounce(updateButtonCount, 200));
  42.         observer.observe(document.body, {
  43.             childList: true,
  44.             subtree: true,
  45.             attributes: true,
  46.             attributeFilter: ['style', 'class', 'hidden']
  47.         });
  48.     }
  49.  
  50.     function debounce(func, wait) {
  51.         let timeout;
  52.         return function() {
  53.             clearTimeout(timeout);
  54.             timeout = setTimeout(() => func.apply(this, arguments), wait);
  55.         };
  56.     }
  57.  
  58.     function updateButtonCount() {
  59.         // Find ALL buttons (Top, Bottom, Mobile)
  60.         const buttons = document.querySelectorAll('.' + BTN_CLASS);
  61.         if (buttons.length === 0) return;
  62.  
  63.         // Perform the scan
  64.         const allPosts = document.querySelectorAll('.post');
  65.         let visibleCount = 0;
  66.         let hiddenCount = 0;
  67.  
  68.         allPosts.forEach(post => {
  69.             const txt = (post.textContent || "").toLowerCase();
  70.             // Logic: Has tag AND (is visible OR is hidden)
  71.             if (txt.includes('[deleted]') || txt.includes('restored from external')) {
  72.                 // Check if visible on screen (height > 0)
  73.                 if (post.offsetWidth > 0 || post.getClientRects().length > 0) {
  74.                     visibleCount++;
  75.                 } else {
  76.                     hiddenCount++;
  77.                 }
  78.             }
  79.         });
  80.  
  81.         // Determine Text & Color
  82.         let labelText = `Deleted: ${visibleCount}`;
  83.         if (hiddenCount > 0) labelText += ` (+${hiddenCount} Hidden)`;
  84.  
  85.         const color = (visibleCount > 0 || hiddenCount > 0) ? (hiddenCount > 0 ? 'orange' : '#d00') : '#888';
  86.  
  87.         // Update EVERY button
  88.         buttons.forEach(btn => {
  89.             btn.textContent = labelText;
  90.             btn.style.color = color;
  91.         });
  92.     }
  93.  
  94.     // --- Report Generator (unchanged) ---
  95.     function fetchImageAsBase64(url) {
  96.         return new Promise((resolve) => {
  97.             if (url.startsWith('//')) url = 'https:' + url;
  98.             if (url.startsWith('/')) url = window.location.origin + url;
  99.  
  100.             GM_xmlhttpRequest({
  101.                 method: "GET",
  102.                 url: url,
  103.                 responseType: "blob",
  104.                 onload: function(response) {
  105.                     if (response.status === 200) {
  106.                         const reader = new FileReader();
  107.                         reader.onloadend = () => resolve(reader.result);
  108.                         reader.readAsDataURL(response.response);
  109.                     } else { resolve(null); }
  110.                 },
  111.                 onerror: function() { resolve(null); }
  112.             });
  113.         });
  114.     }
  115.  
  116.     async function startProcess() {
  117.         const status = document.createElement('div');
  118.         Object.assign(status.style, {
  119.             position: 'fixed', top: '10px', right: '10px', background: 'rgba(0,0,0,0.9)',
  120.             color: '#fff', padding: '15px', zIndex: '999999', borderRadius: '5px',
  121.             fontFamily: 'sans-serif', fontSize: '14px'
  122.         });
  123.         document.body.appendChild(status);
  124.         function updateStatus(msg) { status.innerText = msg; }
  125.  
  126.         updateStatus('Scanning visible posts...');
  127.         const allPosts = document.querySelectorAll('.post');
  128.  
  129.         // Only report VISIBLE posts
  130.         const targets = Array.from(allPosts).filter(post => {
  131.             const txt = (post.textContent || "").toLowerCase();
  132.             const isDel = txt.includes('[deleted]') || txt.includes('restored from external');
  133.             const isVisible = post.offsetWidth > 0 || post.getClientRects().length > 0;
  134.             return isDel && isVisible;
  135.         });
  136.  
  137.         if (targets.length === 0) {
  138.             alert('No visible deleted posts found.\n(Hidden posts are excluded from the report image)');
  139.             status.remove();
  140.             return;
  141.         }
  142.  
  143.         updateStatus(`Found ${targets.length} visible posts. Preparing...`);
  144.  
  145.         const cleanPosts = [];
  146.         const imageJobs = [];
  147.  
  148.         targets.forEach(post => {
  149.             const clone = post.cloneNode(true);
  150.             const removeSelectors = [
  151.                 '.sauce', '.backlink', '.menu-button', '.download-button',
  152.                 'input[type="checkbox"]', '.postInfo > span.container',
  153.                 '.sideArrows', '.mobile', '.expanded-thumb'
  154.             ];
  155.             clone.querySelectorAll(removeSelectors.join(',')).forEach(el => el.remove());
  156.  
  157.             const warning = clone.querySelector('.warning');
  158.             if (warning) {
  159.                 warning.style.cssText = "font-size: inherit; visibility: visible; display: inline; opacity: 1; color: red; font-weight: bold;";
  160.                 warning.textContent = '[Deleted]';
  161.             }
  162.  
  163.             const fileThumb = clone.querySelector('.fileThumb img');
  164.             if (fileThumb) {
  165.                 const src = fileThumb.getAttribute('src') || fileThumb.src;
  166.                 imageJobs.push({ element: fileThumb, url: src });
  167.                 fileThumb.style.cssText = "width: auto; height: auto; max-width: 250px; max-height: 250px;";
  168.                 fileThumb.removeAttribute('loading');
  169.             }
  170.  
  171.             clone.style.cssText = "position: static; display: block; margin: 0; border: none; box-shadow: none; max-width: 100%; opacity: 1;";
  172.             cleanPosts.push(clone);
  173.         });
  174.  
  175.         if (imageJobs.length > 0) {
  176.             updateStatus(`Downloading ${imageJobs.length} thumbnails...`);
  177.             const downloads = imageJobs.map(job => fetchImageAsBase64(job.url));
  178.             const results = await Promise.all(downloads);
  179.             results.forEach((base64Data, index) => {
  180.                 if (base64Data) imageJobs[index].element.src = base64Data;
  181.             });
  182.         }
  183.  
  184.         updateStatus('Generating HTML...');
  185.         let styles = '';
  186.         document.querySelectorAll('link[rel="stylesheet"], style').forEach(style => {
  187.             styles += style.outerHTML;
  188.         });
  189.  
  190.         const htmlContent = `
  191.             <!DOCTYPE html>
  192.             <html>
  193.             <head>
  194.                 <title>Deleted Posts Report</title>
  195.                 <meta charset="utf-8">
  196.                 <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
  197.                 ${styles}
  198.                 <style>
  199.                     body { padding: 10px; margin: 0; background-color: #eef2ff; }
  200.                     body.dark { background-color: #222; color: #aaa; }
  201.                     #header-bar { display: flex; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #ccc; }
  202.                     h1 { font-family: sans-serif; font-size: 20px; margin: 0; flex-grow: 1; }
  203.                     #save-btn { background: #d00; color: white; border: none; padding: 8px 15px; font-size: 14px; font-weight: bold; cursor: pointer; border-radius: 4px; margin-left: 20px; }
  204.                     #save-btn:hover { background: #b00; }
  205.                     #save-btn:disabled { background: #777; cursor: wait; }
  206.                     #container { column-width: 300px; column-gap: 5px; width: 100%; }
  207.                     .post-wrapper { break-inside: avoid; page-break-inside: avoid; margin-bottom: 5px; padding: 4px; display: inline-block; width: 100%; box-sizing: border-box; }
  208.                     .post { display: block !important; margin: 0 !important; width: 100% !important; opacity: 1 !important; }
  209.                     blockquote { margin: 4px 0 0 0; }
  210.                     s, .spoiler { background-color: #000 !important; color: #fff !important; text-decoration: none !important; padding: 0 2px; }
  211.                 </style>
  212.             </head>
  213.             <body class="${document.body.className}">
  214.                 <div id="header-bar">
  215.                     <h1>Deleted Posts Report (${targets.length})</h1>
  216.                     <button id="save-btn" onclick="saveAsImage()">Save as PNG</button>
  217.                 </div>
  218.                 <div id="container">
  219.                     ${cleanPosts.map(p => `<div class="post-wrapper">${p.outerHTML}</div>`).join('')}
  220.                 </div>
  221.                 <script>
  222.                     function saveAsImage() {
  223.                         const btn = document.getElementById('save-btn');
  224.                         const originalText = btn.innerText;
  225.                         btn.disabled = true; btn.innerText = 'Rendering...';
  226.                         window.scrollTo(0,0);
  227.                         html2canvas(document.body, {
  228.                             scale: 1, ignoreElements: (element) => element.id === 'header-bar', backgroundColor: null
  229.                         }).then(canvas => {
  230.                             canvas.toBlob(function(blob) {
  231.                                 const url = URL.createObjectURL(blob);
  232.                                 const a = document.createElement('a'); a.href = url; a.download = '4chan_deleted_collage.png';
  233.                                 document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
  234.                                 btn.disabled = false; btn.innerText = originalText;
  235.                             });
  236.                         }).catch(err => {
  237.                             alert('Error: ' + err.message);
  238.                             btn.disabled = false; btn.innerText = originalText;
  239.                         });
  240.                     }
  241.                 </script>
  242.             </body>
  243.             </html>
  244.         `;
  245.  
  246.         updateStatus('Opening Report...');
  247.         const blob = new Blob([htmlContent], { type: 'text/html' });
  248.         const url = URL.createObjectURL(blob);
  249.         window.open(url, '_blank');
  250.         setTimeout(() => status.remove(), 1000);
  251.     }
  252.  
  253.     init();
  254. })();
Add Comment
Please, Sign In to add comment