MidjourneyCN

Midjourney 中文翻译 + 浮动控制面板(增强动态翻译)

目前為 2025-05-07 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         MidjourneyCN
// @namespace    http://tampermonkey.net/
// @version      1.0.2
// @description  Midjourney 中文翻译 + 浮动控制面板(增强动态翻译)
// @author       G哥
// @match        https://www.midjourney.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @connect      cdn.jsdelivr.net
// ==/UserScript==

(function () {
    'use strict';

    const LANG_URLS = {
        'zh-CN': 'https://cdn.jsdelivr.net/gh/cwser/midjourney-chinese-plugin@main/lang/zh-CN.json',
        'zh-TW': 'https://cdn.jsdelivr.net/gh/cwser/midjourney-chinese-plugin@main/lang/zh-TW.json'
    };
    const CACHE_EXPIRY = 6 * 60 * 60 * 1000;
    const TRANSLATED_ATTR = 'data-translated';

    let currentLang = GM_getValue('language', 'zh-CN');
    let translationEnabled = GM_getValue('translationEnabled', true);
    let dictionaryTimestamp = null;
    let dictionaryStatus = '⏳ 加载中';
    let translationError = null;
    let statusIndicator;

    function fetchTranslationDict(lang) {
        return new Promise((resolve, reject) => {
            if (!LANG_URLS[lang]) {
                translationError = 'Language not supported';
                updateStatusDisplay();
                return reject(new Error(translationError));
            }
            GM_xmlhttpRequest({
                method: 'GET',
                url: LANG_URLS[lang],
                onload: (response) => {
                    if (response.status === 200) {
                        try {
                            const data = JSON.parse(response.responseText);
                            dictionaryTimestamp = new Date().toLocaleString();
                            dictionaryStatus = '✅ 已加载';
                            GM_setValue(`${lang}_cache`, {
                                timestamp: Date.now(),
                                data
                            });
                            translationError = null;
                            updateStatusDisplay();
                            resolve(data);
                        } catch (e) {
                            translationError = `解析错误: ${e.message}`;
                            dictionaryStatus = '❌ 解析错误';
                            updateStatusDisplay();
                            reject(e);
                        }
                    } else {
                        translationError = `加载失败: ${response.statusText}`;
                        dictionaryStatus = '❌ 加载失败';
                        updateStatusDisplay();
                        reject(new Error(translationError));
                    }
                },
                onerror: (err) => {
                    translationError = `网络错误: ${err.message}`;
                    dictionaryStatus = '❌ 网络错误';
                    updateStatusDisplay();
                    reject(err);
                }
            });
        });
    }

    function loadTranslationDict(lang) {
        const cache = GM_getValue(`${lang}_cache`, null);
        if (cache && (Date.now() - cache.timestamp) < CACHE_EXPIRY) {
            dictionaryTimestamp = new Date(cache.timestamp).toLocaleString();
            dictionaryStatus = '✅ 来自缓存';
            return Promise.resolve(cache.data);
        }
        return fetchTranslationDict(lang);
    }

    function translateText(text, dict) {
        try {
            const clean = text.trim();
            return dict[clean] || text;
        } catch (error) {
            translationError = `翻译文本时出错: ${error.message}`;
            updateStatusDisplay();
            return text;
        }
    }

    function translateNode(node, dict) {
        try {
            if (node.nodeType === Node.TEXT_NODE) {
                const original = node.textContent.trim();
                if (!original) return;

                const translated = dict[original];
                if (translated && node.parentNode?.getAttribute(TRANSLATED_ATTR) !== original) {
                    node.textContent = translated;
                    node.parentNode?.setAttribute(TRANSLATED_ATTR, original);
                }
            } else if (node.nodeType === Node.ELEMENT_NODE) {
                if (node.getAttribute(TRANSLATED_ATTR) === '__translated__') return;

                Array.from(node.childNodes).forEach(child => translateNode(child, dict));
                node.setAttribute(TRANSLATED_ATTR, '__translated__');
            }
        } catch (error) {
            translationError = `翻译节点时出错: ${error.message}`;
            updateStatusDisplay();
        }
    }

    function initializeTranslation(dict) {
        try {
            const observer = new MutationObserver(mutations => {
                mutations.forEach(mutation => {
                    if (mutation.type === 'childList') {
                        mutation.addedNodes.forEach(node => translateNode(node, dict));
                    } else if (mutation.type === 'characterData' || mutation.type === 'attributes') {
                        translateNode(mutation.target, dict);
                    }
                });
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true,
                characterData: true,
                attributes: true
            });

            translateNode(document.body, dict);
        } catch (error) {
            translationError = `初始化翻译时出错: ${error.message}`;
            updateStatusDisplay();
        }
    }

    function updateStatusDisplay() {
        if (!statusIndicator) return;
        const panelStatusText = document.getElementById('panel-status-text');
        const panelError = document.getElementById('panel-error');
        const panelErrorText = document.getElementById('panel-error-text');
        const panelUpdateTimeText = document.getElementById('panel-update-time-text');

        if (translationError) {
            panelStatusText.textContent = '异常';
            statusIndicator.textContent = '异常';
            statusIndicator.style.backgroundColor = '#ef4444';
            panelError.classList.remove('hidden');
            panelErrorText.textContent = translationError;
        } else if (translationEnabled) {
            panelStatusText.textContent = '开启';
            statusIndicator.textContent = '正常';
            statusIndicator.style.backgroundColor = '#a5d6a7';
            panelError.classList.add('hidden');
        } else {
            panelStatusText.textContent = '关闭';
            statusIndicator.textContent = '正常';
            statusIndicator.style.backgroundColor = '#cfd8dc';
            panelError.classList.add('hidden');
        }

        const now = new Date();
        const diff = now - new Date(dictionaryTimestamp);
        if (diff < 60 * 1000) {
            panelUpdateTimeText.textContent = '1分钟内';
        } else if (diff < 3600 * 1000) {
            panelUpdateTimeText.textContent = `${Math.floor(diff / 60000)}分钟前`;
        } else if (diff < 86400 * 1000) {
            panelUpdateTimeText.textContent = `${Math.floor(diff / 3600000)}小时前`;
        } else {
            panelUpdateTimeText.textContent = `${Math.floor(diff / 86400000)}天前`;
        }
    }

    function createControlPanel() {
        const panel = document.createElement('div');
        panel.id = 'translation-control-panel';
        panel.classList.add('fixed', 'bottom-2', 'right-2', 'z-50', 'transition-opacity', 'duration-500');
        panel.innerHTML = `
            <div id="mini-status-panel" class="bg-white rounded-lg shadow-md p-2 text-sm cursor-pointer w-16 h-16 flex flex-col justify-center items-center space-y-1 transform transition-transform duration-300">
                <div id="panel-title" class="text-xs font-bold">翻译工具</div>
                <div id="status-indicator" class="rounded-md px-1 py-0.5 text-xs"></div>
            </div>
            <div id="panel-body" class="overflow-hidden max-h-0 opacity-0 transition-all duration-300 absolute bottom-full right-full">
                <div class="mt-3 w-64 p-4 bg-white rounded-2xl shadow-lg space-y-4">
                    <h3 class="text-xl font-bold border-b border-gray-300 pb-2">翻译工具</h3>
                    <div id="panel-info" class="space-y-2">
                        <div id="panel-status" class="flex items-center space-x-2">
                            <span class="font-medium">翻译状态:</span>
                            <span id="panel-status-text"></span>
                        </div>
                        <div id="panel-update-time" class="flex items-center space-x-2">
                            <span class="font-medium">更新时间:</span>
                            <span id="panel-update-time-text"></span>
                        </div>
                        <div id="panel-error" class="flex items-center space-x-2 text-red-500 hidden">
                            <span class="font-medium">翻译错误:</span>
                            <span id="panel-error-text"></span>
                        </div>
                    </div>
                    <button id="toggle-translation" class="w-full py-2 px-4 text-white bg-blue-500 rounded-lg hover:bg-blue-600 transition-colors duration-300">${translationEnabled ? ' 关闭翻译' : ' 开启翻译'}</button>
                    <button id="reload-dictionary" class="w-full py-2 px-4 text-white bg-blue-500 rounded-lg hover:bg-blue-600 transition-colors duration-300"> 重新加载词典</button>
                    <div>
                        <label for="language-selector" class="block font-bold"> 选择语言:</label>
                        <select id="language-selector" class="w-full p-2 border border-gray-300 rounded-md mt-1">
                            <option value="zh-CN" ${currentLang === 'zh-CN' ? 'selected' : ''}>简体中文</option>
                            <option value="zh-TW" ${currentLang === 'zh-TW' ? 'selected' : ''}>繁体中文</option>
                        </select>
                    </div>
                </div>
            </div>
        `;
        document.body.appendChild(panel);

        const body = panel.querySelector('#panel-body');
        const mini = panel.querySelector('#mini-status-panel');
        statusIndicator = document.getElementById('status-indicator');

        mini.addEventListener('click', () => {
            if (body.classList.contains('max-h-0')) {
                body.style.maxHeight = body.scrollHeight + 'px';
                body.classList.remove('max-h-0', 'opacity-0');
                body.classList.add('max-h-full', 'opacity-100');
                mini.classList.add('hidden');
            } else {
                body.style.maxHeight = '0';
                body.classList.remove('max-h-full', 'opacity-100');
                body.classList.add('max-h-0', 'opacity-0');
                mini.classList.remove('hidden');
            }
            panel.classList.add('opacity-100');
            resetAutoHideTimer();
        });

        panel.querySelector('#toggle-translation').addEventListener('click', () => {
            translationEnabled = !translationEnabled;
            GM_setValue('translationEnabled', translationEnabled);
            location.reload();
        });

        panel.querySelector('#reload-dictionary').addEventListener('click', () => {
            GM_setValue(`${currentLang}_cache`, null);
            location.reload();
        });

        panel.querySelector('#language-selector').addEventListener('change', (e) => {
            currentLang = e.target.value;
            GM_setValue('language', currentLang);
            location.reload();
        });

        let hideTimer = null;
        let transparencyTimer = null;
        function resetAutoHideTimer() {
            clearTimeout(hideTimer);
            clearTimeout(transparencyTimer);
            panel.classList.add('opacity-100');
            hideTimer = setTimeout(() => {
                if (!body.classList.contains('max-h-0')) {
                    body.style.maxHeight = '0';
                    body.classList.remove('max-h-full', 'opacity-100');
                    body.classList.add('max-h-0', 'opacity-0');
                    mini.classList.remove('hidden');
                }
            }, 6000);
            transparencyTimer = setTimeout(() => {
                if (!body.classList.contains('opacity-100')) {
                    panel.classList.remove('opacity-100');
                    panel.classList.add('opacity-20');
                }
            }, 5000);
        }

        panel.addEventListener('mouseenter', () => {
            clearTimeout(hideTimer);
            clearTimeout(transparencyTimer);
            panel.classList.add('opacity-100');
        });
        panel.addEventListener('mouseleave', () => resetAutoHideTimer());

        document.addEventListener('click', (e) => {
            if (!panel.contains(e.target)) {
                if (!body.classList.contains('max-h-0')) {
                    body.style.maxHeight = '0';
                    body.classList.remove('max-h-full', 'opacity-100');
                    body.classList.add('max-h-0', 'opacity-0');
                    mini.classList.remove('hidden');
                }
            }
        });

        updateStatusDisplay();
        resetAutoHideTimer();
    }

    if (translationEnabled) {
        loadTranslationDict(currentLang).then(dict => {
            initializeTranslation(dict);
        }).catch(err => console.error('翻译加载失败:', err));
    }

    createControlPanel();
})();