您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Counts and displays all posters who made more than 1 reply in a thread. Auto-updates every minute. If you use 4chan-X or XT, hit Alt+X to show it.
// ==UserScript== // @name 4chan /pol/ Poster Count (Auto-updating) // @namespace http://tampermonkey.net/ // @version 0.3 // @description Counts and displays all posters who made more than 1 reply in a thread. Auto-updates every minute. If you use 4chan-X or XT, hit Alt+X to show it. // @author LeafAnon // @match https://boards.4chan.org/pol/thread/* // @grant none // @run-at document-end // @license GPL // ==/UserScript== (function() { 'use strict'; // Global variables let updateInterval = null; let lastPostCount = 0; let isAutoUpdateActive = false; let dialogElement = null; // Function to collect all unique IDs, their post counts, and flags function collectPosterIDs() { // Use a more specific selector to avoid counting duplicates // Select only IDs from the desktop version to avoid double counting const posterInfoElements = document.querySelectorAll('.postInfo.desktop .posteruid'); const idCounts = {}; const idFlags = {}; console.log(`Found ${posterInfoElements.length} unique poster elements`); posterInfoElements.forEach(element => { // Extract the ID directly from the inner text const idText = element.textContent || element.innerText; const idMatch = idText.match(/ID: ([a-zA-Z0-9+/]+)/); if (idMatch && idMatch[1]) { const posterId = idMatch[1]; // Increment post count idCounts[posterId] = (idCounts[posterId] || 0) + 1; // Find the flag for this post if not already stored if (!idFlags[posterId]) { // Navigate up to find the postInfo container, then find the flag const postInfo = element.closest('.postInfo') || element.closest('.postInfoM'); if (postInfo) { // First try real flags let flagElement = postInfo.querySelector('.flag'); let isMemeFlag = false; // If no standard flag found, try for memeflags (bfl class) if (!flagElement) { flagElement = postInfo.querySelector('.bfl'); isMemeFlag = true; } if (flagElement) { const flagTitle = flagElement.getAttribute('title'); // Get class differently based on flag type let flagClass; if (isMemeFlag) { // For meme flags, get the second class (bfl-xx) flagClass = flagElement.className.split(' ')[1]; } else { // For standard flags, get the second class (flag-xx) flagClass = flagElement.className.split(' ')[1]; } idFlags[posterId] = { title: flagTitle || '', class: flagClass || '', isMeme: isMemeFlag }; } } } } }); console.log(`Found ${Object.keys(idCounts).length} unique IDs`); return { idCounts, idFlags }; } // Function to sort IDs by post count and get all multi-posters function getMultiPosters(idCounts) { return Object.entries(idCounts) .filter(poster => poster[1] > 1) // Only include posters with more than 1 post .sort((a, b) => b[1] - a[1]); // Sort by post count (highest first) } // Function to create and display a dialog with the results function displayResults(multiPosters, totalUnique, idFlags) { // Remove any existing dialog or update existing one if (dialogElement) { // Update existing dialog updateExistingDialog(multiPosters, totalUnique, idFlags); return; } // Create dialog container const dialog = document.createElement('div'); dialog.id = 'id-counter-dialog'; dialog.className = 'extPanel reply'; dialog.style.position = 'fixed'; dialog.style.top = '50px'; dialog.style.right = '20px'; dialog.style.width = '270px'; // Slightly wider dialog.style.maxHeight = '80vh'; // Limit maximum height dialog.style.overflowY = 'auto'; // Add scrolling if needed dialog.style.zIndex = '9999'; dialog.style.opacity = '0.95'; // Create header const header = document.createElement('div'); header.className = 'postInfo'; header.style.cursor = 'move'; header.style.display = 'flex'; header.style.justifyContent = 'space-between'; // Create header content with update status indicator const headerContent = document.createElement('span'); headerContent.id = 'id-counter-header'; headerContent.innerHTML = `<span>Multi-Posters (${totalUnique} unique IDs)</span>`; header.appendChild(headerContent); // Create controls container const controls = document.createElement('div'); controls.style.display = 'flex'; controls.style.gap = '5px'; // Create auto-update toggle const autoUpdateToggle = document.createElement('span'); autoUpdateToggle.id = 'auto-update-toggle'; autoUpdateToggle.textContent = '🔄'; autoUpdateToggle.title = 'Toggle auto-update (every 60s)'; autoUpdateToggle.style.cursor = 'pointer'; autoUpdateToggle.style.fontWeight = 'bold'; autoUpdateToggle.style.opacity = '0.5'; // Start inactive autoUpdateToggle.onclick = toggleAutoUpdate; controls.appendChild(autoUpdateToggle); // Create manual update button const updateButton = document.createElement('span'); updateButton.textContent = '↻'; updateButton.title = 'Update now'; updateButton.style.cursor = 'pointer'; updateButton.style.fontWeight = 'bold'; updateButton.onclick = main; controls.appendChild(updateButton); // Create close button const closeButton = document.createElement('span'); closeButton.textContent = '×'; closeButton.title = 'Close'; closeButton.style.cursor = 'pointer'; closeButton.style.fontWeight = 'bold'; closeButton.style.fontSize = '20px'; closeButton.style.lineHeight = '15px'; closeButton.onclick = function() { stopAutoUpdate(); document.body.removeChild(dialog); dialogElement = null; }; controls.appendChild(closeButton); header.appendChild(controls); // Create content const content = document.createElement('div'); content.className = 'message'; content.style.padding = '10px'; content.id = 'id-counter-content'; // Create update status indicator const statusIndicator = document.createElement('div'); statusIndicator.id = 'update-status-indicator'; statusIndicator.style.fontSize = '10px'; statusIndicator.style.color = '#999'; statusIndicator.style.textAlign = 'right'; statusIndicator.style.padding = '2px 5px'; statusIndicator.style.borderTop = '1px solid #d9bfb7'; statusIndicator.style.marginTop = '5px'; content.appendChild(statusIndicator); // Create and populate the content populateDialogContent(content, multiPosters, totalUnique, idFlags); // Assemble dialog dialog.appendChild(header); dialog.appendChild(content); document.body.appendChild(dialog); dialogElement = dialog; // Make dialog draggable makeDraggable(dialog, header); // Update last post count tracker lastPostCount = document.querySelectorAll('.post').length; // Update the status indicator updateStatusIndicator(); } // Function to populate dialog content function populateDialogContent(contentElement, multiPosters, totalUnique, idFlags) { // Clear existing content except the status indicator const statusIndicator = document.getElementById('update-status-indicator'); contentElement.innerHTML = ''; // Show message if no multi-posters if (multiPosters.length === 0) { contentElement.innerHTML = '<p>No posters with multiple posts found.</p>'; if (statusIndicator) contentElement.appendChild(statusIndicator); return; } // Create table for multi-posters const table = document.createElement('table'); table.style.width = '100%'; table.style.borderCollapse = 'collapse'; // Add table header const thead = document.createElement('thead'); const headerRow = document.createElement('tr'); const rankHeader = document.createElement('th'); rankHeader.textContent = 'Rank'; rankHeader.style.textAlign = 'left'; rankHeader.style.width = '40px'; const flagHeader = document.createElement('th'); flagHeader.textContent = 'Flag'; flagHeader.style.textAlign = 'center'; flagHeader.style.width = '30px'; const idHeader = document.createElement('th'); idHeader.textContent = 'ID'; idHeader.style.textAlign = 'left'; const postsHeader = document.createElement('th'); postsHeader.textContent = 'Posts'; postsHeader.style.textAlign = 'right'; postsHeader.style.width = '40px'; headerRow.appendChild(rankHeader); headerRow.appendChild(flagHeader); headerRow.appendChild(idHeader); headerRow.appendChild(postsHeader); thead.appendChild(headerRow); table.appendChild(thead); // Add table body with multi-posters const tbody = document.createElement('tbody'); multiPosters.forEach((poster, index) => { const posterId = poster[0]; const postCount = poster[1]; const row = document.createElement('tr'); const rankCell = document.createElement('td'); rankCell.textContent = `${index + 1}.`; const flagCell = document.createElement('td'); flagCell.style.textAlign = 'center'; // Add flag if available if (idFlags[posterId]) { const flagSpan = document.createElement('span'); if (idFlags[posterId].isMeme) { // For meme flags flagSpan.className = `bfl ${idFlags[posterId].class}`; } else { // For standard flags flagSpan.className = `flag ${idFlags[posterId].class}`; } flagSpan.title = idFlags[posterId].title || ''; flagSpan.style.display = 'inline-block'; flagCell.appendChild(flagSpan); } const idCell = document.createElement('td'); idCell.textContent = posterId; const postsCell = document.createElement('td'); postsCell.textContent = postCount; postsCell.style.textAlign = 'right'; row.appendChild(rankCell); row.appendChild(flagCell); row.appendChild(idCell); row.appendChild(postsCell); tbody.appendChild(row); }); table.appendChild(tbody); contentElement.appendChild(table); // Re-add the status indicator if (statusIndicator) contentElement.appendChild(statusIndicator); } // Function to update existing dialog function updateExistingDialog(multiPosters, totalUnique, idFlags) { // Update header information const headerElement = document.getElementById('id-counter-header'); if (headerElement) { headerElement.innerHTML = `<span>Multi-Posters (${totalUnique} unique IDs)</span>`; } // Update content const contentElement = document.getElementById('id-counter-content'); if (contentElement) { populateDialogContent(contentElement, multiPosters, totalUnique, idFlags); } // Update last post count lastPostCount = document.querySelectorAll('.post').length; // Update the status indicator updateStatusIndicator(); } // Function to update the status indicator function updateStatusIndicator() { const indicator = document.getElementById('update-status-indicator'); if (indicator) { const now = new Date(); const timeStr = now.toLocaleTimeString(); indicator.textContent = isAutoUpdateActive ? `Auto-update active | Last: ${timeStr}` : `Last updated: ${timeStr}`; } } // Function to toggle auto-update function toggleAutoUpdate() { if (isAutoUpdateActive) { stopAutoUpdate(); } else { startAutoUpdate(); } // Update visual indication const toggle = document.getElementById('auto-update-toggle'); if (toggle) { toggle.style.opacity = isAutoUpdateActive ? '1' : '0.5'; } // Update the status indicator updateStatusIndicator(); } // Function to start auto-update function startAutoUpdate() { if (updateInterval) { clearInterval(updateInterval); } updateInterval = setInterval(checkForUpdates, 60000); // Check every 60 seconds isAutoUpdateActive = true; console.log("Auto-update started"); } // Function to stop auto-update function stopAutoUpdate() { if (updateInterval) { clearInterval(updateInterval); updateInterval = null; } isAutoUpdateActive = false; console.log("Auto-update stopped"); } // Function to check if there are new posts before updating function checkForUpdates() { const currentPostCount = document.querySelectorAll('.post').length; // Only update if there are new posts if (currentPostCount > lastPostCount) { console.log(`New posts detected (${currentPostCount - lastPostCount}), updating...`); main(); } else { console.log("No new posts, skipping update"); // Still update the timestamp updateStatusIndicator(); } } // Function to make the dialog draggable function makeDraggable(element, dragHandle) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; dragHandle.onmousedown = dragMouseDown; function dragMouseDown(e) { e = e || window.event; e.preventDefault(); // Get the mouse cursor position at startup pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; // Call a function whenever the cursor moves document.onmousemove = elementDrag; } function elementDrag(e) { e = e || window.event; e.preventDefault(); // Calculate the new cursor position pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; // Set the element's new position element.style.top = (element.offsetTop - pos2) + "px"; element.style.left = (element.offsetLeft - pos1) + "px"; } function closeDragElement() { // Stop moving when mouse button is released document.onmouseup = null; document.onmousemove = null; } } // Main function to execute when page is loaded function main() { console.log("Running ID counter main function"); const { idCounts, idFlags } = collectPosterIDs(); const totalUnique = Object.keys(idCounts).length; const multiPosters = getMultiPosters(idCounts); displayResults(multiPosters, totalUnique, idFlags); } // Add button to the thread to trigger the analysis function addButton() { console.log("Attempting to add ID Counter button"); // Create our button elements const countButton = document.createTextNode('['); const button = document.createElement('a'); button.href = 'javascript:void(0);'; button.textContent = 'Count IDs'; button.title = "Count and display unique poster IDs"; button.onclick = main; const closeButton = document.createTextNode('] '); // Try multiple ways to insert the button for better compatibility // First attempt: Insert into navtopright const navTopRight = document.getElementById('navtopright'); if (navTopRight) { // Directly insert at the beginning of navtopright navTopRight.insertBefore(closeButton, navTopRight.firstChild); navTopRight.insertBefore(button, navTopRight.firstChild); navTopRight.insertBefore(countButton, navTopRight.firstChild); console.log("Button added to navtopright"); return; } // Second attempt: Try boardNavDesktop const boardNavDesktop = document.getElementById('boardNavDesktop'); if (boardNavDesktop) { // Create a span to hold our button const buttonSpan = document.createElement('span'); buttonSpan.style.marginLeft = '5px'; buttonSpan.appendChild(countButton); buttonSpan.appendChild(button); buttonSpan.appendChild(closeButton); // Append to boardNavDesktop boardNavDesktop.appendChild(buttonSpan); console.log("Button added to boardNavDesktop"); return; } // Final fallback: Create a floating button console.log("Creating floating button as fallback"); const floatingButton = document.createElement('div'); floatingButton.style.position = 'fixed'; floatingButton.style.top = '10px'; floatingButton.style.right = '10px'; floatingButton.style.backgroundColor = '#f0e0d6'; floatingButton.style.border = '1px solid #d9bfb7'; floatingButton.style.padding = '5px 10px'; floatingButton.style.zIndex = '9999'; floatingButton.style.cursor = 'pointer'; floatingButton.textContent = 'Count IDs'; floatingButton.onclick = main; document.body.appendChild(floatingButton); } // Add a direct execution option for debugging function init() { console.log("4chan ID Counter script initialized"); // Create direct keyboard shortcut to trigger the counter document.addEventListener('keydown', function(e) { // Alt+C to count IDs if (e.altKey && e.key === 'c') { console.log("Keyboard shortcut detected, running counter"); main(); } }); // Add button with a slight delay to ensure page is ready setTimeout(addButton, 1000); // Also run immediately if URL contains a special parameter if (window.location.href.includes('autocount=true')) { console.log("Auto-count parameter detected"); setTimeout(() => { main(); // Also start auto-update if parameter is present startAutoUpdate(); }, 1500); } } // Load required CSS for flags if not already present function ensureFlagCSSLoaded() { // Check if flag CSS is already loaded const flagCSSExists = Array.from(document.styleSheets).some(sheet => { try { return sheet.href && (sheet.href.includes('flags.css') || sheet.href.includes('flags.2.css')); } catch (e) { return false; } }); if (!flagCSSExists) { console.log("Adding flag CSS"); const flagCSS = document.createElement('link'); flagCSS.rel = 'stylesheet'; flagCSS.href = '//s.4cdn.org/css/flags.690.css'; document.head.appendChild(flagCSS); const polFlagCSS = document.createElement('link'); polFlagCSS.rel = 'stylesheet'; polFlagCSS.href = '//s.4cdn.org/image/flags/pol/flags.2.css'; document.head.appendChild(polFlagCSS); } } // Initialize the script ensureFlagCSSLoaded(); init(); })();