// ==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 // @downloadURL https://update.greasyfork.icu/scripts/535125/MidjourneyCN.user.js // @updateURL https://update.greasyfork.icu/scripts/535125/MidjourneyCN.meta.js // ==/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 = `