MidjourneyCN

Midjourney 全界面汉化 + 浮动控制面板(⚠️⚠️⚠️需开启开发者模式)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         MidjourneyCN
// @namespace    https://github.com/cwser/midjourney-chinese-plugin
// @version      1.0.2
// @license      MIT
// @description  Midjourney 全界面汉化 + 浮动控制面板(⚠️⚠️⚠️需开启开发者模式)
// @author       cwser
// @homepageURL  https://github.com/cwser/midjourney-chinese-plugin
// @supportURL   https://github.com/cwser/midjourney-chinese-plugin/issues
// @match        https://www.midjourney.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @connect      cdn.jsdelivr.net
// @connect      midjourney.com
// @run-at       document-end
// @noframes
// ==/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();
})();