Axiom推文翻译

对axiom的推文监控,代币内推文,扫链出现的推文进行翻译

// ==UserScript==
// @name         Axiom推文翻译
// @namespace    http://tampermonkey.net/
// @version      4.6
// @author       @Gufii_666
// @description  对axiom的推文监控,代币内推文,扫链出现的推文进行翻译
// @match        https://axiom.trade/pulse*
// @match        https://axiom.trade/trackers*
// @match        https://axiom.trade/meme/*
// @match        https://axiom.trade/discover*
// @license      MIT
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const TRANSLATION_BASE_URL = 'https://translate.googleapis.com/translate_a/single';
    const CLIENT_PARAM = 'gtx';
    const SOURCE_LANG = 'auto';
    const TARGET_LANG = 'zh-CN';
    const DATA_TYPE = 't';

    const TRANSLATED_TEXT_CLASS = 'localized-content-display';
    const ORIGINAL_DATA_ATTR = 'data-translation-processed-status';
    const ORIGINAL_TEXT_STORE_ATTR = 'data-original-text';
    const UNIQUE_TRANSLATION_ID_ATTR = 'data-translation-id';

    const ONGOING_TRANSLATIONS = new Map();

    // Removed LAZY_SCAN_INTERVAL_MS and lazyScanTimer

    async function obtainLocalizedText(inputString) {
        if (!inputString || typeof inputString !== 'string') {
            return '[Translation Input Error]';
        }

        const queryParams = new URLSearchParams({
            client: CLIENT_PARAM,
            sl: SOURCE_LANG,
            tl: TARGET_LANG,
            dt: DATA_TYPE,
            q: inputString
        });
        const fullUrl = `${TRANSLATION_BASE_URL}?${queryParams.toString()}`;

        try {
            const response = await fetch(fullUrl);
            const data = await response.json();
            if (data && data[0] && Array.isArray(data[0])) {
                return data[0].map(segment => segment[0]).join('');
            }
            throw new Error("Invalid translation response structure.");
        } catch (error) {
            console.error("Content localization failed:", error);
            return '[翻译失败]';
        }
    }

    // Function to apply layout fixes to a specific element
    function applyLayoutFixes(element) {
        if (!element) return;

        const currentDisplay = getComputedStyle(element).display;
        if (currentDisplay.includes('inline') && !currentDisplay.includes('flex') && !currentDisplay.includes('grid')) {
            element.style.display = 'block';
        }

        Object.assign(element.style, {
            maxHeight: "none",
            height: "auto",
            overflow: "visible",
            overflowY: "visible",
            overflowX: "visible",
            minHeight: "unset",
            flexShrink: "0",
            alignSelf: "stretch"
        });

        const gradient = element.querySelector("div[class*='bg-gradient-to-b']");
        if (gradient) {
            gradient.style.display = "none";
        }
    }

    function updateTranslationBox(targetElement, statusOrContent, prepend = false) {
        if (!targetElement || !targetElement.parentElement) return;

        if (!targetElement.dataset.translationId) {
            targetElement.dataset.translationId = Math.random().toString(36).substring(2, 15);
        }
        const translationId = targetElement.dataset.translationId;

        let translationParagraph = targetElement.parentElement.querySelector(`p.${TRANSLATED_TEXT_CLASS}[${UNIQUE_TRANSLATION_ID_ATTR}="${translationId}"]`);

        if (!translationParagraph) {
            translationParagraph = document.createElement("p");
            translationParagraph.classList.add(TRANSLATED_TEXT_CLASS);
            translationParagraph.setAttribute(UNIQUE_TRANSLATION_ID_ATTR, translationId);
            if (prepend) {
                targetElement.parentElement.insertBefore(translationParagraph, targetElement);
            } else {
                targetElement.parentElement.appendChild(translationParagraph);
            }
        }

        translationParagraph.textContent = statusOrContent;

        Object.assign(translationParagraph.style, {
            color: "#FFFFFF",
            fontSize: "14px",
            padding: "8px 12px",
            borderRadius: "6px",
            margin: "8px 0",
            boxShadow: "0 2px 8px rgba(0, 0, 0, 0.3)",
            lineHeight: "1.5",
            textShadow: "1px 1px 2px rgba(0,0,0,0.2)",
            backgroundColor: "",
            border: "",
            fontWeight: "",
            cursor: "",
            opacity: ""
        });


        if (statusOrContent === '[翻译中...]') {
            Object.assign(translationParagraph.style, {
                backgroundColor: "#4A90E2",
                border: "1px solid #337AB7",
                fontWeight: "normal",
                cursor: "wait",
                opacity: "0.8"
            });
            translationParagraph.title = "翻译中,请稍候...";
            translationParagraph.onclick = null;
        } else if (statusOrContent === '[翻译失败]') {
            Object.assign(translationParagraph.style, {
                backgroundColor: "#DC3545",
                border: "1px solid #DC3545",
                fontWeight: "normal",
                cursor: "pointer",
                opacity: "1"
            });
            translationParagraph.title = "点击重试翻译";
            translationParagraph.onclick = (event) => { // Keep the event parameter
                // Prevent event from bubbling up to parent elements and causing navigation
                event.stopPropagation();
                event.preventDefault(); // Also prevent default action if any

                // Clear related attributes and remove from ongoing translations to force re-processing
                targetElement.removeAttribute(ORIGINAL_DATA_ATTR);
                targetElement.removeAttribute(ORIGINAL_TEXT_STORE_ATTR);
                targetElement.removeAttribute(UNIQUE_TRANSLATION_ID_ATTR); // Clear translation ID on retry
                ONGOING_TRANSLATIONS.delete(targetElement);
                translationParagraph.remove(); // Remove the error box
                processElementForTranslation(targetElement); // Re-process the element
            };
        } else { // Successful translation
            Object.assign(translationParagraph.style, {
                backgroundColor: "#2E8B57",
                border: "1px solid #4CAF50",
                fontWeight: "bold",
                cursor: "default",
                opacity: "1"
            });
            translationParagraph.title = "";
            translationParagraph.onclick = null; // No click action for successful translation
        }

        // --- Layout Adjustment Logic ---
        // Apply fixes to the immediate parent of the targetElement
        applyLayoutFixes(targetElement.parentElement);

        // Also apply to relevant ancestors (up to 5 levels)
        let currentParent = targetElement.parentElement;
        let depth = 0;
        while (currentParent && depth < 5) {
            if (currentParent.matches("div.hover\\:bg-primaryStroke\\/20") ||
                currentParent.matches("article.tweet-container_article__0ERPK") ||
                currentParent.matches("div.mt-2.border.border-secondaryStroke.rounded-\\[4px\\].relative.group.overflow-hidden") ||
                currentParent.matches("div.flex-1.min-w-0")
            ) {
                applyLayoutFixes(currentParent);
            }
            currentParent = currentParent.parentElement;
            depth++;
        }
    }

    /**
     * Centralized function to determine element type and initiate translation.
     * This makes sure all entry points (initial scan, observer, retry)
     * funnel through one place for consistent logic.
     */
    async function processElementForTranslation(el) {
        if (!el || ONGOING_TRANSLATIONS.has(el)) {
            return;
        }

        let prepend = true; // Default for tweets
        if (el.matches("p.break-words") && el.tagName === 'P') {
            prepend = false; // Append for bios
        }

        initiateTextTranslation(el, prepend);
    }

    /**
     * Unified function to initiate translation for a text element.
     * This function now handles content change detection more robustly.
     */
    async function initiateTextTranslation(textElement, prepend) {
        const rawContent = textElement.innerText.trim();
        if (!rawContent) {
            // Mark empty elements as processed to avoid re-checking them
            textElement.setAttribute(ORIGINAL_DATA_ATTR, 'true');
            return;
        }

        const storedOriginalText = textElement.getAttribute(ORIGINAL_TEXT_STORE_ATTR);
        const isProcessed = textElement.getAttribute(ORIGINAL_DATA_ATTR) === 'true';
        const hasTranslationBox = textElement.parentElement ? textElement.parentElement.querySelector(`p.${TRANSLATED_TEXT_CLASS}[${UNIQUE_TRANSLATION_ID_ATTR}="${textElement.dataset.translationId}"]`) : null;

        // --- IMPORTANT LOGIC FOR CONTENT CHANGE DETECTION AND RE-PROCESSING ---
        // Determine if we need to force a re-translation.
        let forceRetranslate = false;

        if (isProcessed) {
            // Case 1: Content has genuinely changed for an already processed element.
            if (rawContent !== storedOriginalText) {
                console.log('Content changed for element, forcing re-processing:', rawContent, 'vs', storedOriginalText);
                forceRetranslate = true;
            }
            // Case 2: Previous translation failed, allow retry (click handler will trigger this path).
            else if (hasTranslationBox && hasTranslationBox.textContent.includes('[翻译失败]')) {
                console.log('Previous translation failed, forcing re-processing for retry:', rawContent);
                forceRetranslate = true;
            }
            // Case 3: Already processed successfully and content is same. Do not re-process.
            else {
                return;
            }
        }
        // If not already processed (isProcessed is false), then we should always process.

        // If forceRetranslate is true, or it's a truly new element (isProcessed is false),
        // then we need to reset its state for a fresh translation attempt.
        if (forceRetranslate || !isProcessed) {
            // Remove old translation box if it exists
            if (hasTranslationBox) {
                hasTranslationBox.remove();
            }
            // Clear all tracking attributes to treat as a brand new element
            textElement.removeAttribute(ORIGINAL_DATA_ATTR);
            textElement.removeAttribute(ORIGINAL_TEXT_STORE_ATTR);
            textElement.removeAttribute(UNIQUE_TRANSLATION_ID_ATTR); // Crucial for new translation box
            ONGOING_TRANSLATIONS.delete(textElement); // Remove from cache
        }
        // --- END NEW LOGIC ---

        // At this point, we are confident we need to initiate a new translation process for this element.
        textElement.setAttribute(ORIGINAL_TEXT_STORE_ATTR, rawContent); // Store current content as original

        // Immediately show "Translating..." status before API call
        updateTranslationBox(textElement, '[翻译中...]', prepend);

        // Store the translation promise to prevent multiple simultaneous requests
        const translationPromise = obtainLocalizedText(rawContent).then(localizedContent => {
            updateTranslationBox(textElement, localizedContent, prepend);
            textElement.setAttribute(ORIGINAL_DATA_ATTR, 'true'); // Mark as processed after API call result
        }).catch(() => {
            updateTranslationBox(textElement, '[翻译失败]', prepend);
            textElement.setAttribute(ORIGINAL_DATA_ATTR, 'true'); // Mark as processed even if failed
        }).finally(() => {
            ONGOING_TRANSLATIONS.delete(textElement); // Remove from cache when done
        });

        ONGOING_TRANSLATIONS.set(textElement, translationPromise);
    }


    function performFullPageScan() {
        // Clear ALL previous translation boxes.
        document.querySelectorAll(`.${TRANSLATED_TEXT_CLASS}`).forEach(el => el.remove());

        // Process all known translatable text elements.
        // Their internal content change detection will handle if they need actual re-translation.
        document.querySelectorAll(
            "p.tweet-body_root__ChzUj," +
            "p.text-textSecondary.mt-1.whitespace-pre-wrap," +
            "div.mt-2.border.border-secondaryStroke.rounded-\\[4px\\].relative.group.overflow-hidden p.text-textSecondary.mt-1," +
            "p.break-words"
        ).forEach(processElementForTranslation);
    }


    let lastPathname = window.location.pathname;
    let scanTimeoutId = null;

    const contentWatcher = new MutationObserver((mutations) => {
        // Primary trigger for full page scan on URL change (SPA navigation)
        if (window.location.pathname !== lastPathname) {
            lastPathname = window.location.pathname;
            clearTimeout(scanTimeoutId); // Clear any pending scan from previous URL
            scanTimeoutId = setTimeout(performFullPageScan, 500); // Debounce full scan
            return; // Don't process individual mutations for a changing page
        }

        // Process individual mutations for content added or attributes changed on the *same* page
        for (const mutationRecord of mutations) {
            // Check for added nodes (new elements appearing)
            if (mutationRecord.type === 'childList' && mutationRecord.addedNodes.length > 0) {
                for (const addedNode of mutationRecord.addedNodes) {
                    // Only query if it's an element node and has querySelector (e.g., skip text nodes)
                    if (addedNode.nodeType !== 1 || !addedNode.querySelector) continue;

                    // Query for all possible translatable P elements within the added node or if addedNode is the P itself
                    addedNode.querySelectorAll(
                        "p.tweet-body_root__ChzUj," +
                        "p.text-textSecondary.mt-1.whitespace-pre-wrap," +
                        "div.mt-2.border.border-secondaryStroke.rounded-\\[4px\\].relative.group.overflow-hidden p.text-textSecondary.mt-1," +
                        "p.break-words"
                    ).forEach(processElementForTranslation);
                }
            }
            // --- NEW: Handle characterData changes for in-place text updates ---
            else if (mutationRecord.type === 'characterData') {
                // The target of characterData is the TextNode itself.
                // We need to get its parent element to process.
                const parentElement = mutationRecord.target.parentElement;
                if (parentElement) {
                    // Check if this parent is one of our translatable elements
                    if (parentElement.matches("p.tweet-body_root__ChzUj") ||
                        parentElement.matches("p.text-textSecondary.mt-1.whitespace-pre-wrap") ||
                        parentElement.matches("div.mt-2.border.border-secondaryStroke.rounded-\\[4px\\].relative.group.overflow-hidden p.text-textSecondary.mt-1") ||
                        parentElement.matches("p.break-words")) {
                        processElementForTranslation(parentElement);
                    }
                }
            }
            // --- END NEW ---
        }
    });

    // Configure MutationObserver to observe characterData as well, and optimize attributeFilter
    contentWatcher.observe(document.body, {
        childList: true,
        subtree: true,
        characterData: true, // Crucial: now observe text content changes directly
        attributes: false, // Reverted: Only observe specific attributes if absolutely necessary, to avoid performance issues
        // attributeFilter: ['class', 'style', ORIGINAL_DATA_ATTR, ORIGINAL_TEXT_STORE_ATTR] // Removed as 'attributes: false'
    });

    // Initial full scan when the script first runs
    performFullPageScan();

    // Removed lazyScanTimer

    // Ensure timers are cleared on page unload
    window.addEventListener('beforeunload', () => {
        if (scanTimeoutId) clearTimeout(scanTimeoutId);
        // if (lazyScanTimer) clearInterval(lazyScanTimer); // Removed
    });

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址