您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Modifies Enter key behavior in Gemini and AI Studio. Gemini: Enter for newline, Modifier+Enter to send. AI Studio: Configurable send key (modifier, Enter-sends, or native). Includes a settings panel. Now with Meta key support for Ctrl mode and improved IME handling.
当前为
// ==UserScript== // @name Gemini & AI Studio Enter Key Customizer // @name:en Gemini & AI Studio Enter Key Customizer // @name:ja Gemini & AI Studio Enterキーカスタマイザー // @name:zh-TW Gemini 與 AI Studio Enter 鍵自訂器 // @namespace https://greatest.deepsurf.us/en/users/1467948-stonedkhajiit // @version 1.0.1 // @description Modifies Enter key behavior in Gemini and AI Studio. Gemini: Enter for newline, Modifier+Enter to send. AI Studio: Configurable send key (modifier, Enter-sends, or native). Includes a settings panel. Now with Meta key support for Ctrl mode and improved IME handling. // @description:en Modifies Enter key behavior in Gemini and AI Studio. Gemini: Enter for newline, Modifier+Enter to send. AI Studio: Configurable send key (modifier, Enter-sends, or native). Includes a settings panel. Now with Meta key support for Ctrl mode and improved IME handling. // @description:ja GeminiとAI StudioのEnterキー動作を変更。Gemini: Enterで改行、修飾キー+Enterで送信。AI Studio: 送信キーを選択可 (修飾キー、Enter送信、標準)。設定パネルあり。CtrlモードのMetaキーサポートとIME処理の改善を追加。 // @description:zh-TW 調整 Gemini 與 AI Studio 的 Enter 鍵行為。Gemini:Enter 鍵換行,組合鍵送出。AI Studio:可自訂傳送鍵 (組合鍵、Enter即送、或預設)。附設定面板。已加入 Ctrl 模式的 Meta 鍵支援與改良的 IME 輸入法處理。 // @author StonedKhajiit // @match https://gemini.google.com/* // @match https://aistudio.google.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @license MIT // ==/UserScript== (function() { 'use strict'; // --- Constants and Configuration --- const SCRIPT_ID = 'GeminiEnterNewlineMultiSite_v1.0.1'; // Version update // CSS Selectors for input fields and send buttons const GEMINI_INPUT_SELECTOR_PRIMARY = 'div.ql-editor[contenteditable="true"]'; const GEMINI_INPUT_SELECTORS_FALLBACK = [ 'textarea[enterkeyhint="send"]', 'textarea[aria-label*="Prompt"]', 'textarea[placeholder*="Message Gemini"]', 'div[role="textbox"][contenteditable="true"]' ]; const AISTUDIO_INPUT_SELECTORS = [ 'ms-autosize-textarea textarea[aria-label="Type something or tab to choose an example prompt"]', 'ms-autosize-textarea textarea', 'ms-autosize-textarea textarea[aria-label="Start typing a prompt"]' ]; const GEMINI_SEND_BUTTON_SELECTORS = [ 'button[aria-label*="Send"]', 'button[aria-label*="傳送"]', // Keep original labels for matching 'button[aria-label*="送信"]', 'button[data-test-id="send-button"]', ]; const AISTUDIO_SEND_BUTTON_SELECTORS = ['button[aria-label="Run"]']; const AISTUDIO_SEND_BUTTON_MODIFIER_HINT_SELECTOR = 'span.secondary-key'; // GM Storage keys and default values for settings const GM_GLOBAL_ENABLE_KEY_STORAGE = 'geminiEnterGlobalEnable'; const GM_MODIFIER_KEY_STORAGE = 'geminiEnterModifierKey'; const MODIFIER_KEYS = { NONE: 'none', CTRL: 'ctrl', SHIFT: 'shift', ALT: 'alt', NATIVE_GEMINI: 'native_gemini' }; const DEFAULT_MODIFIER_KEY = MODIFIER_KEYS.CTRL; const GM_AISTUDIO_MODE_STORAGE = 'aiStudioKeyMode'; const AISTUDIO_KEY_MODES = { SHIFT_SEND: 'shift_send', ALT_SEND: 'alt_send', AISTUDIO_SPECIFIC: 'aistudio_specific', NATIVE_BEHAVIOR: 'native_behavior' }; const DEFAULT_AISTUDIO_KEY_MODE = AISTUDIO_KEY_MODES.NATIVE_BEHAVIOR; // --- State Variables --- let activeTextarea = null; let isScriptGloballyEnabled = true; let currentGlobalModifierKey = DEFAULT_MODIFIER_KEY; let currentAIStudioKeyMode = DEFAULT_AISTUDIO_KEY_MODE; let menuCommandIds = []; // --- Debounce Function --- function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // --- Internationalization (i18n) --- // User-facing strings are kept in their respective languages. const i18n = { currentLang: 'en', strings: { 'en': { notifySettingsSaved: 'Settings saved!', notifyScriptEnabled: 'Custom Enter key behavior enabled. Reload page if needed.', notifyScriptDisabled: 'Custom Enter key behavior disabled. Reload page if needed.', settingsTitle: 'Script Settings', geminiKeyModeLabel: 'Gemini Key Mode (Enter for newline):', // Updated label text aiStudioKeyModeLabel: 'AI Studio Key Mode:', closeButton: 'Close', saveButton: 'Save', openSettingsMenu: 'Configure Enter Key Behavior...', enableScriptMenu: 'Enable Custom Enter Key Behavior', disableScriptMenu: 'Disable Custom Enter Key Behavior', geminiCtrl: 'Ctrl/Cmd+Enter to send', // Updated label text geminiShift: 'Shift+Enter to send', geminiAlt: 'Alt+Enter to send', geminiNative: 'Use Gemini Native Behavior (Enter sends)', aiStudioShift: 'Shift+Enter to send', aiStudioAlt: 'Alt+Enter to send', aiStudioSpecific: 'Enter to Send, Shift+Enter for Newline', aiStudioNative: 'Use AI Studio Native Behavior (Ctrl/Cmd+Enter sends)', // Updated label text modifierCtrl: 'Ctrl/Cmd', // Updated label text modifierShift: 'Shift', modifierAlt: 'Alt', }, 'zh-TW': { // Keep zh-TW strings for i18n notifySettingsSaved: '設定已儲存!', notifyScriptEnabled: '自訂 Enter 鍵行為已啟用。若未立即生效請重載頁面。', notifyScriptDisabled: '自訂 Enter 鍵行為已停用。若未立即生效請重載頁面。', settingsTitle: '腳本設定', geminiKeyModeLabel: 'Gemini 按鍵模式 (Enter 換行):', aiStudioKeyModeLabel: 'AI Studio 按鍵模式:', closeButton: '關閉', saveButton: '儲存', openSettingsMenu: '設定 Enter 鍵行為...', enableScriptMenu: '啟用自訂 Enter 鍵行為', disableScriptMenu: '停用自訂 Enter 鍵行為', geminiCtrl: 'Ctrl/Cmd+Enter 送出', geminiShift: 'Shift+Enter 送出', geminiAlt: 'Alt+Enter 送出', geminiNative: '使用 Gemini 原生行為 (Enter 送出)', aiStudioShift: 'Shift+Enter 送出', aiStudioAlt: 'Alt+Enter 送出', aiStudioSpecific: 'Enter 送出,Shift+Enter 換行', aiStudioNative: '使用 AI Studio 原生行為 (Ctrl/Cmd+Enter 送出)', modifierCtrl: 'Ctrl/Cmd', modifierShift: 'Shift', modifierAlt: 'Alt', }, 'ja': { // Keep ja strings for i18n notifySettingsSaved: '設定を保存しました!', notifyScriptEnabled: 'Enterキーのカスタム動作が有効になりました。必要に応じてページを再読み込みしてください。', notifyScriptDisabled: 'Enterキーのカスタム動作が無効になりました。必要に応じてページを再読み込みしてください。', settingsTitle: 'スクリプト設定', geminiKeyModeLabel: 'Gemini のキーモード (Enter で改行):', aiStudioKeyModeLabel: 'AI Studio のキーモード:', closeButton: '閉じる', saveButton: '保存', openSettingsMenu: 'Enterキーの動作を設定...', enableScriptMenu: 'Enterキーのカスタム動作を有効化', disableScriptMenu: 'Enterキーのカスタム動作を無効化', geminiCtrl: 'Ctrl/Cmd+Enter で送信', geminiShift: 'Shift+Enter で送信', geminiAlt: 'Alt+Enter で送信', geminiNative: 'Gemini ネイティブ動作を使用 (Enter で送信)', aiStudioShift: 'Shift+Enter で送信', aiStudioAlt: 'Alt+Enter で送信', aiStudioSpecific: 'Enter で送信、Shift+Enter で改行', aiStudioNative: 'AI Studio ネイティブ動作を使用 (Ctrl/Cmd+Enter で送信)', modifierCtrl: 'Ctrl/Cmd', modifierShift: 'Shift', modifierAlt: 'Alt', } }, detectLanguage() { const lang = navigator.language || navigator.userLanguage; if (lang) { if (lang.startsWith('ja')) this.currentLang = 'ja'; else if (lang.startsWith('zh-TW') || lang.startsWith('zh-Hant')) this.currentLang = 'zh-TW'; else if (lang.startsWith('en')) this.currentLang = 'en'; else this.currentLang = 'en'; // Default to English if specific match not found } else { this.currentLang = 'en'; // Default to English if language detection fails } }, get(key, ...args) { const langStrings = this.strings[this.currentLang] || this.strings.en; const template = langStrings[key] || (this.strings.en && this.strings.en[key]); // Fallback to English if (typeof template === 'function') return template(...args); if (typeof template === 'string') return template; console.warn(`[${SCRIPT_ID}] Missing i18n string for key: ${key} in lang: ${this.currentLang}`); return `Missing string: ${key}`; // Fallback string } }; // --- UI Functions --- function createSettingsUI() { if (document.getElementById('gemini-ai-settings-overlay')) return; // Prevent multiple UI creations const overlay = document.createElement('div'); overlay.id = 'gemini-ai-settings-overlay'; overlay.classList.add('hidden'); // Start hidden const style = document.createElement('style'); // CSS for the settings panel - kept as is, no translatable comments here. style.textContent = ` @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); #gemini-ai-settings-overlay { position: fixed; inset: 0px; background-color: rgba(0, 0, 0, 0.6); display: flex; align-items: center; justify-content: center; z-index: 2147483647; font-family: 'Inter', Arial, sans-serif; opacity: 0; transition: opacity 0.2s ease-in-out; } #gemini-ai-settings-overlay.visible { opacity: 1; } #gemini-ai-settings-overlay.hidden { display: none !important; } #gemini-ai-settings-panel { background-color: #ffffff; color: #1f2937; padding: 18px; border-radius: 8px; box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04); width: 90%; max-width: 420px; position: relative; overflow-y: auto; max-height: 90vh; } body.userscript-dark-mode #gemini-ai-settings-panel { background-color: #2d3748; color: #e2e8f0; } body.userscript-dark-mode #gemini-ai-settings-panel h2, body.userscript-dark-mode #gemini-ai-settings-panel h3, body.userscript-dark-mode #gemini-ai-settings-panel label { color: #e2e8f0; } body.userscript-dark-mode #gemini-ai-settings-panel button#gemini-ai-close-btn { background-color: #4a5568; color: #e2e8f0; } body.userscript-dark-mode #gemini-ai-settings-panel button#gemini-ai-close-btn:hover { background-color: #718096; } body.userscript-dark-mode #gemini-ai-settings-panel input[type="radio"] { filter: invert(1) hue-rotate(180deg); } #gemini-ai-settings-panel h2 { font-size: 1.15rem; font-weight: 600; margin-bottom: 0.8rem; } #gemini-ai-settings-panel h3 { font-size: 1rem; font-weight: 500; margin-bottom: 0.5rem; margin-top: 0.7rem; } #gemini-ai-settings-panel .section-divider { border-top: 1px solid #e5e7eb; margin-top: 1rem; margin-bottom: 1rem; } body.userscript-dark-mode #gemini-ai-settings-panel .section-divider { border-top-color: #4a5568; } #gemini-ai-settings-panel .options-group > div { margin-bottom: 0.3rem; } #gemini-ai-settings-panel label { display: inline-flex; align-items: center; cursor: pointer; font-size: 0.875rem; } #gemini-ai-settings-panel input[type="radio"] { margin-right: 0.4rem; cursor: pointer; transform: scale(0.95); } .settings-buttons-container { display: flex; justify-content: flex-end; margin-top: 1rem; gap: 0.5rem; } #gemini-ai-settings-panel button { padding: 0.4rem 0.9rem; border-radius: 6px; font-weight: 500; transition: background-color 0.2s ease, box-shadow 0.2s ease; border: none; cursor: pointer; font-size: 0.875rem; } #gemini-ai-settings-panel button#gemini-ai-close-btn { background-color: #e5e7eb; color: #374151; } #gemini-ai-settings-panel button#gemini-ai-close-btn:hover { background-color: #d1d5db; } #gemini-ai-settings-panel button#gemini-ai-save-btn { background-color: #3b82f6; color: white; } #gemini-ai-settings-panel button#gemini-ai-save-btn:hover { background-color: #2563eb; } #gemini-ai-notification { position: fixed; bottom: 25px; left: 50%; transform: translateX(-50%); background-color: #10b981; color: white; padding: 0.8rem 1.5rem; border-radius: 6px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06); z-index: 2147483647; opacity: 0; transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out; font-family: 'Inter', Arial, sans-serif; font-size: 0.9rem; } #gemini-ai-notification.visible { opacity: 1; transform: translateX(-50%) translateY(0px); } #gemini-ai-notification.hidden { display: none !important; } `; document.head.appendChild(style); const settingsPanel = document.createElement('div'); settingsPanel.id = 'gemini-ai-settings-panel'; const titleElement = document.createElement('h2'); // Renamed to avoid conflict with global 'title' titleElement.textContent = i18n.get('settingsTitle'); settingsPanel.appendChild(titleElement); const geminiTitle = document.createElement('h3'); geminiTitle.textContent = i18n.get('geminiKeyModeLabel'); settingsPanel.appendChild(geminiTitle); const geminiOptionsDiv = document.createElement('div'); geminiOptionsDiv.id = 'gemini-key-options'; geminiOptionsDiv.className = 'options-group'; settingsPanel.appendChild(geminiOptionsDiv); settingsPanel.appendChild(document.createElement('div')).className = 'section-divider'; const aistudioTitle = document.createElement('h3'); aistudioTitle.textContent = i18n.get('aiStudioKeyModeLabel'); settingsPanel.appendChild(aistudioTitle); const aistudioOptionsDiv = document.createElement('div'); aistudioOptionsDiv.id = 'aistudio-key-options'; aistudioOptionsDiv.className = 'options-group'; settingsPanel.appendChild(aistudioOptionsDiv); const buttonDiv = document.createElement('div'); buttonDiv.className = 'settings-buttons-container'; const closeButton = document.createElement('button'); closeButton.id = 'gemini-ai-close-btn'; closeButton.textContent = i18n.get('closeButton'); buttonDiv.appendChild(closeButton); const saveButton = document.createElement('button'); saveButton.id = 'gemini-ai-save-btn'; saveButton.textContent = i18n.get('saveButton'); buttonDiv.appendChild(saveButton); settingsPanel.appendChild(buttonDiv); overlay.appendChild(settingsPanel); document.body.appendChild(overlay); const notificationDiv = document.createElement('div'); notificationDiv.id = 'gemini-ai-notification'; notificationDiv.classList.add('hidden'); // Start hidden document.body.appendChild(notificationDiv); closeButton.addEventListener('click', closeSettings); saveButton.addEventListener('click', saveSettingsFromUI); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeSettings(); }); // Close on overlay click // Dark mode detection if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { document.body.classList.add('userscript-dark-mode'); } window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { document.body.classList.toggle('userscript-dark-mode', e.matches); }); } function populateSettingsUI() { const geminiOptionsDiv = document.getElementById('gemini-key-options'); const aistudioOptionsDiv = document.getElementById('aistudio-key-options'); if (!geminiOptionsDiv || !aistudioOptionsDiv) return; // Ensure elements exist // Clear previous options while (geminiOptionsDiv.firstChild) geminiOptionsDiv.removeChild(geminiOptionsDiv.firstChild); while (aistudioOptionsDiv.firstChild) aistudioOptionsDiv.removeChild(aistudioOptionsDiv.firstChild); const geminiModifierOptions = [ { key: MODIFIER_KEYS.CTRL, labelKey: 'geminiCtrl' }, { key: MODIFIER_KEYS.SHIFT, labelKey: 'geminiShift' }, { key: MODIFIER_KEYS.ALT, labelKey: 'geminiAlt' }, { key: MODIFIER_KEYS.NATIVE_GEMINI, labelKey: 'geminiNative' }, ]; geminiModifierOptions.forEach(opt => { const div = document.createElement('div'); const input = document.createElement('input'); input.type = 'radio'; input.name = 'geminiKeyMode'; input.id = `gemini-${opt.key}`; input.value = opt.key; if (currentGlobalModifierKey === opt.key) input.checked = true; const label = document.createElement('label'); label.htmlFor = `gemini-${opt.key}`; label.textContent = i18n.get(opt.labelKey); div.appendChild(input); div.appendChild(label); geminiOptionsDiv.appendChild(div); }); const aiStudioModeOptions = [ { mode: AISTUDIO_KEY_MODES.SHIFT_SEND, labelKey: 'aiStudioShift' }, { mode: AISTUDIO_KEY_MODES.ALT_SEND, labelKey: 'aiStudioAlt' }, { mode: AISTUDIO_KEY_MODES.AISTUDIO_SPECIFIC, labelKey: 'aiStudioSpecific' }, { mode: AISTUDIO_KEY_MODES.NATIVE_BEHAVIOR, labelKey: 'aiStudioNative' }, ]; aiStudioModeOptions.forEach(opt => { const div = document.createElement('div'); const input = document.createElement('input'); input.type = 'radio'; input.name = 'aistudioKeyMode'; input.id = `aistudio-${opt.mode}`; input.value = opt.mode; if (currentAIStudioKeyMode === opt.mode) input.checked = true; const label = document.createElement('label'); label.htmlFor = `aistudio-${opt.mode}`; label.textContent = i18n.get(opt.labelKey); div.appendChild(input); div.appendChild(label); aistudioOptionsDiv.appendChild(div); }); } function openSettings() { loadSettings(); // Load current settings before populating populateSettingsUI(); const overlay = document.getElementById('gemini-ai-settings-overlay'); if (overlay) { overlay.classList.remove('hidden'); void overlay.offsetWidth; // Trigger reflow for transition overlay.classList.add('visible'); } } function closeSettings() { const overlay = document.getElementById('gemini-ai-settings-overlay'); if (overlay) { overlay.classList.remove('visible'); setTimeout(() => { // Wait for transition to complete if (!overlay.classList.contains('visible')) { // Check if it wasn't re-opened overlay.classList.add('hidden'); } }, 200); // Duration of the opacity transition } } function showNotification(message) { const notificationDiv = document.getElementById('gemini-ai-notification'); if (notificationDiv) { notificationDiv.textContent = message; notificationDiv.classList.remove('hidden'); void notificationDiv.offsetWidth; // Trigger reflow for transition notificationDiv.classList.add('visible'); setTimeout(() => { notificationDiv.classList.remove('visible'); setTimeout(() => { // Wait for transition if (!notificationDiv.classList.contains('visible')) { notificationDiv.classList.add('hidden'); } }, 300); // Duration of opacity/transform transition }, 2500); // Notification display time } } // --- Core Logic Functions --- function loadSettings() { isScriptGloballyEnabled = GM_getValue(GM_GLOBAL_ENABLE_KEY_STORAGE, true); // Default to true const savedGeminiModifier = GM_getValue(GM_MODIFIER_KEY_STORAGE, DEFAULT_MODIFIER_KEY); // Ensure saved value is valid, otherwise use default currentGlobalModifierKey = Object.values(MODIFIER_KEYS).includes(savedGeminiModifier) ? savedGeminiModifier : DEFAULT_MODIFIER_KEY; const savedAIStudioMode = GM_getValue(GM_AISTUDIO_MODE_STORAGE, DEFAULT_AISTUDIO_KEY_MODE); // Ensure saved value is valid, otherwise use default currentAIStudioKeyMode = Object.values(AISTUDIO_KEY_MODES).includes(savedAIStudioMode) ? savedAIStudioMode : DEFAULT_AISTUDIO_KEY_MODE; } function saveSettingsFromUI() { const selectedGeminiMode = document.querySelector('input[name="geminiKeyMode"]:checked')?.value; if (selectedGeminiMode) { saveGeminiKeyModeSetting(selectedGeminiMode); } else { saveGeminiKeyModeSetting(DEFAULT_MODIFIER_KEY); // Fallback if nothing selected (should not happen with radio) } const selectedAIStudioMode = document.querySelector('input[name="aistudioKeyMode"]:checked')?.value; if (selectedAIStudioMode) { saveAIStudioKeyModeSetting(selectedAIStudioMode); } else { saveAIStudioKeyModeSetting(DEFAULT_AISTUDIO_KEY_MODE); // Fallback } registerMenuCommand(); // Update menu command text if needed (e.g., enable/disable script) showNotification(i18n.get('notifySettingsSaved')); closeSettings(); } function saveGeminiKeyModeSetting(key) { if (Object.values(MODIFIER_KEYS).includes(key) && key !== MODIFIER_KEYS.NONE) { // NONE is not a valid saveable mode here currentGlobalModifierKey = key; GM_setValue(GM_MODIFIER_KEY_STORAGE, key); updateActiveTextareaListener(); // Re-evaluate if listener should be attached } } function saveAIStudioKeyModeSetting(mode) { if (Object.values(AISTUDIO_KEY_MODES).includes(mode)) { currentAIStudioKeyMode = mode; GM_setValue(GM_AISTUDIO_MODE_STORAGE, mode); updateAIStudioButtonModifierHint(); // Update hint on AI Studio page updateActiveTextareaListener(); // Re-evaluate listener } } function updateAIStudioButtonModifierHint() { if (!window.location.hostname.includes('aistudio.google.com')) { return; // Only run on AI Studio } const sendButton = findSendButton(); if (sendButton) { const hintSpan = sendButton.querySelector(AISTUDIO_SEND_BUTTON_MODIFIER_HINT_SELECTOR); if (hintSpan) { hintSpan.style.display = 'inline'; // Default to visible let hintTextKey = 'modifierCtrl'; // Default to Ctrl/Cmd switch (currentAIStudioKeyMode) { case AISTUDIO_KEY_MODES.SHIFT_SEND: hintTextKey = 'modifierShift'; break; case AISTUDIO_KEY_MODES.ALT_SEND: hintTextKey = 'modifierAlt'; break; case AISTUDIO_KEY_MODES.AISTUDIO_SPECIFIC: // Enter sends, no modifier hint needed hintSpan.style.display = 'none'; hintTextKey = ''; break; case AISTUDIO_KEY_MODES.NATIVE_BEHAVIOR: // AI Studio native is typically Ctrl+Enter, i18n handles Ctrl/Cmd hintTextKey = 'modifierCtrl'; break; } const hintText = hintTextKey ? i18n.get(hintTextKey) : ''; hintSpan.textContent = hintSpan.style.display !== 'none' ? hintText + ' ' : ''; // Add space if hint is visible } } } function handleKeydown(event) { // --- IME (Input Method Editor) Handling --- if (event.isComposing) { return; // If user is composing text with an IME, do not interfere } // Check if script is globally disabled for Gemini if (window.location.hostname.includes('gemini.google.com') && !isScriptGloballyEnabled) { return; } // Ensure the event target is the active textarea or a child of it if (event.target !== activeTextarea && (!activeTextarea || !activeTextarea.contains(event.target))) { return; } const currentHost = window.location.hostname; // --- Meta Key Support & Updated Modifier Key Checks --- const isCtrlOrMetaPressed = event.ctrlKey || event.metaKey; // Check for Ctrl (Windows/Linux) or Cmd (Mac) const ctrlOrMetaOnly = isCtrlOrMetaPressed && !event.shiftKey && !event.altKey; // Only Ctrl/Cmd is pressed const shiftOnly = event.shiftKey && !isCtrlOrMetaPressed && !event.altKey; // Only Shift is pressed const altOnly = event.altKey && !isCtrlOrMetaPressed && !event.shiftKey; // Only Alt is pressed const plainEnter = !isCtrlOrMetaPressed && !event.shiftKey && !event.altKey; // Only Enter is pressed if (event.key === 'Enter') { if (currentHost.includes('gemini.google.com')) { if (currentGlobalModifierKey === MODIFIER_KEYS.NATIVE_GEMINI) { return; // Use Gemini's native behavior } // Pass updated modifier states to applyModifierSendBehavior applyModifierSendBehavior(event, currentGlobalModifierKey, ctrlOrMetaOnly, shiftOnly, altOnly, plainEnter, activeTextarea); } else if (currentHost.includes('aistudio.google.com')) { if (currentAIStudioKeyMode === AISTUDIO_KEY_MODES.NATIVE_BEHAVIOR) { return; // Use AI Studio's native behavior } else if (currentAIStudioKeyMode === AISTUDIO_KEY_MODES.AISTUDIO_SPECIFIC) { // 'Enter to Send, Shift+Enter for Newline' mode if (plainEnter) { // Use updated plainEnter check event.preventDefault(); event.stopImmediatePropagation(); const sendButton = findSendButton(); if (sendButton && !sendButton.disabled) sendButton.click(); else { const form = event.target?.closest('form'); if (form) form.requestSubmit ? form.requestSubmit() : form.submit(); } } else if (shiftOnly) { // Use updated shiftOnly check event.preventDefault(); event.stopImmediatePropagation(); insertNewline(activeTextarea); } else { // Other modifier combinations (like Ctrl/Meta+Enter, Alt+Enter) should be blocked in this specific mode event.preventDefault(); event.stopImmediatePropagation(); } } else if (currentAIStudioKeyMode === AISTUDIO_KEY_MODES.SHIFT_SEND) { applyModifierSendBehavior(event, MODIFIER_KEYS.SHIFT, ctrlOrMetaOnly, shiftOnly, altOnly, plainEnter, activeTextarea); } else if (currentAIStudioKeyMode === AISTUDIO_KEY_MODES.ALT_SEND) { applyModifierSendBehavior(event, MODIFIER_KEYS.ALT, ctrlOrMetaOnly, shiftOnly, altOnly, plainEnter, activeTextarea); } } } } function insertNewline(element) { if (!element) return; if (element.isContentEditable) { element.focus(); // Ensure focus before command let success = false; try { success = document.execCommand('insertParagraph', false, null); } catch (e) { /* console.warn("execCommand('insertParagraph') failed:", e); */ } if (!success) { // Fallback if insertParagraph fails or is not supported try { const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const br = document.createElement('br'); range.deleteContents(); // Clear selected text if any range.insertNode(br); // Move cursor after the inserted <br> and ensure focus range.setStartAfter(br); range.collapse(true); // Collapse range to a point selection.removeAllRanges(); // Remove old ranges selection.addRange(range); // Add new range element.focus(); // Re-ensure focus } else { // Fallback for very old browsers or unusual states document.execCommand('insertHTML', false, '<br>'); } } catch (e) { /* console.warn("Fallback newline insertion for contentEditable failed:", e); */ } } } else if (element.tagName === 'TEXTAREA') { const start = element.selectionStart; const end = element.selectionEnd; element.value = `${element.value.substring(0, start)}\n${element.value.substring(end)}`; element.selectionStart = element.selectionEnd = start + 1; // Move cursor after newline // Dispatch an input event to notify frameworks/libraries of the change element.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); } } // Updated parameter names for clarity function applyModifierSendBehavior(event, modifierKeyToSend, ctrlOrMetaOnlyFlag, shiftOnlyFlag, altOnlyFlag, plainEnterFlag, activeTextareaElement) { let send = false; // Determine if the current key combination matches the "send" criteria if ((modifierKeyToSend === MODIFIER_KEYS.CTRL && ctrlOrMetaOnlyFlag) || (modifierKeyToSend === MODIFIER_KEYS.SHIFT && shiftOnlyFlag) || (modifierKeyToSend === MODIFIER_KEYS.ALT && altOnlyFlag)) { send = true; } if (send) { event.preventDefault(); event.stopImmediatePropagation(); // Prevent default and stop further handlers const sendButton = findSendButton(); if (sendButton && !sendButton.disabled) sendButton.click(); // Click send button if available else { // Fallback: try to submit the closest form const form = event.target?.closest('form'); if (form) form.requestSubmit ? form.requestSubmit() : form.submit(); // Modern vs older form submission } } else if (plainEnterFlag) { // If it's a plain Enter (and not a send combination) event.preventDefault(); event.stopImmediatePropagation(); insertNewline(activeTextareaElement); // Insert a newline } else { // Handle cases where a modifier key is pressed with Enter, but it's NOT the send combination // Example: Send key is Ctrl/Meta+Enter. If user presses Shift+Enter, allow native newline. let allowNativeBehavior = false; if (modifierKeyToSend === MODIFIER_KEYS.CTRL) { // Current send mode is Ctrl/Meta+Enter if (shiftOnlyFlag || altOnlyFlag) { // If user pressed Shift+Enter or Alt+Enter allowNativeBehavior = true; } } else if (modifierKeyToSend === MODIFIER_KEYS.SHIFT) { // Current send mode is Shift+Enter if (ctrlOrMetaOnlyFlag || altOnlyFlag) { // If user pressed Ctrl/Meta+Enter or Alt+Enter allowNativeBehavior = true; } } else if (modifierKeyToSend === MODIFIER_KEYS.ALT) { // Current send mode is Alt+Enter if (ctrlOrMetaOnlyFlag || shiftOnlyFlag) { // If user pressed Ctrl/Meta+Enter or Shift+Enter allowNativeBehavior = true; } } if (!allowNativeBehavior) { // If not a recognized "send" combo, not "plain Enter", and not an "allowed native behavior" combo, // then prevent default to avoid unexpected actions (e.g., Alt+Enter might trigger browser menus). event.preventDefault(); event.stopImmediatePropagation(); } // If allowNativeBehavior is true, do nothing, letting the browser/page handle it. } } function updateActiveTextareaListener() { if (activeTextarea) { const listenerAttached = activeTextarea.dataset.keydownListenerAttached === 'true'; const onGemini = window.location.hostname.includes('gemini.google.com'); const onAIStudio = window.location.hostname.includes('aistudio.google.com'); let shouldCurrentlyBeAttached = false; if (onGemini) { shouldCurrentlyBeAttached = isScriptGloballyEnabled && currentGlobalModifierKey !== MODIFIER_KEYS.NATIVE_GEMINI; } else if (onAIStudio) { shouldCurrentlyBeAttached = currentAIStudioKeyMode !== AISTUDIO_KEY_MODES.NATIVE_BEHAVIOR; } if (listenerAttached && !shouldCurrentlyBeAttached) { activeTextarea.removeEventListener('keydown', handleKeydown, true); // Use capture phase delete activeTextarea.dataset.keydownListenerAttached; // console.log(`[${SCRIPT_ID}] Keydown listener removed from:`, activeTextarea); } else if (!listenerAttached && shouldCurrentlyBeAttached) { activeTextarea.addEventListener('keydown', handleKeydown, true); // Use capture phase activeTextarea.dataset.keydownListenerAttached = 'true'; // console.log(`[${SCRIPT_ID}] Keydown listener attached to:`, activeTextarea); } } } function toggleScriptGlobally() { isScriptGloballyEnabled = !isScriptGloballyEnabled; GM_setValue(GM_GLOBAL_ENABLE_KEY_STORAGE, isScriptGloballyEnabled); updateActiveTextareaListener(); // Update listener status based on new global state registerMenuCommand(); // Update menu command text (Enable/Disable) showNotification(isScriptGloballyEnabled ? i18n.get('notifyScriptEnabled') : i18n.get('notifyScriptDisabled')); } function registerMenuCommand() { // Unregister previous commands to prevent duplicates menuCommandIds.forEach(id => { if (typeof GM_unregisterMenuCommand === 'function') try { GM_unregisterMenuCommand(id); } catch (e) { /* console.warn("Error unregistering menu command:", e); */ } }); menuCommandIds = []; // Clear the array try { const settingsCmdId = GM_registerMenuCommand(i18n.get('openSettingsMenu'), openSettings, 's'); // Access key 's' if (settingsCmdId) menuCommandIds.push(settingsCmdId); } catch (e) { console.error(`[${SCRIPT_ID}] Error registering 'Open Settings' menu command:`, e); } try { const toggleCmdText = isScriptGloballyEnabled ? i18n.get('disableScriptMenu') : i18n.get('enableScriptMenu'); const toggleCmdId = GM_registerMenuCommand(toggleCmdText, toggleScriptGlobally, 't'); // Access key 't' if (toggleCmdId) menuCommandIds.push(toggleCmdId); } catch (e) { console.error(`[${SCRIPT_ID}] Error registering toggle script menu command:`, e); } } // --- Initialization and Observation --- function findTextarea() { let el; const currentHost = window.location.hostname; if (currentHost.includes('aistudio.google.com')) { for (const selector of AISTUDIO_INPUT_SELECTORS) { el = document.querySelector(selector); if (el) return el; } } else if (currentHost.includes('gemini.google.com')) { el = document.querySelector(GEMINI_INPUT_SELECTOR_PRIMARY); if (el) return el; // Try primary first for (const selector of GEMINI_INPUT_SELECTORS_FALLBACK) { // Then fallbacks el = document.querySelector(selector); if (el) return el; } } return null; // Not found } function findSendButton() { let el; const currentHost = window.location.hostname; if (currentHost.includes('aistudio.google.com')) { for (const selector of AISTUDIO_SEND_BUTTON_SELECTORS) { el = document.querySelector(selector); if (el) return el; } } else if (currentHost.includes('gemini.google.com')) { for (const selector of GEMINI_SEND_BUTTON_SELECTORS) { el = document.querySelector(selector); if (el) return el; } } return null; // Not found } // Debounced handler for DOM changes to avoid performance issues const debouncedDOMChangeHandler = debounce(() => { const newTextarea = findTextarea(); if (newTextarea) { const onGemini = window.location.hostname.includes('gemini.google.com'); const onAIStudio = window.location.hostname.includes('aistudio.google.com'); // Determine if a listener should be attached based on current settings and page const needsListenerOnGemini = onGemini && isScriptGloballyEnabled && currentGlobalModifierKey !== MODIFIER_KEYS.NATIVE_GEMINI; const needsListenerOnAIStudio = onAIStudio && currentAIStudioKeyMode !== AISTUDIO_KEY_MODES.NATIVE_BEHAVIOR; const listenerShouldBeAttached = needsListenerOnGemini || needsListenerOnAIStudio; // If textarea changed, or if listener was missing but should be attached if (activeTextarea !== newTextarea || (activeTextarea && activeTextarea.dataset.keydownListenerAttached !== 'true' && listenerShouldBeAttached)) { if (activeTextarea && activeTextarea.dataset.keydownListenerAttached === 'true') { activeTextarea.removeEventListener('keydown', handleKeydown, true); delete activeTextarea.dataset.keydownListenerAttached; } activeTextarea = newTextarea; updateActiveTextareaListener(); // This will attach/detach based on current state } } else if (activeTextarea && activeTextarea.dataset.keydownListenerAttached === 'true') { // If textarea disappeared, remove listener activeTextarea.removeEventListener('keydown', handleKeydown, true); delete activeTextarea.dataset.keydownListenerAttached; activeTextarea = null; } // Update AI Studio button hint if on that page if (window.location.hostname.includes('aistudio.google.com')) { updateAIStudioButtonModifierHint(); } }, 300); // 300ms debounce time const observeDOM = function() { const observer = new MutationObserver((mutationsList, obs) => { debouncedDOMChangeHandler(); // Call debounced handler on DOM changes }); // Observe changes in the body and its subtree observer.observe(document.body, { childList: true, subtree: true }); }; function init() { i18n.detectLanguage(); loadSettings(); createSettingsUI(); // Ensure UI is created after settings are loaded for correct i18n display registerMenuCommand(); observeDOM(); // Start observing DOM changes // Initial setup for textarea and button hint activeTextarea = findTextarea(); updateActiveTextareaListener(); // Delay initial AI Studio button hint update slightly to allow page to settle if (window.location.hostname.includes('aistudio.google.com')) { setTimeout(updateAIStudioButtonModifierHint, 500); // Increased delay for AI Studio } else { updateAIStudioButtonModifierHint(); // For Gemini or other contexts, update immediately (though it does nothing if not AI Studio) } console.log(`[${SCRIPT_ID}] Initialized. Gemini Mode: ${currentGlobalModifierKey}, AI Studio Mode: ${currentAIStudioKeyMode}, Script Enabled: ${isScriptGloballyEnabled}`); } // Wait for DOM to be ready before initializing if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); // DOM already loaded } })();