您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add a translate icon to the chat in Torn
// ==UserScript== // @name Torn Chat Translator // @namespace http://tampermonkey.net/ // @version 1.0.4 // @description Add a translate icon to the chat in Torn // @author JESUUS [2353554] // @license MIT // @match https://www.torn.com/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; function getStoredConfig() { return { translateIconSvg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-translate" viewBox="0 0 16 16"><path d="M4.545 6.714 4.11 8H3l1.862-5h1.284L8 8H6.833l-.435-1.286zm1.634-.736L5.5 3.956h-.049l-.679 2.022z"/><path d="M0 2a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v3h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-3H2a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zm7.138 9.995q.289.451.63.846c-.748.575-1.673 1.001-2.768 1.292.178.217.451.635.555.867 1.125-.359 2.08-.844 2.886-1.494.777.665 1.739 1.165 2.93 1.472.133-.254.414-.673.629-.89-1.125-.253-2.057-.694-2.82-1.284.681-.747 1.222-1.651 1.621-2.757H14V8h-3v1.047h.765c-.318.844-.74 1.546-1.272 2.13a6 6 0 0 1-.415-.492 2 2 0 0 1-.94.31"/></svg>', targetLanguage: 'en', // Always translate to English sourceLanguage: 'auto' // Auto-detect source language }; } let CONFIG = getStoredConfig(); function insertTextNaturally(textarea, text) { try { if (document.execCommand && document.queryCommandSupported('insertText')) { const success = document.execCommand('insertText', false, text); if (success) { return; } } } catch (e) { console.log('execCommand failed, using fallback'); } const originalValue = textarea.value; textarea.value = text; textarea.selectionStart = textarea.selectionEnd = text.length; textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); textarea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); textarea.dispatchEvent(new Event('keyup', { bubbles: true, cancelable: true })); textarea.dispatchEvent(new Event('blur', { bubbles: true, cancelable: true })); textarea.dispatchEvent(new Event('focus', { bubbles: true, cancelable: true })); } async function translateText(text, sourceLang = 'auto', targetLang = 'en') { try { const textToTranslate = text.replace(/\n/g, ' ').trim(); const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(textToTranslate)}`; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) { try { const data = JSON.parse(response.responseText); let translatedText = ''; if (data[0] && Array.isArray(data[0])) { translatedText = data[0].map(item => item[0]).join(''); } else { translatedText = data[0][0][0]; } resolve(translatedText); } catch (error) { reject(new Error('Translation error: ' + error.message)); } }, onerror: function(error) { reject(new Error('Connection error: ' + error)); } }); }); } catch (error) { throw new Error('Translation error: ' + error.message); } } function setupAutoClear(textarea) { const parent = textarea.closest('.chat-box-footer, .tt-chat-autocomp, .chat-input-container') || textarea.parentElement; const sendButtons = parent.querySelectorAll('button, input[type="submit"], .send-button, [class*="send"]'); sendButtons.forEach(button => { button.addEventListener('click', () => { setTimeout(() => { if (textarea.value.trim() === '') { textarea.blur(); textarea.focus(); } }, 200); }); }); textarea.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { setTimeout(() => { if (textarea.value.trim() === '') { textarea.blur(); textarea.focus(); } }, 200); } }); } function createLanguageDropdown(parent) { const dropdown = document.createElement('div'); dropdown.className = 'language-dropdown'; dropdown.style.cssText = ` position: absolute; right: 40px; top: 100%; background: rgba(0, 0, 0, 0.9); border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 5px; padding: 5px; z-index: 10000; display: none; min-width: 150px; `; const languages = [ { code: 'en', flag: '🇺🇸', name: 'English' }, { code: 'fr', flag: '🇫🇷', name: 'Français' }, { code: 'es', flag: '🇪🇸', name: 'Español' }, { code: 'de', flag: '🇩🇪', name: 'Deutsch' }, { code: 'it', flag: '🇮🇹', name: 'Italiano' }, { code: 'pt', flag: '🇵🇹', name: 'Português' }, { code: 'ru', flag: '🇷🇺', name: 'Русский' }, { code: 'zh', flag: '🇨🇳', name: '中文' }, { code: 'ja', flag: '🇯🇵', name: '日本語' }, { code: 'ko', flag: '🇰🇷', name: '한국어' } ]; languages.forEach(lang => { const option = document.createElement('div'); option.style.cssText = ` padding: 8px 12px; cursor: pointer; display: flex; align-items: center; gap: 8px; color: white; border-radius: 3px; transition: background-color 0.2s; ${lang.code === CONFIG.targetLanguage ? 'background-color: rgba(255, 255, 255, 0.2);' : ''} `; option.innerHTML = `<span style="font-size: 18px;">${lang.flag}</span><span style="font-size: 12px;">${lang.name}</span>`; option.addEventListener('mouseenter', () => { if (lang.code !== CONFIG.targetLanguage) { option.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'; } }); option.addEventListener('mouseleave', () => { if (lang.code !== CONFIG.targetLanguage) { option.style.backgroundColor = 'transparent'; } }); option.addEventListener('click', () => { CONFIG.targetLanguage = lang.code; showNotification(`Language changed to ${lang.name}`, 'success'); dropdown.style.display = 'none'; // Update selection styling dropdown.querySelectorAll('div').forEach(opt => { opt.style.backgroundColor = 'transparent'; }); option.style.backgroundColor = 'rgba(255, 255, 255, 0.2)'; }); dropdown.appendChild(option); }); return dropdown; } function createTranslateIcon() { const icon = document.createElement('button'); icon.className = 'translate-icon'; icon.title = `Translate to ${CONFIG.targetLanguage.toUpperCase()}`; icon.style.cssText = ` position: absolute; right: 30px; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; padding: 5px; border-radius: 3px; transition: background-color 0.2s; z-index: 1000; `; icon.innerHTML = CONFIG.translateIconSvg; icon.style.cssText += ` color: rgba(255, 255, 255, 0.7); `; icon.addEventListener('mouseenter', () => { icon.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'; icon.style.color = 'rgba(255, 255, 255, 1)'; }); icon.addEventListener('mouseleave', () => { icon.style.backgroundColor = 'transparent'; icon.style.color = 'rgba(255, 255, 255, 0.7)'; }); return icon; } function addTranslateIconToChat() { const chatSelectors = [ 'textarea[placeholder*="message"]', 'textarea[placeholder*="Type your message"]', '.chat-box-footer textarea', '.tt-chat-autocomp', 'textarea.chat-input' ]; chatSelectors.forEach(selector => { const textareas = document.querySelectorAll(selector); textareas.forEach(textarea => { if (textarea.parentElement.querySelector('.translate-icon')) { return; } const parent = textarea.parentElement; if (getComputedStyle(parent).position === 'static') { parent.style.position = 'relative'; } const translateIcon = createTranslateIcon(); const languageDropdown = createLanguageDropdown(parent); // Add padding to textarea to prevent text from going under the icon textarea.style.paddingRight = '60px'; setupAutoClear(textarea); let clickCount = 0; let clickTimer; translateIcon.addEventListener('click', (e) => { console.log('Click event triggered, clickCount:', clickCount); clickCount++; if (clickCount === 1) { // Start timer for single click clickTimer = setTimeout(() => { console.log('Single click - translating'); clickCount = 0; const textToTranslate = textarea.value.trim(); if (!textToTranslate) { showNotification('Nothing to translate!', 'warning'); return; } translateIcon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" stroke-width="2"><animate attributeName="stroke-dasharray" values="0 38;19 19;0 38;0 38" dur="2s" repeatCount="indefinite"/><animate attributeName="stroke-dashoffset" values="0;0;-19;-38" dur="2s" repeatCount="indefinite"/></circle></svg>'; translateIcon.disabled = true; translateText(textToTranslate, CONFIG.sourceLanguage, CONFIG.targetLanguage) .then(translatedText => { setTimeout(() => { textarea.focus(); textarea.select(); insertTextNaturally(textarea, translatedText); textarea.focus(); const protectTranslation = () => { if (textarea.value !== translatedText && textarea.value.trim() === '') { textarea.value = translatedText; } }; const protectionInterval = setInterval(protectTranslation, 100); setTimeout(() => clearInterval(protectionInterval), 2000); showNotification('Text translated successfully!', 'success'); }, 100); }) .catch(error => { console.error('Translation error:', error); showNotification('Translation error: ' + error, 'error'); }) .finally(() => { translateIcon.innerHTML = CONFIG.translateIconSvg; translateIcon.disabled = false; }); }, 300); // Wait 300ms to see if there's a second click } else if (clickCount === 2) { // Double click - show dropdown console.log('Double click - showing dropdown'); clearTimeout(clickTimer); clickCount = 0; languageDropdown.style.display = 'block'; // Hide dropdown when clicking elsewhere const hideDropdown = (event) => { if (!languageDropdown.contains(event.target) && !translateIcon.contains(event.target)) { languageDropdown.style.display = 'none'; document.removeEventListener('click', hideDropdown); } }; setTimeout(() => document.addEventListener('click', hideDropdown), 100); } }); parent.appendChild(translateIcon); parent.appendChild(languageDropdown); }); }); } function showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.textContent = message; notification.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 10px 15px; border-radius: 5px; color: white; font-weight: bold; z-index: 10000; animation: slideIn 0.3s ease-out; `; const colors = { success: '#4CAF50', error: '#f44336', warning: '#ff9800', info: '#2196F3' }; notification.style.backgroundColor = colors[type] || colors.info; const style = document.createElement('style'); style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } `; document.head.appendChild(style); document.body.appendChild(notification); setTimeout(() => { notification.remove(); style.remove(); }, 3000); } function observeDOM() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { setTimeout(addTranslateIconToChat, 100); } }); } }); }); observer.observe(document.body, { childList: true, subtree: true }); } function initialize() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(addTranslateIconToChat, 1000); observeDOM(); }); } else { setTimeout(addTranslateIconToChat, 1000); observeDOM(); } setInterval(addTranslateIconToChat, 5000); } initialize(); })();