您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add reaction buttons to posts on civitai.com
// ==UserScript== // @name Add Reactions to Civitai Posts // @namespace http://tampermonkey.net/ // @version 0.10 // @description Add reaction buttons to posts on civitai.com // @author You // @match https://civitai.com/posts* // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // Reaction types with correct emojis const reactions = { 'Like': { emoji: '👍', key: 'likeCount' }, 'Heart': { emoji: '❤️', key: 'heartCount' }, 'Laugh': { emoji: '😂', key: 'laughCount' }, 'Cry': { emoji: '😢', key: 'cryCount' } }; // Function to send reaction API request async function sendReaction(imageId, reactionType) { try { const response = await fetch('https://civitai.com/api/trpc/reaction.toggle', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ json: { entityId: imageId, entityType: "image", reaction: reactionType, authed: true } }) }); if (response.ok) { return await response.json(); } else { throw new Error(`HTTP ${response.status}`); } } catch (error) { console.error('Reaction failed:', error); return null; } } // Function to create reaction buttons function createReactionButtons(postElement, imageId, stats = {}) { // Check if buttons already exist if (postElement.querySelector('.custom-reactions')) { return; } const reactionsContainer = document.createElement('div'); reactionsContainer.className = 'custom-reactions'; reactionsContainer.style.cssText = ` position: absolute; bottom: 8px; right: 8px; display: flex; gap: 4px; z-index: 20; background: rgba(0, 0, 0, 0.7); border-radius: 20px; padding: 4px 8px; backdrop-filter: blur(4px); `; Object.entries(reactions).forEach(([reactionType, config]) => { const currentCount = stats[config.key] || 0; const button = document.createElement('button'); button.className = 'reaction-btn'; button.style.cssText = ` background: transparent; border: none; cursor: pointer; padding: 4px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: all 0.2s; opacity: 0.8; color: white; font-size: 16px; gap: 2px; min-width: 35px; `; const countSpan = document.createElement('span'); countSpan.className = 'reaction-count'; countSpan.style.cssText = 'font-size: 12px; font-weight: bold;'; countSpan.textContent = currentCount; button.textContent = config.emoji; if (currentCount > 0) { button.appendChild(document.createTextNode(' ')); button.appendChild(countSpan); } // Hover effects button.addEventListener('mouseenter', () => { button.style.opacity = '1'; button.style.transform = 'scale(1.1)'; button.style.background = 'rgba(255, 255, 255, 0.2)'; }); button.addEventListener('mouseleave', () => { button.style.opacity = '0.8'; button.style.transform = 'scale(1)'; button.style.background = 'transparent'; }); // Click handler button.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); // Disable button during request button.style.opacity = '0.5'; button.style.pointerEvents = 'none'; const result = await sendReaction(imageId, reactionType); if (result) { // Update count optimistically const newCount = currentCount + 1; countSpan.textContent = newCount; if (currentCount === 0) { // Add count display if it wasn't there before button.appendChild(document.createTextNode(' ')); button.appendChild(countSpan); } // Success feedback button.style.background = 'rgba(0, 255, 0, 0.3)'; setTimeout(() => { button.style.background = 'transparent'; }, 1000); } else { // Error feedback button.style.background = 'rgba(255, 0, 0, 0.3)'; setTimeout(() => { button.style.background = 'transparent'; }, 1000); } // Restore button button.style.opacity = '0.8'; button.style.pointerEvents = 'auto'; }); reactionsContainer.appendChild(button); }); // Add the reactions container to the post postElement.style.position = 'relative'; postElement.appendChild(reactionsContainer); } // Function to intercept and parse API responses to get image IDs and stats const originalFetch = window.fetch; const imageIdMap = new Map(); const postStatsMap = new Map(); window.fetch = async function(...args) { const response = await originalFetch.apply(this, args); // Check if this is a posts API call if (args[0] && args[0].includes('/api/trpc/post.getInfinite')) { try { const clonedResponse = response.clone(); const data = await clonedResponse.json(); if (data.result?.data?.json?.items) { console.log('Processing', data.result.data.json.items.length, 'posts from API'); data.result.data.json.items.forEach(post => { if (post.images && post.images.length > 0) { const firstImage = post.images[0]; // Map the image URL hash to the actual image ID and stats imageIdMap.set(firstImage.url, { id: firstImage.id, stats: post.stats || {} }); console.log('Mapped image:', firstImage.url, 'to ID:', firstImage.id); } }); // Process posts after API response setTimeout(processPostCards, 1000); } } catch (error) { console.error('Error parsing posts data:', error); } } return response; }; // Function to process posts and add reaction buttons function processPostCards() { console.log('Processing post cards...'); const postCards = document.querySelectorAll('div[id]:not(.reactions-processed)'); console.log('Found', postCards.length, 'unprocessed post cards'); let processed = 0; postCards.forEach(postCard => { const imgElement = postCard.querySelector('img.EdgeImage_image__iH4_q'); if (imgElement && imgElement.src) { // Extract the image hash from the URL const urlParts = imgElement.src.split('/'); const hashPart = urlParts.find(part => part.length > 30 && part.includes('-')); if (hashPart && imageIdMap.has(hashPart)) { const imageData = imageIdMap.get(hashPart); console.log('Creating reactions for image:', imageData.id); createReactionButtons(postCard, imageData.id, imageData.stats); postCard.classList.add('reactions-processed'); processed++; } else if (hashPart) { console.log('No image data found for hash:', hashPart); } else { console.log('Could not extract hash from image URL:', imgElement.src); } } }); console.log('Successfully processed', processed, 'posts'); console.log('Current imageIdMap size:', imageIdMap.size); } // Observer to watch for new posts being loaded const observer = new MutationObserver((mutations) => { let shouldProcess = false; mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1 && (node.matches('div[id]') || node.querySelector('div[id]'))) { shouldProcess = true; } }); }); if (shouldProcess) { // Small delay to ensure all content is loaded setTimeout(processPostCards, 500); } }); // Start observing observer.observe(document.body, { childList: true, subtree: true }); // Process initial posts with multiple attempts for slow loading setTimeout(processPostCards, 2000); setTimeout(processPostCards, 5000); setTimeout(processPostCards, 10000); setTimeout(processPostCards, 15000); // Additional attempt for very slow loading // Also process when scrolling stops (for infinite scroll) - more aggressive let scrollTimeout; window.addEventListener('scroll', () => { clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { console.log('Scroll stopped, processing posts...'); processPostCards(); }, 500); // Reduced delay for faster response }); // Also process periodically to catch any missed posts setInterval(() => { const unprocessedPosts = document.querySelectorAll('div[id]:not(.reactions-processed)').length; if (unprocessedPosts > 0) { console.log('Periodic check: found', unprocessedPosts, 'unprocessed posts'); processPostCards(); } }, 3000); // Check every 3 seconds console.log('Civitai Posts Reactions script v0.10 loaded'); })();