Gemini & AI Studio Enter Key Customizer

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.

// ==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://greasyfork.dpdns.org/en/users/1467948-stonedkhajiit
// @version           1.0.0
// @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.
// @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.
// @description:ja    GeminiとAI StudioのEnterキー動作を変更。Gemini: Enterで改行、修飾キー+Enterで送信。AI Studio: 送信キーを選択可 (修飾キー、Enter送信、標準)。設定パネルあり。
// @description:zh-TW 調整 Gemini 與 AI Studio 的 Enter 鍵行為。Gemini:Enter 鍵換行,組合鍵送出。AI Studio:可自訂傳送鍵 (組合鍵、Enter即送、或預設)。附設定面板。
// @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.0';

    // 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*="傳送"]',
        '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) ---
    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:',
                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+Enter to send',
                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+Enter sends)',
                modifierCtrl: 'Ctrl',
                modifierShift: 'Shift',
                modifierAlt: 'Alt',
            },
            'zh-TW': {
                notifySettingsSaved: '設定已儲存!',
                notifyScriptEnabled: '自訂 Enter 鍵行為已啟用。若未立即生效請重載頁面。',
                notifyScriptDisabled: '自訂 Enter 鍵行為已停用。若未立即生效請重載頁面。',
                settingsTitle: '腳本設定',
                geminiKeyModeLabel: 'Gemini 按鍵模式:',
                aiStudioKeyModeLabel: 'AI Studio 按鍵模式:',
                closeButton: '關閉',
                saveButton: '儲存',
                openSettingsMenu: '設定 Enter 鍵行為...',
                enableScriptMenu: '啟用自訂 Enter 鍵行為',
                disableScriptMenu: '停用自訂 Enter 鍵行為',
                geminiCtrl: 'Ctrl+Enter 送出',
                geminiShift: 'Shift+Enter 送出',
                geminiAlt: 'Alt+Enter 送出',
                geminiNative: '使用 Gemini 原生行為 (Enter 送出)',
                aiStudioShift: 'Shift+Enter 送出',
                aiStudioAlt: 'Alt+Enter 送出',
                aiStudioSpecific: 'Enter 送出,Shift+Enter 換行',
                aiStudioNative: '使用 AI Studio 原生行為 (Ctrl+Enter 送出)',
                modifierCtrl: 'Ctrl',
                modifierShift: 'Shift',
                modifierAlt: 'Alt',
            },
            'ja': {
                notifySettingsSaved: '設定を保存しました!',
                notifyScriptEnabled: 'Enterキーのカスタム動作が有効になりました。必要に応じてページを再読み込みしてください。',
                notifyScriptDisabled: 'Enterキーのカスタム動作が無効になりました。必要に応じてページを再読み込みしてください。',
                settingsTitle: 'スクリプト設定',
                geminiKeyModeLabel: 'Gemini のキーモード:',
                aiStudioKeyModeLabel: 'AI Studio のキーモード:',
                closeButton: '閉じる',
                saveButton: '保存',
                openSettingsMenu: 'Enterキーの動作を設定...',
                enableScriptMenu: 'Enterキーのカスタム動作を有効化',
                disableScriptMenu: 'Enterキーのカスタム動作を無効化',
                geminiCtrl: 'Ctrl+Enter で送信',
                geminiShift: 'Shift+Enter で送信',
                geminiAlt: 'Alt+Enter で送信',
                geminiNative: 'Gemini ネイティブ動作を使用 (Enter で送信)',
                aiStudioShift: 'Shift+Enter で送信',
                aiStudioAlt: 'Alt+Enter で送信',
                aiStudioSpecific: 'Enter で送信、Shift+Enter で改行',
                aiStudioNative: 'AI Studio ネイティブ動作を使用 (Ctrl+Enter で送信)',
                modifierCtrl: 'Ctrl',
                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';
            } else {
                this.currentLang = 'en';
            }
        },
        get(key, ...args) {
            const langStrings = this.strings[this.currentLang] || this.strings.en;
            const template = langStrings[key] || (this.strings.en && this.strings.en[key]);
            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}`;
        }
    };

    // --- UI Functions ---
    function createSettingsUI() {
        if (document.getElementById('gemini-ai-settings-overlay')) return;

        const overlay = document.createElement('div');
        overlay.id = 'gemini-ai-settings-overlay';
        overlay.classList.add('hidden');

        const style = document.createElement('style');
        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 title = document.createElement('h2');
        title.textContent = i18n.get('settingsTitle');
        settingsPanel.appendChild(title);

        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');
        document.body.appendChild(notificationDiv);

        closeButton.addEventListener('click', closeSettings);
        saveButton.addEventListener('click', saveSettingsFromUI);
        overlay.addEventListener('click', (e) => { if (e.target === overlay) closeSettings(); });

        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;

        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();
        populateSettingsUI();
        const overlay = document.getElementById('gemini-ai-settings-overlay');
        if (overlay) {
            overlay.classList.remove('hidden');
            void overlay.offsetWidth;
            overlay.classList.add('visible');
        }
    }

    function closeSettings() {
        const overlay = document.getElementById('gemini-ai-settings-overlay');
        if (overlay) {
            overlay.classList.remove('visible');
            setTimeout(() => {
                if (!overlay.classList.contains('visible')) {
                     overlay.classList.add('hidden');
                }
            }, 200);
        }
    }

    function showNotification(message) {
        const notificationDiv = document.getElementById('gemini-ai-notification');
        if (notificationDiv) {
            notificationDiv.textContent = message;
            notificationDiv.classList.remove('hidden');
            void notificationDiv.offsetWidth;
            notificationDiv.classList.add('visible');

            setTimeout(() => {
                notificationDiv.classList.remove('visible');
                setTimeout(() => {
                    if (!notificationDiv.classList.contains('visible')) {
                        notificationDiv.classList.add('hidden');
                    }
                }, 300);
            }, 2500);
        }
    }

    // --- Core Logic Functions ---
    function loadSettings() {
        isScriptGloballyEnabled = GM_getValue(GM_GLOBAL_ENABLE_KEY_STORAGE, true);

        const savedGeminiModifier = GM_getValue(GM_MODIFIER_KEY_STORAGE, DEFAULT_MODIFIER_KEY);
        currentGlobalModifierKey = Object.values(MODIFIER_KEYS).includes(savedGeminiModifier) ? savedGeminiModifier : DEFAULT_MODIFIER_KEY;

        const savedAIStudioMode = GM_getValue(GM_AISTUDIO_MODE_STORAGE, DEFAULT_AISTUDIO_KEY_MODE);
        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);
        }

        const selectedAIStudioMode = document.querySelector('input[name="aistudioKeyMode"]:checked')?.value;
        if (selectedAIStudioMode) {
            saveAIStudioKeyModeSetting(selectedAIStudioMode);
        } else {
            saveAIStudioKeyModeSetting(DEFAULT_AISTUDIO_KEY_MODE);
        }

        registerMenuCommand();
        showNotification(i18n.get('notifySettingsSaved'));
        closeSettings();
    }

    function saveGeminiKeyModeSetting(key) {
        if (Object.values(MODIFIER_KEYS).includes(key) && key !== MODIFIER_KEYS.NONE) {
            currentGlobalModifierKey = key;
            GM_setValue(GM_MODIFIER_KEY_STORAGE, key);
            updateActiveTextareaListener(); // Important to update listener status
        }
    }

    function saveAIStudioKeyModeSetting(mode) {
        if (Object.values(AISTUDIO_KEY_MODES).includes(mode)) {
            currentAIStudioKeyMode = mode;
            GM_setValue(GM_AISTUDIO_MODE_STORAGE, mode);
            updateAIStudioButtonModifierHint();
            updateActiveTextareaListener();
        }
    }

    function updateAIStudioButtonModifierHint() {
        if (!window.location.hostname.includes('aistudio.google.com')) {
            return;
        }

        const sendButton = findSendButton();
        if (sendButton) {
            const hintSpan = sendButton.querySelector(AISTUDIO_SEND_BUTTON_MODIFIER_HINT_SELECTOR);
            if (hintSpan) {
                hintSpan.style.display = 'inline';
                let hintText = i18n.get('modifierCtrl');

                switch (currentAIStudioKeyMode) {
                    case AISTUDIO_KEY_MODES.SHIFT_SEND:
                        hintText = i18n.get('modifierShift');
                        break;
                    case AISTUDIO_KEY_MODES.ALT_SEND:
                        hintText = i18n.get('modifierAlt');
                        break;
                    case AISTUDIO_KEY_MODES.AISTUDIO_SPECIFIC:
                        hintSpan.style.display = 'none';
                        hintText = '';
                        break;
                    case AISTUDIO_KEY_MODES.NATIVE_BEHAVIOR:
                        hintText = i18n.get('modifierCtrl');
                        break;
                }
                hintSpan.textContent = hintSpan.style.display !== 'none' ? hintText + ' ' : '';
            }
        }
    }


    function handleKeydown(event) {
        if (window.location.hostname.includes('gemini.google.com') && !isScriptGloballyEnabled) {
            return;
        }
        if (event.target !== activeTextarea && (!activeTextarea || !activeTextarea.contains(event.target))) {
            return;
        }

        const currentHost = window.location.hostname;
        const ctrlOnly = event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey;
        const shiftOnly = event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey;
        const altOnly = event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey;
        const plainEnter = !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey;

        if (event.key === 'Enter') {
            if (currentHost.includes('gemini.google.com')) {
                if (currentGlobalModifierKey === MODIFIER_KEYS.NATIVE_GEMINI) {
                    return;
                }
                applyModifierSendBehavior(event, currentGlobalModifierKey, ctrlOnly, shiftOnly, altOnly, plainEnter, activeTextarea);
            } else if (currentHost.includes('aistudio.google.com')) {
                if (currentAIStudioKeyMode === AISTUDIO_KEY_MODES.NATIVE_BEHAVIOR) {
                    return;
                } else if (currentAIStudioKeyMode === AISTUDIO_KEY_MODES.AISTUDIO_SPECIFIC) {
                    if (plainEnter) {
                        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) {
                        event.preventDefault(); event.stopImmediatePropagation();
                        insertNewline(activeTextarea);
                    } else {
                        event.preventDefault(); event.stopImmediatePropagation();
                    }
                } else if (currentAIStudioKeyMode === AISTUDIO_KEY_MODES.SHIFT_SEND) {
                    applyModifierSendBehavior(event, MODIFIER_KEYS.SHIFT, ctrlOnly, shiftOnly, altOnly, plainEnter, activeTextarea);
                } else if (currentAIStudioKeyMode === AISTUDIO_KEY_MODES.ALT_SEND) {
                    applyModifierSendBehavior(event, MODIFIER_KEYS.ALT, ctrlOnly, shiftOnly, altOnly, plainEnter, activeTextarea);
                }
            }
        }
    }

    function insertNewline(element) {
        if (!element) return;
        if (element.isContentEditable) {
            element.focus();
            let success = false;
            try { success = document.execCommand('insertParagraph', false, null); } catch (e) { /* console.warn("execCommand('insertParagraph') failed:", e); */ }
            if (!success) {
                try {
                    const selection = window.getSelection();
                    if (selection && selection.rangeCount > 0) {
                        const range = selection.getRangeAt(0);
                        const br = document.createElement('br');
                        range.deleteContents();
                        range.insertNode(br);
                        const newRange = document.createRange();
                        newRange.setStartAfter(br);
                        newRange.collapse(true);
                        selection.removeAllRanges();
                        selection.addRange(newRange);
                    } else {
                       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;
            element.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
        }
    }


    function applyModifierSendBehavior(event, modifierKeyToSend, ctrlOnly, shiftOnly, altOnly, plainEnter, activeTextareaElement) {
        let send = false;
        if ((modifierKeyToSend === MODIFIER_KEYS.CTRL && ctrlOnly) ||
            (modifierKeyToSend === MODIFIER_KEYS.SHIFT && shiftOnly) ||
            (modifierKeyToSend === MODIFIER_KEYS.ALT && altOnly)) {
            send = true;
        }

        if (send) {
            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 (plainEnter) {
            event.preventDefault(); event.stopImmediatePropagation();
            insertNewline(activeTextareaElement);
        } else {
            const isShiftEnterForNativeNewline = shiftOnly && modifierKeyToSend !== MODIFIER_KEYS.SHIFT;
            if (!isShiftEnterForNativeNewline) {
                 event.preventDefault(); event.stopImmediatePropagation();
            }
        }
    }

    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);
                delete activeTextarea.dataset.keydownListenerAttached;
                // console.log(`[${SCRIPT_ID}] Keydown listener removed from:`, activeTextarea);
            } else if (!listenerAttached && shouldCurrentlyBeAttached) {
                activeTextarea.addEventListener('keydown', handleKeydown, true);
                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();
        registerMenuCommand();
        showNotification(isScriptGloballyEnabled ? i18n.get('notifyScriptEnabled') : i18n.get('notifyScriptDisabled'));
    }

    function registerMenuCommand() {
        menuCommandIds.forEach(id => { if (typeof GM_unregisterMenuCommand === 'function') try { GM_unregisterMenuCommand(id); } catch (e) { /* console.warn("Error unregistering menu command:", e); */ } });
        menuCommandIds = [];

        try {
            const settingsCmdId = GM_registerMenuCommand(i18n.get('openSettingsMenu'), openSettings, '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');
            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;
            for (const selector of GEMINI_INPUT_SELECTORS_FALLBACK) {
                el = document.querySelector(selector); if (el) return el;
            }
        }
        return null;
    }

    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;
    }

    // Debounced handler for DOM changes
    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');
            const needsListenerOnGemini = onGemini && isScriptGloballyEnabled && currentGlobalModifierKey !== MODIFIER_KEYS.NATIVE_GEMINI;
            const needsListenerOnAIStudio = onAIStudio && currentAIStudioKeyMode !== AISTUDIO_KEY_MODES.NATIVE_BEHAVIOR;
            const listenerShouldBeAttached = needsListenerOnGemini || needsListenerOnAIStudio;

            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') {
            activeTextarea.removeEventListener('keydown', handleKeydown, true);
            delete activeTextarea.dataset.keydownListenerAttached;
            activeTextarea = null;
        }

        if (window.location.hostname.includes('aistudio.google.com')) {
            updateAIStudioButtonModifierHint();
        }
    }, 300); // 300ms debounce time

    const observeDOM = function() {
        const observer = new MutationObserver((mutationsList, obs) => {
            debouncedDOMChangeHandler();
        });
        observer.observe(document.body, { childList: true, subtree: true });
    };

    function init() {
        i18n.detectLanguage();
        loadSettings();
        createSettingsUI();
        registerMenuCommand();
        observeDOM();

        // 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
        } else {
            updateAIStudioButtonModifierHint(); // For Gemini or other contexts, update immediately (though it does nothing)
        }

        console.log(`[${SCRIPT_ID}] Initialized. Gemini Mode: ${currentGlobalModifierKey}, AI Studio Mode: ${currentAIStudioKeyMode}, Script Enabled: ${isScriptGloballyEnabled}`);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();
长期地址
遇到问题?请前往 GitHub 提 Issues,或加Q群1031348184

赞助商

Fishcpy

广告

Rainyun

注册一下就行

Rainyun

一年攒够 12 元