您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
v2.8基础: 增加"重试缩写"按钮再次处理已翻译文本; 增加内存缓存; 状态栏移到输入框内部; 增加翻译重试按钮.
当前为
// ==UserScript== // @name Telegram 输入框翻译并发送 (v2.9 - 重试缩写+缓存+内部状态栏) // @namespace http://tampermonkey.net/ // @version 2.9 // @description v2.8基础: 增加"重试缩写"按钮再次处理已翻译文本; 增加内存缓存; 状态栏移到输入框内部; 增加翻译重试按钮. // @author Your Name / AI Assistant // @match https://web.telegram.org/k/* // @match https://web.telegram.org/a/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @connect api.ohmygpt.com // @icon https://upload.wikimedia.org/wikipedia/commons/thumb/8/82/Telegram_logo.svg/48px-Telegram_logo.svg.png // ==/UserScript== (function() { 'use strict'; // --- Configuration --- const OHMYGPT_API_KEY = "sk-RK1MU6Cg6a48fBecBBADT3BlbKFJ4C209a954d3b4428b54b"; // Replace with your actual key if different const OHMYGPT_API_ENDPOINT = "https://api.ohmygpt.com/v1/chat/completions"; const INPUT_TRANSLATE_MODEL = "gpt-4o-mini"; const MAX_CACHE_SIZE = 100; // --- Translation Prompt (Unchanged) --- const TRANSLATION_PROMPT = ` Role: You are a professional translator executing a specific task. Task: Translate the user's Chinese or Burmese text, OR existing English text, into US-style English, adhering strictly to the output requirements below. Apply the abbreviation rules even if the input is already English. Strict Output Requirements: 1. **Style:** Use US-style English with common letter-based abbreviations (e.g., u, ur, r, thx, &, bfr, frst, tmrw, nxt). 2. **Sophistication:** Maintain a high English level with polished, articulate, and sophisticated word choices despite abbreviations. 3. **Meaning:** Preserve the full original meaning. Include question marks if the original is a question. 4. **Punctuation:** Do NOT end the translation with a period (.). 5. **Abbreviations:** ONLY use letter-based abbreviations. ABSOLUTELY NO number-based abbreviations (NO "2" for "to", NO "4" for "for"). Use "to", "for". Double-check your output for numbers in abbreviations. 6. **Format:** Output ONLY the translated text. NO explanations, NO notes, NO apologies, NO preliminary remarks. If translation/abbreviation is unnecessary (e.g., proper nouns, codes), return the original text unmodified. Input Text to Translate/Abbreviate: {text_to_translate} `; // Updated prompt description slightly // Selectors const INPUT_SELECTOR = 'div.input-message-input[contenteditable="true"]'; const SEND_BUTTON_SELECTOR = 'button.btn-send'; const INPUT_AREA_CONTAINER_SELECTOR = '.chat-input-main'; // UI Element IDs const STATUS_BAR_ID = 'custom-input-status-bar'; const AUTO_SEND_TOGGLE_ID = 'custom-auto-send-toggle'; const RETRY_BUTTON_ID = 'custom-translate-retry-button'; // Retries ORIGINAL text const RETRY_ABBREVIATION_BUTTON_ID = 'custom-abbreviation-retry-button'; // Retries CURRENT text // Language Detection Regex const CHINESE_REGEX = /[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]/; const BURMESE_REGEX = /[\u1000-\u109F]/; // State Variables let statusBarElement = null; let autoSendToggleElement = null; let currentInputApiXhr = null; let isTranslatingAndSending = false; let sendButtonClickListenerAttached = false; let autoSendEnabled = false; let lastOriginalText = null; // Stores the initial Chinese/Burmese text const translationCache = new Map(); // --- CSS Styles --- GM_addStyle(` /* Container */ ${INPUT_AREA_CONTAINER_SELECTOR} { position: relative !important; overflow: visible !important; } /* Status Bar */ #${STATUS_BAR_ID} { position: absolute; bottom: 2px; left: 8px; right: 8px; display: none; padding: 4px 8px; font-size: 12px; color: #ccc; background-color: rgba(20, 20, 20, 0.85); backdrop-filter: blur(2px); border-top: 1px solid rgba(255, 255, 255, 0.1); border-radius: 4px; z-index: 149; line-height: 1.3; text-align: left; transition: opacity 0.2s ease-in-out, bottom 0.2s ease-in-out; opacity: 0; pointer-events: none; } #${STATUS_BAR_ID}.visible { display: flex; justify-content: space-between; align-items: center; opacity: 1; pointer-events: auto; } #${STATUS_BAR_ID} .status-text { /* Container for the text message */ flex-grow: 1; margin-right: 8px; } #${STATUS_BAR_ID} .status-buttons { /* Container for buttons */ display: flex; gap: 5px; /* Add gap between buttons */ flex-shrink: 0; /* Prevent buttons container from shrinking */ } /* Text Status Types */ #${STATUS_BAR_ID} .status { font-style: italic; color: #a0a0a0; } #${STATUS_BAR_ID} .info { font-style: italic; color: #87cefa; } /* Light blue for cache */ #${STATUS_BAR_ID} .error { font-weight: bold; color: #ff8a8a; } #${STATUS_BAR_ID} .success { font-weight: bold; color: #8ade8a; } /* Retry Buttons Style (shared) */ #${RETRY_BUTTON_ID}, #${RETRY_ABBREVIATION_BUTTON_ID} { padding: 2px 6px; font-size: 11px; font-weight: bold; color: #d0d0d0; background-color: rgba(80, 80, 80, 0.9); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 3px; cursor: pointer; flex-shrink: 0; /* Prevent shrinking */ transition: background-color 0.2s ease, color 0.2s ease; white-space: nowrap; /* Prevent button text wrapping */ } #${RETRY_BUTTON_ID}:hover, #${RETRY_ABBREVIATION_BUTTON_ID}:hover { background-color: rgba(100, 100, 100, 0.9); color: #fff; } #${RETRY_BUTTON_ID}:active, #${RETRY_ABBREVIATION_BUTTON_ID}:active { background-color: rgba(60, 60, 60, 0.9); } /* Auto-Send Toggle Button */ #${AUTO_SEND_TOGGLE_ID} { position: absolute; bottom: 100%; margin-bottom: 1px; right: 10px; z-index: 151; padding: 4px 10px; font-size: 12px; font-weight: bold; background-color: rgba(80, 80, 80, 0.9); color: #ccc; border: 1px solid rgba(255, 255, 255, 0.2); border-bottom: none; border-radius: 6px 6px 0 0; cursor: pointer; user-select: none; transition: background-color 0.2s ease, color 0.2s ease; } #${AUTO_SEND_TOGGLE_ID}.autosend-on { background-color: rgba(70, 130, 180, 0.95); color: #fff; } #${AUTO_SEND_TOGGLE_ID}:hover { filter: brightness(1.1); } `); // --- Helper Functions --- function detectLanguage(text) { if (!text) return null; if (CHINESE_REGEX.test(text)) return 'Chinese'; if (BURMESE_REGEX.test(text)) return 'Burmese'; return 'Other'; } function setCursorToEnd(element) { try { const range = document.createRange(); const sel = window.getSelection(); range.selectNodeContents(element); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); element.focus(); } catch (e) { console.warn("[InputTranslate] Error setting cursor:", e); } } function ensureControlsExist() { /* ... (unchanged from v2.8) ... */ const inputMainContainer = document.querySelector(INPUT_AREA_CONTAINER_SELECTOR); if (!inputMainContainer) return; if (window.getComputedStyle(inputMainContainer).position !== 'relative') { inputMainContainer.style.position = 'relative'; } if (!autoSendToggleElement || !inputMainContainer.contains(autoSendToggleElement)) { autoSendToggleElement = document.createElement('button'); autoSendToggleElement.id = AUTO_SEND_TOGGLE_ID; updateAutoSendButtonVisual(); autoSendToggleElement.addEventListener('click', toggleAutoSend); inputMainContainer.appendChild(autoSendToggleElement); console.log("[InputTranslate] Auto-send toggle button created."); } if (!statusBarElement || !inputMainContainer.contains(statusBarElement)) { statusBarElement = document.createElement('div'); statusBarElement.id = STATUS_BAR_ID; inputMainContainer.appendChild(statusBarElement); console.log("[InputTranslate] Status bar element created."); } } // Updated status display to handle both retry buttons function updateStatusDisplay(content, type = 'status', duration = 0, showRetryButton = false, showRetryAbbreviationButton = false) { ensureControlsExist(); if (!statusBarElement) { console.error("[InputTranslate] Status bar element not found for update."); return; } let buttonsHtml = ''; // Build buttons HTML string if (showRetryButton && lastOriginalText) { buttonsHtml += `<button id="${RETRY_BUTTON_ID}" type="button">重试原文</button>`; // Retry Original } if (showRetryAbbreviationButton) { // Condition to show the new button // Add a check for input element content? Maybe not needed here, handler will check. buttonsHtml += `<button id="${RETRY_ABBREVIATION_BUTTON_ID}" type="button">重试缩写</button>`; // Retry Abbreviation } // Use flex structure with separate containers for text and buttons statusBarElement.innerHTML = ` <span class="status-text ${type}">${content}</span> ${buttonsHtml ? `<div class="status-buttons">${buttonsHtml}</div>` : ''} `; statusBarElement.classList.add('visible'); // Attach listeners AFTER adding buttons to DOM if (showRetryButton && lastOriginalText) { const retryBtn = statusBarElement.querySelector(`#${RETRY_BUTTON_ID}`); if (retryBtn) retryBtn.addEventListener('click', handleRetryOriginalClick); } if (showRetryAbbreviationButton) { const retryAbbrBtn = statusBarElement.querySelector(`#${RETRY_ABBREVIATION_BUTTON_ID}`); if (retryAbbrBtn) retryAbbrBtn.addEventListener('click', handleRetryAbbreviationClick); } // Auto-hide logic (unchanged) if (statusBarElement.hideTimeout) clearTimeout(statusBarElement.hideTimeout); statusBarElement.hideTimeout = duration > 0 ? setTimeout(hideStatusDisplay, duration) : null; } function hideStatusDisplay() { /* ... (unchanged from v2.8) ... */ if (statusBarElement) { if (statusBarElement.hideTimeout) clearTimeout(statusBarElement.hideTimeout); statusBarElement.hideTimeout = null; statusBarElement.classList.remove('visible'); setTimeout(() => { if (statusBarElement && !statusBarElement.classList.contains('visible')) { statusBarElement.innerHTML = ''; } }, 250); } } // --- Retry Button Handlers --- // Renamed original retry handler slightly for clarity function handleRetryOriginalClick(event) { event.preventDefault(); event.stopPropagation(); console.log("[InputTranslate] Retry Original button clicked."); if (isTranslatingAndSending) { console.warn("[InputTranslate] Already translating, ignoring retry original click."); return; } if (!lastOriginalText) { console.warn("[InputTranslate] No original text stored for retry."); hideStatusDisplay(); return; } const inputElement = document.querySelector(INPUT_SELECTOR); const sendButton = document.querySelector(SEND_BUTTON_SELECTOR); if (!inputElement || !sendButton) { updateStatusDisplay("重试失败: 界面元素丢失", 'error', 4000, true, false); return; } // Show only original retry on element error if (sendButton.disabled) { updateStatusDisplay("重试失败: 发送按钮不可用", 'error', 4000, true, false); return; } console.log("[InputTranslate] Retrying original translation for:", lastOriginalText); // Call the main translation function with the *original* text, force API translateAndSend(lastOriginalText, inputElement, sendButton, true); } // << NEW >> Handler for the "Retry Abbreviation" button function handleRetryAbbreviationClick(event) { event.preventDefault(); event.stopPropagation(); console.log("[InputTranslate] Retry Abbreviation button clicked."); if (isTranslatingAndSending) { console.warn("[InputTranslate] Already translating, ignoring retry abbreviation click."); return; } const inputElement = document.querySelector(INPUT_SELECTOR); const sendButton = document.querySelector(SEND_BUTTON_SELECTOR); if (!inputElement || !sendButton) { updateStatusDisplay("重试失败: 界面元素丢失", 'error', 4000, true, false); return; } // Show only original retry on element error const currentText = inputElement.textContent?.trim(); if (!currentText) { console.warn("[InputTranslate] Input field is empty, cannot retry abbreviation."); hideStatusDisplay(); return; } if (sendButton.disabled) { updateStatusDisplay("重试失败: 发送按钮不可用", 'error', 4000, true, true); return; } // Show both buttons maybe? Or just original? Let's show both if possible. console.log("[InputTranslate] Retrying abbreviation/translation for current text:", currentText); // Call the main translation function with the *current* text from input, force API // The prompt is designed to handle English input too for re-abbreviation. translateAndSend(currentText, inputElement, sendButton, true); } // --- Auto Send Toggle Logic (Unchanged) --- function updateAutoSendButtonVisual() { /* ... (unchanged from v2.8) ... */ if (!autoSendToggleElement) return; autoSendToggleElement.textContent = autoSendEnabled ? "自动发送: 开" : "自动发送: 关"; autoSendToggleElement.className = autoSendEnabled ? 'autosend-on' : ''; autoSendToggleElement.id = AUTO_SEND_TOGGLE_ID; } function toggleAutoSend() { /* ... (unchanged from v2.8) ... */ autoSendEnabled = !autoSendEnabled; console.log(`[InputTranslate] Auto Send Toggled: ${autoSendEnabled ? 'ON' : 'OFF'}`); updateAutoSendButtonVisual(); updateStatusDisplay(`自动发送已${autoSendEnabled ? '开启' : '关闭'}`, 'status', 2000); } // --- Main Translation Logic with Cache & Updated Status Calls --- function translateAndSend(textToProcess, inputElement, sendButton, forceApi = false) { if (isTranslatingAndSending) { console.warn("[InputTranslate] Already processing, ignoring translateAndSend call."); return; } if (!inputElement || !sendButton) { updateStatusDisplay("错误: 无法找到输入或发送按钮", 'error', 4000, true, false); // Show only original retry return; } isTranslatingAndSending = true; // Only update lastOriginalText if the input seems like Chinese/Burmese (heuristic) const detectedLang = detectLanguage(textToProcess); if (detectedLang === 'Chinese' || detectedLang === 'Burmese') { lastOriginalText = textToProcess; } // If textToProcess is already English (likely from Retry Abbreviation), lastOriginalText remains unchanged. hideStatusDisplay(); // --- Cache Check --- (Only check cache if not forcing API and input looks like source lang) const useCache = !forceApi && (detectedLang === 'Chinese' || detectedLang === 'Burmese'); if (useCache && translationCache.has(textToProcess)) { const cachedTranslation = translationCache.get(textToProcess); console.log("[InputTranslate] Cache Hit:", textToProcess); updateStatusDisplay("已从缓存加载 ✓", 'info', 3000, false, !autoSendEnabled); // Show retry abbr if manual send inputElement.textContent = cachedTranslation; setCursorToEnd(inputElement); inputElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); if (autoSendEnabled) { const sendDelay = 50; console.log(`[InputTranslate][Cache] Auto-sending ON. Setting timeout (${sendDelay}ms) for click.`); setTimeout(() => { if (!isTranslatingAndSending) { console.log("[InputTranslate][CacheTimeout] Sending aborted."); isTranslatingAndSending = false; return; } if (sendButton && sendButton.isConnected && !sendButton.disabled) { sendButton.click(); hideStatusDisplay(); } else { updateStatusDisplay("发送失败 (按钮不可用?)", 'error', 4000, true, true); // Show both retries on send fail } isTranslatingAndSending = false; }, sendDelay); } else { console.log("[InputTranslate][Cache] Auto-sending OFF."); // Status already shown above with Retry Abbreviation button isTranslatingAndSending = false; } return; // <<< Return after cache hit handled } // --- End Cache Check --- // --- API Call --- console.log(`[InputTranslate] ${forceApi ? 'Forcing API call' : 'Cache Miss'}. Calling API (${INPUT_TRANSLATE_MODEL}) for: "${textToProcess.substring(0, 30)}..."`); updateStatusDisplay("翻译/缩写中...", 'status'); // No buttons during processing const finalPrompt = TRANSLATION_PROMPT.replace('{text_to_translate}', textToProcess); const requestBody = { model: INPUT_TRANSLATE_MODEL, messages: [{"role": "user", "content": finalPrompt }], temperature: 0.6 }; if (currentInputApiXhr && typeof currentInputApiXhr.abort === 'function') { currentInputApiXhr.abort(); } currentInputApiXhr = GM_xmlhttpRequest({ method: "POST", url: OHMYGPT_API_ENDPOINT, headers: { "Content-Type": "application/json", "Authorization": `Bearer ${OHMYGPT_API_KEY}` }, data: JSON.stringify(requestBody), onload: function(response) { currentInputApiXhr = null; try { if (response.status >= 200 && response.status < 300) { const data = JSON.parse(response.responseText); const translation = data.choices?.[0]?.message?.content?.trim(); if (translation) { console.log("[InputTranslate] API Success:", translation); // Cache the result only if it was an initial translation (not forced) if (!forceApi && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) { if (translationCache.size >= MAX_CACHE_SIZE) { const oldestKey = translationCache.keys().next().value; translationCache.delete(oldestKey); } translationCache.set(textToProcess, translation); // Cache original -> translation console.log("[InputTranslate] Cached:", textToProcess); } inputElement.textContent = translation; setCursorToEnd(inputElement); inputElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); if (autoSendEnabled) { const sendDelay = 150; console.log(`[InputTranslate][API] Auto-sending ON. Setting timeout (${sendDelay}ms) for click.`); setTimeout(() => { if (!isTranslatingAndSending) { console.log("[InputTranslate][APITimeout] Sending aborted."); isTranslatingAndSending = false; return; } if (sendButton && sendButton.isConnected && !sendButton.disabled) { sendButton.click(); hideStatusDisplay(); } else { updateStatusDisplay("发送失败 (按钮不可用?)", 'error', 4000, true, true); // Show both retries on send fail } isTranslatingAndSending = false; }, sendDelay); } else { console.log("[InputTranslate][API] Auto-sending OFF."); // Show success message WITH BOTH retry buttons updateStatusDisplay("完成 ✓ (请手动发送或重试)", 'success', 5000, true, true); isTranslatingAndSending = false; } } else { // API returned 2xx but no content throw new Error(`API Error: No translation content (Choice: ${data.choices?.[0]?.finish_reason || 'N/A'})`); } } else { // API returned non-2xx status let errorDetail = `HTTP ${response.status}: ${response.statusText}`; try { const errData = JSON.parse(response.responseText); errorDetail = errData.error?.message || errorDetail; } catch (e) { /* ignore */ } throw new Error(errorDetail); } } catch (e) { // Catch JSON parse errors or errors thrown above console.error("[InputTranslate] API/Parse Error:", e); // Show error message WITH ONLY original retry button updateStatusDisplay(`处理失败: ${e.message.substring(0, 60)}`, 'error', 5000, true, false); isTranslatingAndSending = false; } }, onerror: function(response) { currentInputApiXhr = null; console.error("[InputTranslate] Request Error:", response); updateStatusDisplay(`处理失败: 网络错误 (${response.status || 'N/A'})`, 'error', 5000, true, false); isTranslatingAndSending = false; }, ontimeout: function() { currentInputApiXhr = null; console.error("[InputTranslate] Timeout"); updateStatusDisplay("处理失败: 请求超时", 'error', 5000, true, false); isTranslatingAndSending = false; }, onabort: function() { currentInputApiXhr = null; console.log("[InputTranslate] API request aborted."); hideStatusDisplay(); isTranslatingAndSending = false; }, timeout: 30000 }); } // --- Event Listeners --- function handleInputKeyDown(event) { const inputElement = event.target; if (!inputElement || !inputElement.matches(INPUT_SELECTOR)) return; if (event.key === 'Enter' && !event.shiftKey && !event.altKey && !event.ctrlKey) { // Allow normal send if status shows success/info and not processing + manual send if (statusBarElement && statusBarElement.classList.contains('visible') && !isTranslatingAndSending && !autoSendEnabled) { const nonBlockingStatus = statusBarElement.querySelector('span.success, span.info'); if (nonBlockingStatus) { console.log("[InputTranslate][Enter] Allowing manual send."); hideStatusDisplay(); return; } } if (isTranslatingAndSending) { event.preventDefault(); event.stopPropagation(); return; } const text = inputElement.textContent?.trim() || ""; const detectedLang = detectLanguage(text); if (text && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) { console.log(`[InputTranslate][Enter] Detected ${detectedLang}. Processing...`); event.preventDefault(); event.stopPropagation(); const sendButton = document.querySelector(SEND_BUTTON_SELECTOR); if (!sendButton) { updateStatusDisplay("错误: 未找到发送按钮!", 'error', 5000, true, false); return; } if (sendButton.disabled) { updateStatusDisplay("错误: 发送按钮不可用!", 'error', 5000, true, false); return; } translateAndSend(text, inputElement, sendButton); // Initial translation call } else { hideStatusDisplay(); } // Allow normal send } // Abort logic (unchanged) else if (isTranslatingAndSending && !['Shift', 'Control', 'Alt', 'Meta', 'Enter'].includes(event.key)) { hideStatusDisplay(); if (currentInputApiXhr && typeof currentInputApiXhr.abort === 'function') { currentInputApiXhr.abort(); } else { isTranslatingAndSending = false; } } else if (!isTranslatingAndSending) { if (statusBarElement && statusBarElement.classList.contains('visible')) { const statusSpan = statusBarElement.querySelector('span.status'); // Only hide non-"Translating..." messages on typing if (!statusSpan) { hideStatusDisplay(); } } } } function handleSendButtonClick(event) { const sendButton = event.target.closest(SEND_BUTTON_SELECTOR); if (!sendButton) return; // Allow normal send if status shows success/info and not processing + manual send if (statusBarElement && statusBarElement.classList.contains('visible') && !isTranslatingAndSending && !autoSendEnabled) { const nonBlockingStatus = statusBarElement.querySelector('span.success, span.info'); if (nonBlockingStatus) { console.log("[InputTranslate][Click] Allowing manual send."); hideStatusDisplay(); return; } } const inputElement = document.querySelector(INPUT_SELECTOR); if (!inputElement) return; const text = inputElement.textContent?.trim() || ""; const detectedLang = detectLanguage(text); if (text && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) { if (isTranslatingAndSending) { event.preventDefault(); event.stopPropagation(); return; } console.log(`[InputTranslate][Click] Detected ${detectedLang}. Processing...`); event.preventDefault(); event.stopPropagation(); if (sendButton.disabled) { updateStatusDisplay("错误: 发送按钮不可用!", 'error', 5000, true, false); return; } translateAndSend(text, inputElement, sendButton); // Initial translation call } else { if (!isTranslatingAndSending) { hideStatusDisplay(); } // Allow normal send } } // --- Initialization & Attaching Listeners (Unchanged structure) --- function initialize() { console.log("[Telegram Input Translator v2.9 - Retry Abbr + Cache + Status Inside] Initializing..."); // ... (Observer logic is the same as v2.8) ... const observer = new MutationObserver(mutations => { let controlsNeedCheck = false; mutations.forEach(mutation => { if (mutation.addedNodes) { mutation.addedNodes.forEach(node => { if (node.nodeType !== 1) return; const containerNode = node.matches(INPUT_AREA_CONTAINER_SELECTOR) ? node : node.querySelector(INPUT_AREA_CONTAINER_SELECTOR); if(containerNode) controlsNeedCheck = true; const inputElementNode = node.matches(INPUT_SELECTOR) ? node : node.querySelector(INPUT_SELECTOR); if (inputElementNode && !inputElementNode.dataset.customInputTranslateListener) { attachInputListeners(inputElementNode); controlsNeedCheck = true; } }); } if (mutation.target && mutation.target.matches && mutation.target.matches(INPUT_AREA_CONTAINER_SELECTOR)) { controlsNeedCheck = true; } }); if (controlsNeedCheck) { ensureControlsExist(); } if (!sendButtonClickListenerAttached) { const sendButton = document.querySelector(SEND_BUTTON_SELECTOR); if (sendButton && !sendButton.dataset.customSendClickListener) { attachSendButtonListener(sendButton); } } }); observer.observe(document.body, { childList: true, subtree: true }); // Initial check const initialContainer = document.querySelector(INPUT_AREA_CONTAINER_SELECTOR); if (initialContainer) { ensureControlsExist(); } const initialInputElement = document.querySelector(INPUT_SELECTOR); if (initialInputElement && !initialInputElement.dataset.customInputTranslateListener) { attachInputListeners(initialInputElement); } const initialSendButton = document.querySelector(SEND_BUTTON_SELECTOR); if(initialSendButton && !initialSendButton.dataset.customSendClickListener) { attachSendButtonListener(initialSendButton); } console.log("[Telegram Input Translator v2.9] Observer active."); } function attachInputListeners(inputElement) { /* ... (unchanged from v2.8) ... */ if (inputElement.dataset.customInputTranslateListener) return; console.log("[InputTranslate] Attaching Keydown listener to input:", inputElement); inputElement.addEventListener('keydown', handleInputKeyDown, true); inputElement.dataset.customInputTranslateListener = 'true'; ensureControlsExist(); } function attachSendButtonListener(sendButton) { /* ... (unchanged from v2.8) ... */ if (sendButton.dataset.customSendClickListener) return; console.log("[InputTranslate] Attaching Click listener to Send button:", sendButton); sendButton.addEventListener('click', handleSendButtonClick, true); sendButton.dataset.customSendClickListener = 'true'; sendButtonClickListenerAttached = true; const buttonObserver = new MutationObserver(() => { if (!sendButton.isConnected) { console.log("[InputTranslate] Send button removed. Resetting listener flag."); buttonObserver.disconnect(); if (sendButton.dataset.customSendClickListener) { delete sendButton.dataset.customSendClickListener; } sendButtonClickListenerAttached = false; } }); if (sendButton.parentNode) { buttonObserver.observe(sendButton.parentNode, { childList: true, subtree: false }); } else { console.warn("[InputTranslate] Send button parent node not found for observer."); } } // --- Start Initialization --- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 1800)); } else { setTimeout(initialize, 1800); } })();