您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a "View Replies" button under each post with improved performance and styling.
// ==UserScript== // @name SimpCity Replies // @namespace http://tampermonkey.net/ // @version 0.3 // @description Adds a "View Replies" button under each post with improved performance and styling. // @author remuru // @match https://simpcity.su/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect simpcity.su // @license MIT // ==/UserScript== (function() { 'use strict'; // --- Constants --- const BUTTON_TEXT_VIEW = "View Replies"; const BUTTON_TEXT_HIDE = "Hide Replies"; const LOADING_TEXT_BASE = "Loading replies"; const NO_REPLIES_TEXT = "<strong>No replies found.</strong>"; const ERROR_PREFIX = '<strong style="color: red;">Error:</strong>'; // --- CSS Styling --- GM_addStyle(` .sc-replies-button { margin-top: 10px; cursor: pointer; /* Add other button styles as needed - using default browser/site styles for now */ padding: 5px 10px; border: 1px solid #555; background-color: #444; color: #ddd; border-radius: 3px; } .sc-replies-button:hover { background-color: #555; } .sc-replies-container { margin-top: 10px; border: 1px solid #444; padding: 10px; background-color: #2e2e2e; /* Slightly different background */ border-radius: 3px; } .sc-replies-container[data-loading="true"] strong { display: inline-block; /* Needed for animation */ } .sc-replies-loading-dots::after { display: inline-block; animation: sc-ellipsis 1.25s infinite; content: "."; width: 1em; text-align: left; } @keyframes sc-ellipsis { 0% { content: "."; } 33% { content: ".."; } 66% { content: "..."; } } .sc-replies-table { width: 100%; border-collapse: collapse; margin-top: 10px; } .sc-replies-table th, .sc-replies-table td { padding: 8px; border: 1px solid #4a4a4a; /* Slightly adjusted border */ text-align: left; } .sc-replies-table th { background: #3a3a3a; /* Adjusted header background */ text-align: center; font-weight: bold; } .sc-replies-table tr:nth-child(even) { background-color: #333; /* Zebra striping */ } .sc-replies-table td:nth-child(1), /* Post Num */ .sc-replies-table td:nth-child(2) /* Date */ { text-align: center; white-space: nowrap; /* Prevent date/number wrapping */ } .sc-replies-table th:nth-child(1), /* Post Num Header */ .sc-replies-table th:nth-child(2) /* Date Header */ { width: 12%; } .sc-replies-table th:nth-child(3) { /* Reply Header */ width: 76%; text-align: left; } .sc-replies-table a { color: #4b9dff; /* Link color */ } .sc-replies-table a:hover { text-decoration: underline; } `); // --- Helper Functions --- const extractThreadId = (postHeaderLink) => { // Regex remains the same, seems specific enough const match = postHeaderLink?.match(/\.([0-9]+)\/post-/); return match ? match[1] : null; }; const convertLinks = (text) => { if (!text) return ''; // Basic URL regex, find http/https links const urlRegex = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig; // Use textContent first to prevent potential XSS if source has raw HTML const safeText = document.createElement('div'); safeText.textContent = text; // Replace URLs in the safe text return safeText.innerHTML.replace(urlRegex, (url) => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`); }; const createLoadingContainer = (parent) => { const container = document.createElement('div'); container.className = 'sc-replies-container'; container.dataset.loading = "true"; // Mark as loading const loadingText = document.createElement('strong'); loadingText.textContent = LOADING_TEXT_BASE; loadingText.className = 'sc-replies-loading-dots'; // Add class for CSS animation container.appendChild(loadingText); parent.appendChild(container); return container; }; const showError = (container, message) => { container.innerHTML = `${ERROR_PREFIX} ${message}`; container.dataset.loading = "false"; // Ensure loading state is cleared on error }; const fetchAnswers = (searchURL, container) => { console.log("Fetching replies from:", searchURL); GM_xmlhttpRequest({ method: "GET", url: searchURL, onload: (response) => { container.dataset.loading = "false"; // Stop loading animation if (response.status >= 200 && response.status < 300) { const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, "text/html"); // Use a more specific selector if possible, but this seems standard for XenForo search results const answerBlocks = doc.querySelectorAll("li.block-row.block-row--separated.js-inlineModContainer[data-author]"); if (!answerBlocks || answerBlocks.length === 0) { container.innerHTML = NO_REPLIES_TEXT; return; } // Clear loading text container.innerHTML = ""; // Create table structure const table = document.createElement("table"); table.className = "sc-replies-table"; table.innerHTML = ` <thead> <tr> <th>Post #</th> <th>Date</th> <th>Reply Snippet</th> </tr> </thead>`; const tbody = document.createElement("tbody"); const fragment = document.createDocumentFragment(); // Use fragment for efficiency answerBlocks.forEach((block) => { // Use optional chaining ?. for safer access const postLink = block.querySelector('.contentRow-main a[href*="/post-"]'); const postTime = block.querySelector('time.u-dt'); const contentSnippet = block.querySelector('.contentRow-snippet'); // Extract post number more reliably from the link itself if possible let postIdText = 'N/A'; const postIdMatch = postLink?.href?.match(/\/post-(\d+)/); if (postIdMatch) { postIdText = `#${postIdMatch[1]}`; } else { // Fallback to the list item if needed (less reliable) const postIdElement = block.querySelector('.contentRow-minor.contentRow-minor--hideLinks li:nth-child(2)'); // Example: adjust if needed if (postIdElement) postIdText = postIdElement.textContent.trim(); } if (postLink?.href && postTime && contentSnippet) { const row = document.createElement("tr"); // Post number (link) const postIdCell = document.createElement("td"); const linkElement = document.createElement("a"); linkElement.href = postLink.href; linkElement.textContent = postIdText; linkElement.target = "_blank"; linkElement.rel = "noopener noreferrer"; postIdCell.appendChild(linkElement); // Date const timeCell = document.createElement("td"); timeCell.textContent = postTime.textContent.trim(); timeCell.title = postTime.getAttribute('datetime') || postTime.getAttribute('data-time-string') || ''; // Add tooltip with full date if available // Reply content const contentCell = document.createElement("td"); // Use convertLinks for safety and functionality contentCell.innerHTML = convertLinks(contentSnippet.textContent.trim()); row.append(postIdCell, timeCell, contentCell); fragment.appendChild(row); // Append row to fragment } else { console.warn("Skipping reply block, missing required elements:", block); } }); tbody.appendChild(fragment); // Append fragment to tbody table.appendChild(tbody); container.appendChild(table); } else { showError(container, `Failed to load replies. Status: ${response.status}`); } }, onerror: (error) => { console.error("GM_xmlhttpRequest error:", error); showError(container, "Network error while loading replies."); container.dataset.loading = "false"; }, ontimeout: () => { showError(container, "Request timed out while loading replies."); container.dataset.loading = "false"; } }); }; const addAnswerButton = (postElement) => { const mainContainer = postElement.querySelector('.message-main.js-quickEditTarget') || postElement; if (!mainContainer) return; // Cannot add button if container not found // Check if button already exists if (mainContainer.querySelector('.sc-replies-button')) { return; } const btn = document.createElement('button'); btn.textContent = BUTTON_TEXT_VIEW; btn.className = "sc-replies-button"; mainContainer.appendChild(btn); btn.addEventListener('click', () => { let repliesContainer = postElement.querySelector('.sc-replies-container'); if (repliesContainer) { // Toggle visibility const isHidden = repliesContainer.style.display === 'none'; repliesContainer.style.display = isHidden ? 'block' : 'none'; btn.textContent = isHidden ? BUTTON_TEXT_HIDE : BUTTON_TEXT_VIEW; return; } // --- Create container and fetch data --- repliesContainer = createLoadingContainer(mainContainer); btn.textContent = BUTTON_TEXT_HIDE; // Set text immediately // Find post ID (using data-content attribute often present on XenForo posts) const messageContent = postElement.closest('.message[data-content]'); const postId = messageContent?.getAttribute('data-content')?.replace('post-', ''); // Fallback to lb-id if data-content is not present const lbContainer = !postId ? postElement.querySelector('[data-lb-id]') : null; const postIdFallback = lbContainer?.getAttribute('data-lb-id')?.replace('post-', ''); const finalPostId = postId || postIdFallback; if (!finalPostId) { showError(repliesContainer, "Could not determine Post ID."); btn.textContent = BUTTON_TEXT_VIEW; // Revert button text on immediate error btn.disabled = true; // Disable button if critical info missing return; } // Find thread ID const headerLink = postElement.querySelector('.message-attribution-main a[href*="/threads/"]'); const threadId = extractThreadId(headerLink?.getAttribute("href")); if (!threadId) { showError(repliesContainer, "Could not determine Thread ID."); btn.textContent = BUTTON_TEXT_VIEW; btn.disabled = true; return; } // --- IMPORTANT NOTE --- // The hardcoded search ID '165761218177111' might be temporary or session-specific. // If this script stops working, this URL generation is the most likely cause. // A more robust solution might involve interacting with the search page differently, // but that would significantly increase complexity. const searchId = "1"; // Potentially fragile hardcoded value const searchURL = `https://simpcity.su/search/${searchId}/?q=post-${finalPostId}&t=post&c[thread]=${threadId}&o=date`; // Added &o=date to sort by date (optional) fetchAnswers(searchURL, repliesContainer); }); }; // --- Main Execution --- // Function to process posts currently on the page const processPosts = () => { document.querySelectorAll(".message.message--post .message-inner").forEach(addAnswerButton); }; // Initial run processPosts(); })();