您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
text vote buttons, comment expando preview with inline replies, and vote tally estimation
// ==UserScript== // @name Old Reddit Extended (Works with RES) // @namespace https://old.reddit.com/ // @version 2.3.1 // @description text vote buttons, comment expando preview with inline replies, and vote tally estimation // @license MIT // @author greenwithenvy // @match *://old.reddit.com/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // ==/UserScript== (function() { 'use strict'; // load external css for old.reddit styling const cssUrl = "https://greenwenvy.github.io/homework/ORE.css"; // update if needed const link = document.createElement("link"); link.rel = "stylesheet"; link.href = cssUrl; document.head.appendChild(link); // load marked library for markdown parsing in comment previews if (!window.marked) { let markedScript = document.createElement('script'); markedScript.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js'; document.head.appendChild(markedScript); } // helper: parse markdown text to html if possible function parseMarkdown(text) { if (window.marked) { return window.marked.parse(text); } return text; } // function to style text buttons function styleTextButton(button, hoverColor) { button.style.color = '#888'; button.style.fontWeight = 'bold'; button.style.padding = '0 3px'; button.style.cursor = 'pointer'; button.addEventListener('mouseenter', () => { button.style.color = hoverColor; }); button.addEventListener('mouseleave', () => { if (!button.classList.contains('active')) { button.style.color = '#888'; } }); } // toggle active state of vote button function toggleButtonState(button, hoverColor) { if (button.classList.contains('active')) { button.classList.remove('active'); button.style.color = '#888'; } else { button.classList.add('active'); button.style.color = hoverColor; } } // replace vote buttons with text buttons function replaceVoteButtons(post) { if (post.dataset.modified) return; post.dataset.modified = 'true'; const upvote = post.querySelector('.arrow.up'); const downvote = post.querySelector('.arrow.down'); const buttonContainer = post.querySelector('.flat-list.buttons'); const expando = post.querySelector('.comment-expando'); if (upvote && downvote && buttonContainer) { const upvoteBtn = document.createElement('a'); upvoteBtn.href = '#'; upvoteBtn.textContent = 'upvote'; styleTextButton(upvoteBtn, '#ff4500'); const downvoteBtn = document.createElement('a'); downvoteBtn.href = '#'; downvoteBtn.textContent = 'downvote'; styleTextButton(downvoteBtn, '#7193ff'); upvoteBtn.addEventListener('click', (e) => { e.preventDefault(); upvote.click(); toggleButtonState(upvoteBtn, '#ff4500'); }); downvoteBtn.addEventListener('click', (e) => { e.preventDefault(); downvote.click(); toggleButtonState(downvoteBtn, '#7193ff'); }); if (expando) { expando.insertAdjacentElement('afterend', upvoteBtn); upvoteBtn.insertAdjacentElement('afterend', downvoteBtn); } else { buttonContainer.insertBefore(downvoteBtn, buttonContainer.firstChild); buttonContainer.insertBefore(upvoteBtn, buttonContainer.firstChild); } } } // process posts that already exist function processExistingPosts() { document.querySelectorAll('.thing').forEach(replaceVoteButtons); } processExistingPosts(); // observe new posts being added dynamically const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === 1 && node.classList.contains('thing')) { replaceVoteButtons(node); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); // add expando button for comment preview on posts document.querySelectorAll('.thing.link').forEach(post => { let commentsAnchor = post.querySelector('a.comments'); let postUrl = commentsAnchor ? commentsAnchor.href : null; if (!postUrl) return; let expandoBtn = document.createElement('span'); expandoBtn.textContent = '[+ preview comments]'; expandoBtn.className = 'comment-expando'; expandoBtn.addEventListener('click', function() { // toggle comment preview on/off let preview = expandoBtn.nextSibling; if (preview && preview.classList && preview.classList.contains('comment-preview')) { preview.remove(); expandoBtn.textContent = '[+ preview comments]'; return; } loadComments(expandoBtn, postUrl); }); let buttonContainer = post.querySelector('.flat-list.buttons'); if (buttonContainer) { buttonContainer.prepend(expandoBtn); } }); // load comments using gm_xmlhttprequest function loadComments(expandoBtn, postUrl) { let loadingText = document.createElement('div'); loadingText.className = 'comment-loading'; loadingText.textContent = 'loading comments...'; expandoBtn.after(loadingText); GM_xmlhttpRequest({ method: 'GET', url: postUrl + '.json', onload: function(response) { loadingText.remove(); let data = JSON.parse(response.responseText); let comments = data[1].data.children.filter(c => c.kind === "t1").map(c => c.data); let previewDiv = document.createElement('div'); previewDiv.className = 'comment-preview'; if (comments.length) { let currentCommentIndex = 0; previewDiv.innerHTML = ''; previewDiv.appendChild(formatComment(comments[currentCommentIndex], postUrl)); let nextBtn = document.createElement('span'); nextBtn.textContent = '[next]'; nextBtn.className = 'comment-next'; nextBtn.addEventListener('click', function() { currentCommentIndex = (currentCommentIndex + 1) % comments.length; previewDiv.innerHTML = ''; previewDiv.appendChild(formatComment(comments[currentCommentIndex], postUrl)); previewDiv.appendChild(nextBtn); }); previewDiv.appendChild(nextBtn); } else { previewDiv.innerHTML = '<p>no comments yet.</p>'; } expandoBtn.textContent = '[- hide comments]'; expandoBtn.after(previewDiv); } }); } // create a dom element for a comment with extra features and inline replies function formatComment(comment, postUrl, depth = 0) { const container = document.createElement('div'); container.className = 'comment-preview-item'; container.style.marginBottom = '10px'; container.style.padding = '5px'; container.style.borderBottom = '1px dashed #ccc'; // comment main content const contentP = document.createElement('p'); contentP.style.margin = '0'; const authorLink = document.createElement('a'); authorLink.href = postUrl + comment.id; authorLink.target = '_blank'; authorLink.className = 'comment-author'; authorLink.style.fontWeight = 'bold'; authorLink.textContent = comment.author; contentP.appendChild(authorLink); // add colon and markdown-parsed comment body const colonText = document.createTextNode(': '); contentP.appendChild(colonText); const commentTextDiv = document.createElement('div'); commentTextDiv.innerHTML = parseMarkdown(comment.body); contentP.appendChild(commentTextDiv); // NOTE: Removed score span from content paragraph container.appendChild(contentP); // comment metadata (timestamp, edited, replies count) const metaP = document.createElement('p'); metaP.style.margin = '0'; metaP.style.fontSize = '10px'; metaP.style.color = '#555'; // Create score span const scoreSpan = document.createElement('span'); scoreSpan.className = 'comment-score'; scoreSpan.style.color = '#888'; scoreSpan.textContent = `(${addCommas(comment.score)})`; const createdTime = new Date(comment.created_utc * 1000).toLocaleString(); let metaText = createdTime; if (comment.edited && comment.edited !== false) { metaText += " (edited)"; } let repliesCount = 0; if (comment.replies && comment.replies.data && comment.replies.data.children) { repliesCount = comment.replies.data.children.filter(child => child.kind === "t1").length; } if (repliesCount > 0 && depth === 0) { metaText += ` - ${repliesCount} repl${repliesCount === 1 ? 'y' : 'ies'}`; } // Add score span to metadata and then the text metaP.appendChild(scoreSpan); metaP.appendChild(document.createTextNode(' ' + metaText)); container.appendChild(metaP); // if comment has image url in body, show preview let urlRegex = /(https?:\/\/\S+\.(jpg|jpeg|png|gif))/i; let match = comment.body.match(urlRegex); if (match) { const img = document.createElement('img'); img.src = match[1]; img.alt = "image"; img.style.maxWidth = "200px"; img.style.maxHeight = "200px"; img.style.marginTop = "5px"; img.style.border = "1px solid #ccc"; container.appendChild(img); } // add inline "load replies" if available and if at top level (depth 0) if (depth === 0 && comment.replies && comment.replies.data && comment.replies.data.children.length > 0) { const loadRepliesBtn = document.createElement('span'); loadRepliesBtn.textContent = ' [load replies]'; loadRepliesBtn.style.fontSize = '10px'; loadRepliesBtn.style.color = '#0079d3'; loadRepliesBtn.style.cursor = 'pointer'; loadRepliesBtn.addEventListener('click', function() { let existing = container.querySelector('.comment-replies'); if (existing) { existing.remove(); loadRepliesBtn.textContent = ' [load replies]'; } else { const repliesContainer = formatReplies(comment.replies.data.children, depth + 1); container.appendChild(repliesContainer); loadRepliesBtn.textContent = ' [hide replies]'; } }); container.appendChild(loadRepliesBtn); } // add upvote/downvote buttons for comment preview const voteContainer = document.createElement('div'); voteContainer.style.marginTop = '5px'; const upvoteLink = document.createElement('a'); // append parameters so that the new tab knows to auto-vote upvoteLink.href = `${postUrl}${comment.id}?vote=up&cid=${comment.id}`; upvoteLink.textContent = 'upvote'; upvoteLink.style.fontSize = '10px'; upvoteLink.style.color = '#ff4500'; upvoteLink.style.marginRight = '5px'; upvoteLink.target = '_blank'; const downvoteLink = document.createElement('a'); downvoteLink.href = `${postUrl}${comment.id}?vote=down&cid=${comment.id}`; downvoteLink.textContent = 'downvote'; downvoteLink.style.fontSize = '10px'; downvoteLink.style.color = '#7193ff'; downvoteLink.target = '_blank'; voteContainer.appendChild(upvoteLink); voteContainer.appendChild(downvoteLink); container.appendChild(voteContainer); return container; } // new version of formatReplies with "load more replies" functionality function formatReplies(replies, depth) { const container = document.createElement('div'); container.className = 'comment-replies'; container.style.marginLeft = '20px'; container.style.borderLeft = '1px dashed #ccc'; container.style.paddingLeft = '5px'; let repliesPerPage = 3; let currentIndex = 0; function loadNextReplies() { let loadedCount = 0; let existingBtn = container.querySelector('.load-more-btn'); if (existingBtn) existingBtn.remove(); while (currentIndex < replies.length && loadedCount < repliesPerPage) { const reply = replies[currentIndex]; currentIndex++; if (reply.kind !== 't1') continue; const replyElem = formatComment(reply.data, "", depth); container.appendChild(replyElem); loadedCount++; } if (currentIndex < replies.length) { const loadMoreBtn = document.createElement('span'); loadMoreBtn.textContent = ' [load more replies]'; loadMoreBtn.className = 'load-more-btn'; loadMoreBtn.style.fontSize = '10px'; loadMoreBtn.style.color = '#0079d3'; loadMoreBtn.style.cursor = 'pointer'; loadMoreBtn.addEventListener('click', function() { loadNextReplies(); }); container.appendChild(loadMoreBtn); } } loadNextReplies(); return container; } // utility: add commas to numbers function addCommas(number) { return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } // estimate vote breakdown and display upvote/downvote/totals function estimatePostScoreVotes() { document.querySelectorAll('.linkinfo .score').forEach(linkinfoScore => { const numberElement = linkinfoScore.querySelector('.number'); if (!numberElement) return; const points = parseInt(numberElement.textContent.replace(/[^0-9]/g, ''), 10); const percentageMatch = linkinfoScore.textContent.match(/([0-9]{1,3})\s?%/); const percentage = percentageMatch ? parseInt(percentageMatch[1], 10) : 0; if (points !== 50 && percentage !== 50) { const upvotes = Math.round(points * percentage / (2 * percentage - 100)); const downvotes = upvotes - points; const totalVotes = upvotes + downvotes; const css = ` .linkinfo .upvotes { font-size: 80%; color: orangered; margin-left: 5px; } .linkinfo .downvotes { font-size: 80%; color: #5f99cf; margin-left: 5px; } .linkinfo .totalvotes { font-size: 80%; margin-left: 5px; } `; const style = document.createElement('style'); style.innerHTML = css; document.head.appendChild(style); linkinfoScore.insertAdjacentHTML('afterend', ` <span class="upvotes"><span class="number">${addCommas(upvotes)}</span> <span class="word">${upvotes > 1 ? 'upvotes' : 'upvote'}</span></span> <span class="downvotes"><span class="number">${addCommas(downvotes)}</span> <span class="word">${downvotes > 1 ? 'downvotes' : 'downvote'}</span></span> <span class="totalvotes"><span class="number">${addCommas(totalVotes)}</span> <span class="word">${totalVotes > 1 ? 'votes' : 'vote'}</span></span> `); } }); } // add detailed upvote/downvote info to post taglines async function addUpvoteDownvoteInfo() { const linkListing = document.querySelector(".linklisting") || (document.querySelector(".Post") ? document.querySelector(".Post").parentElement : null); if (!linkListing) return; const linkDivs = linkListing.getElementsByClassName("link"); const promises = Array.from(linkDivs).map(async (linkDiv) => { const commentsLink = linkDiv.querySelector(".comments"); if (!commentsLink) return; const commentsPage = await httpGet(`${commentsLink.href}?limit=1&depth=1`); const scoreSection = /<div class=(["'])score\1[\s\S]*?<\/div>/.exec(commentsPage); if (!scoreSection) return; const scoreMatch = /<span class=(["'])number\1>([\d\,\.]*)<\/span>/.exec(scoreSection[0]); if (!scoreMatch) return; const score = parseInt(scoreMatch[2].replace(/[,\.]/g, ''), 10); const upvotesPercentageMatch = /\((\d+)\s*%[^\)]*\)/.exec(scoreSection[0]); if (!upvotesPercentageMatch) return; const upvotesPercentage = parseInt(upvotesPercentageMatch[1], 10); const upvotes = calcUpvotes(score, upvotesPercentage); const downvotes = upvotes !== "--" ? score - upvotes : "--"; updateTagline(linkDiv, upvotes, downvotes, upvotesPercentage); }); await Promise.all(promises); } // calculate upvotes based on score and percentage function calcUpvotes(score, upvotesPercentage) { if (score === 0) return "--"; return Math.round(((upvotesPercentage / 100) * score) / (2 * (upvotesPercentage / 100) - 1)); } // update the post tagline with vote info function updateTagline(linkDiv, upvotes, downvotes, upvotesPercentage) { const taglineParagraph = linkDiv.querySelector(".tagline") || (linkDiv.querySelector(".Post div[data-test-id='post-content']") ? linkDiv.querySelector(".Post div[data-test-id='post-content']").querySelector(".tagline") : null); if (!taglineParagraph) return; let upvoteSpan = taglineParagraph.querySelector(".res_post_ups"); let downvoteSpan = taglineParagraph.querySelector(".res_post_downs"); let percentageSpan = taglineParagraph.querySelector(".res_post_percentage"); if (!upvoteSpan || !downvoteSpan || !percentageSpan) { const updownInfoSpan = document.createElement("span"); upvoteSpan = createVoteSpan("res_post_ups", upvotes, "#FF8B24"); downvoteSpan = createVoteSpan("res_post_downs", downvotes, "#9494FF"); percentageSpan = createVoteSpan("res_post_percentage", `${upvotesPercentage}%`, "#00A000"); updownInfoSpan.append("(", upvoteSpan, "|", downvoteSpan, "|", percentageSpan, ") "); taglineParagraph.insertBefore(updownInfoSpan, taglineParagraph.firstChild); } else { upvoteSpan.textContent = upvotes; downvoteSpan.textContent = downvotes; percentageSpan.textContent = `${upvotesPercentage}%`; } } // helper: create a vote info span function createVoteSpan(className, textContent, color) { const span = document.createElement("span"); span.classList.add(className); span.style.color = color; span.textContent = textContent; return span; } // helper: perform http get request async function httpGet(url) { const response = await fetch(url); return response.text(); } // run vote estimation and info update on window load window.addEventListener('load', () => { estimatePostScoreVotes(); addUpvoteDownvoteInfo(); autoVoteComment(); // check for auto vote param in url }); // allow refreshing vote info with shift+P window.addEventListener('keydown', (event) => { if (event.shiftKey && event.key === 'P') { estimatePostScoreVotes(); addUpvoteDownvoteInfo(); } }); // auto-vote function: if the url contains vote parameters, auto-click the corresponding arrow on the comment function autoVoteComment() { const params = new URLSearchParams(window.location.search); const vote = params.get('vote'); // 'up' or 'down' const cid = params.get('cid'); // comment id if (!vote || !cid) return; // comment element id in old reddit is typically "thing_t1_" + comment id const commentElem = document.getElementById('thing_t1_' + cid); if (!commentElem) return; const arrow = vote === 'up' ? commentElem.querySelector('.arrow.up') : commentElem.querySelector('.arrow.down'); if (arrow) { arrow.click(); } } // Debug mode - set to true to see console logs const DEBUG = false; // Function to log debug messages function debugLog(...args) { if (DEBUG) { console.log('[View Counter]', ...args); } } // Cache for post view data to avoid duplicate requests const viewCountCache = {}; // Function to format large numbers function formatNumber(num) { if (num === null || num === undefined || num === 0) return '? views'; if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M views'; } else if (num >= 1000) { return (num / 1000).toFixed(1) + 'K views'; } else { return num + ' views'; } } // Function to extract post ID from an element or URL function getPostId(element) { // If given an element, extract the ID from its data attributes or classes if (element) { // For RES expanded posts if (element.dataset.fullname) { return element.dataset.fullname.replace('t3_', ''); } // For link IDs in classes const idClass = Array.from(element.classList).find(c => c.startsWith('id-t3_')); if (idClass) { return idClass.replace('id-t3_', ''); } // For RES thing IDs const thingId = element.getAttribute('data-fullname'); if (thingId && thingId.startsWith('t3_')) { return thingId.replace('t3_', ''); } // From permalink const permalink = element.querySelector('a.permalink'); if (permalink && permalink.href) { const permalinkMatch = permalink.href.match(/\/comments\/([a-z0-9]+)\//i); if (permalinkMatch) { return permalinkMatch[1]; } } } // Extract from URL (for post pages) const urlMatch = window.location.pathname.match(/\/comments\/([a-z0-9]+)\//i); return urlMatch ? urlMatch[1] : null; } // Function to fetch post data from Reddit's API function fetchPostData(postId, targetElements) { // Skip if already in cache if (viewCountCache[postId]) { targetElements.forEach(el => { insertViewCount(el, viewCountCache[postId]); }); return; } // Create URL for JSON API - We need to use the new Reddit API endpoint for view count const jsonUrl = `https://www.reddit.com/by_id/t3_${postId}.json`; debugLog('Fetching data for post', postId, 'from', jsonUrl); // Fetch data using GM_xmlhttpRequest to avoid CORS issues GM_xmlhttpRequest({ method: 'GET', url: jsonUrl, headers: { 'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json' }, onload: function(response) { try { // Parse the JSON response const data = JSON.parse(response.responseText); debugLog('Received data for post', postId, data); // Extract the post data const postData = data.data.children[0].data; // Get the view count - it's under different properties depending on the API version let viewCount = null; // Check multiple possible locations if (postData.view_count !== undefined) { viewCount = postData.view_count; } else if (postData.viewed !== undefined) { viewCount = postData.viewed; } else if (postData.num_views !== undefined) { viewCount = postData.num_views; } else if (postData.viewCount !== undefined) { viewCount = postData.viewCount; } else { // If no view count, try to estimate it based on score and upvote ratio const score = postData.score || 0; const ratio = postData.upvote_ratio || 0.5; const estimatedUpvotes = Math.round(score / (2 * ratio - 1)); // Very rough estimate: typically views are 10-100x upvotes viewCount = estimatedUpvotes * 25; debugLog('Estimated view count:', viewCount, 'based on score:', score, 'and ratio:', ratio); } debugLog('View count for post', postId, ':', viewCount); // If post has no view count data, make an API call to new Reddit if (!viewCount || viewCount === 0) { fetchNewRedditViewCount(postId, targetElements); return; } // Cache the result viewCountCache[postId] = viewCount; // Insert view count into all target elements targetElements.forEach(el => { insertViewCount(el, formatNumber(viewCount)); }); } catch (error) { console.error('Old Reddit View Counter error:', error); // Still attempt to fetch from new Reddit as fallback fetchNewRedditViewCount(postId, targetElements); } }, onerror: function(error) { console.error('Failed to fetch post data:', error); fetchNewRedditViewCount(postId, targetElements); } }); } // Fallback function to try fetching view count from new Reddit function fetchNewRedditViewCount(postId, targetElements) { const newRedditUrl = `https://www.reddit.com/comments/${postId}/.json`; debugLog('Trying new Reddit API for post', postId); GM_xmlhttpRequest({ method: 'GET', url: newRedditUrl, headers: { 'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json' }, onload: function(response) { try { // Parse the JSON response const data = JSON.parse(response.responseText); // Extract the post data from the first element of the array const postData = data[0].data.children[0].data; // Try to find view count in various properties let viewCount = null; if (postData.view_count !== undefined && postData.view_count !== null) { viewCount = postData.view_count; } else if (postData.viewCount !== undefined && postData.viewCount !== null) { viewCount = postData.viewCount; } else if (postData.num_views !== undefined && postData.num_views !== null) { viewCount = postData.num_views; } else { // If no direct view count, use estimates based on votes const totalVotes = postData.ups + postData.downs; // Views are typically 10-100x votes viewCount = totalVotes * 25; debugLog('Estimated view count from votes:', viewCount); } debugLog('New Reddit view count for post', postId, ':', viewCount); if (viewCount) { // Cache the result viewCountCache[postId] = viewCount; // Insert view count into all target elements targetElements.forEach(el => { insertViewCount(el, formatNumber(viewCount)); }); } else { // Use a placeholder if no view count available targetElements.forEach(el => { insertViewCount(el, '? views'); }); } } catch (error) { console.error('New Reddit View Counter error:', error); // Use a placeholder targetElements.forEach(el => { insertViewCount(el, '? views'); }); } }, onerror: function(error) { console.error('Failed to fetch from New Reddit:', error); // Use a placeholder targetElements.forEach(el => { insertViewCount(el, '? views'); }); } }); } // Function to insert view count into the tagline function insertViewCount(element, formattedViews) { // Find the tagline within the element const tagline = element.querySelector('.tagline'); if (!tagline) return; // Skip if already has view count if (tagline.querySelector('.view-count')) return; // Create view count element const viewElement = document.createElement('span'); viewElement.className = 'view-count'; viewElement.textContent = formattedViews; viewElement.style.marginRight = '6px'; viewElement.style.color = '#888'; viewElement.style.fontSize = '0.9em'; // Insert at the beginning of the tagline tagline.insertBefore(viewElement, tagline.firstChild); } // Function to process a single post function processPost(postElement) { const postId = getPostId(postElement); if (!postId) { debugLog('Could not find post ID for element', postElement); return; } debugLog('Processing post', postId); fetchPostData(postId, [postElement]); } // Function to process all posts on the page function processAllPosts() { // Handle individual post page if (window.location.pathname.includes('/comments/')) { const postId = getPostId(); if (!postId) return; const selfPost = document.querySelector('.thing.self'); if (selfPost) { fetchPostData(postId, [selfPost]); } return; } // Handle post listings (frontpage, subreddit, etc.) const posts = document.querySelectorAll('.thing.link:not([data-processed-views])'); posts.forEach(post => { // Mark as processed post.setAttribute('data-processed-views', 'true'); processPost(post); }); } // Handle RES expandos (when a post is expanded) function handleResExpando() { document.addEventListener('click', function(e) { // Give time for RES to expand the post setTimeout(() => { const expandedPosts = document.querySelectorAll('.res-expando-box:not([data-processed-views])'); expandedPosts.forEach(post => { post.setAttribute('data-processed-views', 'true'); // Find the parent post element const parentPost = post.closest('.thing'); if (parentPost) { processPost(parentPost); } }); }, 500); }); } // Handle RES never-ending Reddit (when new posts are loaded) function handleNeverEndingReddit() { // Create a mutation observer to detect when new posts are added const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.addedNodes && mutation.addedNodes.length > 0) { // Process new posts processAllPosts(); } }); }); // Start observing the siteTable and any potential parent containers const container = document.getElementById('siteTable') || document.body; observer.observe(container, { childList: true, subtree: true }); } // Main function function initialize() { debugLog('Initializing View Counter'); // Process all current posts processAllPosts(); // Handle RES expandos handleResExpando(); // Handle RES never-ending Reddit handleNeverEndingReddit(); // Periodically check for new posts that might not trigger the mutation observer setInterval(processAllPosts, 2000); } // Initialize after a short delay to ensure the page is loaded setTimeout(initialize, 500); })();