// ==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.1.0
// @description Modifies Enter key behavior in Gemini (incl. edit mode) & AI Studio (main input). Configurable send keys (Ctrl/Cmd, Shift, Alt) + Enter for send/save, Enter for newline; or native behaviors. Enhanced settings panel.
// @description:en Modifies Enter key behavior in Gemini (incl. edit mode) & AI Studio (main input). Configurable send keys (Ctrl/Cmd, Shift, Alt) + Enter for send/save, Enter for newline; or native behaviors. Enhanced settings panel.
// @description:ja Gemini(編集モード含む)およびAI Studio(メイン入力)のEnterキー動作を変更。送信/保存キーを選択可 (Ctrl/Cmd, Shift, Alt) + Enter、Enterで改行。または標準動作。強化された設定パネルあり。
// @description:zh-TW 調整 Gemini (包含編輯模式) 與 AI Studio (主要輸入框) 的 Enter 鍵行為。可自訂傳送/儲存鍵 (Ctrl/Cmd, Shift, Alt) + Enter,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';
const SCRIPT_ID = 'GeminiEnterNewlineMultiSite_v1.1.0';
const DEBUG_MODE = false;
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_EDIT_INPUT_SELECTORS = ['user-query-content.edit-mode textarea.mat-mdc-input-element'];
const GEMINI_SEND_BUTTON_SELECTORS = [
'button[aria-label*="Send"]', 'button[aria-label*="傳送"]',
'button[aria-label*="送信"]', 'button[data-test-id="send-button"]',
];
const GEMINI_SAVE_EDIT_BUTTON_SELECTORS = ['user-query-content.edit-mode button.update-button'];
const AISTUDIO_SEND_BUTTON_SELECTORS = ['button[aria-label="Run"]', 'button[aria-label="Submit"]'];
const AISTUDIO_SEND_BUTTON_MODIFIER_HINT_SELECTOR = 'span.secondary-key';
const GM_GLOBAL_ENABLE_KEY_STORAGE = 'geminiEnterGlobalEnable_v1_5';
const SITE_MODES = { CUSTOM: 'custom', NATIVE: 'native' };
const INPUT_TYPES = { MAIN: 'main', EDIT: 'edit', UNKNOWN: 'unknown' };
const GM_GEMINI_CONFIG_STORAGE = 'geminiEnterConfig_v1_5_gemini';
const DEFAULT_GEMINI_CONFIG = {
mode: SITE_MODES.CUSTOM,
keys: { ctrlOrCmd: true, shift: false, alt: false }
};
const GM_AISTUDIO_CONFIG_STORAGE = 'aiStudioEnterConfig_v1_5_aistudio';
const DEFAULT_AISTUDIO_CONFIG = {
mode: SITE_MODES.NATIVE,
keys: { ctrlOrCmd: true, shift: false, alt: false }
};
let activeInputInfo = { element: null, type: INPUT_TYPES.UNKNOWN };
let isScriptGloballyEnabled = true;
let currentGeminiConfig = JSON.parse(JSON.stringify(DEFAULT_GEMINI_CONFIG));
let currentAIStudioConfig = JSON.parse(JSON.stringify(DEFAULT_AISTUDIO_CONFIG));
let menuCommandIds = [];
function logDebug(...args) {
if (DEBUG_MODE) {
console.log(`[${SCRIPT_ID}]`, ...args);
}
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => { clearTimeout(timeout); func(...args); };
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
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', closeButton: 'Close', saveButton: 'Save',
openSettingsMenu: 'Configure Enter Key Behavior...',
enableScriptMenu: 'Enable Custom Enter Key Behavior',
disableScriptMenu: 'Disable Custom Enter Key Behavior',
geminiSectionTitle: 'Gemini (gemini.google.com):',
aiStudioSectionTitle: 'AI Studio (aistudio.google.com):',
behaviorLabel: 'Enter Key Behavior:',
customBehavior: 'Custom (Enter for newline, select send/save keys below)',
nativeBehavior: 'Use Native Site Behavior (Script does nothing)',
geminiNativeBehaviorSpecific: 'Use Gemini Native Behavior (Enter sends/saves)',
aiStudioNativeBehaviorSpecific: 'Use AI Studio Native Behavior (Default: Ctrl/Cmd+Enter sends)',
sendKeysLabel: 'Send/Save with (Modifier + Enter):',
keyCtrlCmd: 'Ctrl / ⌘ Cmd',
keyShift: 'Shift',
keyAlt: 'Alt',
hintCtrlCmd: 'Ctrl/Cmd', hintShift: 'Shift', hintAlt: 'Alt',
hintOr: 'or',
hintPreviewWillUse: 'Will use:',
hintPreviewToSend: 'to send/save.',
hintPreviewNoKeySelected: 'No send/save key selected. Enter for newline.',
hintPreviewNative: 'Script will not modify Enter key behavior.',
},
'zh-TW': {
notifySettingsSaved: '設定已儲存!',
notifyScriptEnabled: '自訂 Enter 鍵行為已啟用。若未立即生效請重載頁面。',
notifyScriptDisabled: '自訂 Enter 鍵行為已停用。若未立即生效請重載頁面。',
settingsTitle: '腳本設定', closeButton: '關閉', saveButton: '儲存',
openSettingsMenu: '設定 Enter 鍵行為...',
enableScriptMenu: '啟用自訂 Enter 鍵行為',
disableScriptMenu: '停用自訂 Enter 鍵行為',
geminiSectionTitle: 'Gemini (gemini.google.com):',
aiStudioSectionTitle: 'AI Studio (aistudio.google.com):',
behaviorLabel: 'Enter 鍵行為:',
customBehavior: '自訂 (Enter 換行,於下方選擇傳送/儲存鍵)',
nativeBehavior: '使用網站原生行為 (腳本不介入)',
geminiNativeBehaviorSpecific: '使用 Gemini 原生行為 (Enter 即送出/儲存)',
aiStudioNativeBehaviorSpecific: '使用 AI Studio 原生行為 (預設 Ctrl/Cmd+Enter 送出)',
sendKeysLabel: '使用組合鍵傳送/儲存 (組合鍵 + Enter):',
keyCtrlCmd: 'Ctrl / ⌘ Cmd',
keyShift: 'Shift',
keyAlt: 'Alt',
hintCtrlCmd: 'Ctrl/Cmd', hintShift: 'Shift', hintAlt: 'Alt',
hintOr: '或',
hintPreviewWillUse: '將使用:',
hintPreviewToSend: '進行傳送/儲存。',
hintPreviewNoKeySelected: '未選擇傳送/儲存鍵。Enter 鍵將用於換行。',
hintPreviewNative: '腳本不會修改 Enter 鍵行為。',
},
'ja': {
notifySettingsSaved: '設定を保存しました!',
notifyScriptEnabled: 'Enterキーのカスタム動作が有効になりました。必要に応じてページを再読み込みしてください。',
notifyScriptDisabled: 'Enterキーのカスタム動作が無効になりました。必要に応じてページを再読み込みしてください。',
settingsTitle: 'スクリプト設定', closeButton: '閉じる', saveButton: '保存',
openSettingsMenu: 'Enterキーの動作を設定...',
enableScriptMenu: 'Enterキーのカスタム動作を有効化',
disableScriptMenu: 'Enterキーのカスタム動作を無効化',
geminiSectionTitle: 'Gemini (gemini.google.com):',
aiStudioSectionTitle: 'AI Studio (aistudio.google.com):',
behaviorLabel: 'Enterキーの動作:',
customBehavior: 'カスタム (Enterで改行、送信/保存キーを以下で選択)',
nativeBehavior: 'サイトのネイティブ動作を使用 (スクリプトは何もしません)',
geminiNativeBehaviorSpecific: 'Geminiネイティブ動作を使用 (Enterで送信/保存)',
aiStudioNativeBehaviorSpecific: 'AI Studioネイティブ動作を使用 (デフォルトはCtrl/Cmd+Enterで送信)',
sendKeysLabel: '修飾キーで送信/保存 (修飾キー + Enter):',
keyCtrlCmd: 'Ctrl / ⌘ Cmd',
keyShift: 'Shift',
keyAlt: 'Alt',
hintCtrlCmd: 'Ctrl/Cmd', hintShift: 'Shift', hintAlt: 'Alt',
hintOr: 'または',
hintPreviewWillUse: '使用するキー:',
hintPreviewToSend: 'で送信/保存します。',
hintPreviewNoKeySelected: '送信/保存キーが選択されていません。Enterキーは改行に使用されます。',
hintPreviewNative: 'スクリプトはEnterキーの動作を変更しません。',
}
},
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;
logDebug(`Missing i18n string for key: ${key} in lang: ${this.currentLang}`);
return `Missing string: ${key}`;
}
};
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: 460px;
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 p,
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"],
body.userscript-dark-mode #gemini-ai-settings-panel input[type="checkbox"] { 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: 600; margin-bottom: 0.5rem; margin-top: 1rem; }
#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.4rem; }
#gemini-ai-settings-panel label { display: inline-flex; align-items: center; cursor: pointer; font-size: 0.875rem; }
#gemini-ai-settings-panel label[title] { cursor: help; border-bottom: 1px dotted; }
body.userscript-dark-mode #gemini-ai-settings-panel label[title] { border-bottom-color: #718096; }
#gemini-ai-settings-panel input[type="radio"],
#gemini-ai-settings-panel input[type="checkbox"] { margin-right: 0.5rem; cursor: pointer; transform: scale(0.95); }
#gemini-ai-settings-panel input[type="checkbox"]:disabled + label { color: #9ca3af !important; cursor: not-allowed; }
body.userscript-dark-mode #gemini-ai-settings-panel input[type="checkbox"]:disabled + label { color: #718096 !important; }
.settings-preview-text { font-size: 0.8rem; color: #6b7280; margin-top: 0.5rem; padding-left: 0.5rem; min-height: 1em; font-style: italic; }
body.userscript-dark-mode .settings-preview-text { color: #9ca3af; }
.settings-buttons-container { display: flex; justify-content: flex-end; margin-top: 1.2rem; 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');
titleElement.textContent = i18n.get('settingsTitle');
settingsPanel.appendChild(titleElement);
const geminiTitle = document.createElement('h3');
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');
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 createSiteSettingSection = (container, siteConfig, siteName) => {
const behaviorTitle = document.createElement('p');
behaviorTitle.textContent = i18n.get('behaviorLabel');
behaviorTitle.style.fontWeight = '500'; behaviorTitle.style.marginBottom = '0.3rem';
container.appendChild(behaviorTitle);
const modes = [
{ value: SITE_MODES.CUSTOM, labelKey: 'customBehavior' },
{ value: SITE_MODES.NATIVE, labelKey: siteName === 'gemini' ? 'geminiNativeBehaviorSpecific' : (siteName === 'aistudio' ? 'aiStudioNativeBehaviorSpecific' : 'nativeBehavior') }
];
if (siteName === 'aistudio' && siteConfig.mode === 'site_specific') {
logDebug('Mapping obsolete AI Studio SITE_SPECIFIC mode to CUSTOM for settings UI.');
siteConfig.mode = SITE_MODES.CUSTOM;
}
modes.forEach(modeInfo => {
const div = document.createElement('div');
const input = document.createElement('input');
input.type = 'radio'; input.name = `${siteName}KeyModeBehavior`;
input.id = `${siteName}-mode-${modeInfo.value}`; input.value = modeInfo.value;
if (siteConfig.mode === modeInfo.value) input.checked = true;
const label = document.createElement('label');
label.htmlFor = input.id; label.textContent = i18n.get(modeInfo.labelKey);
div.appendChild(input); div.appendChild(label);
container.appendChild(div);
});
const sendKeysLabelEl = document.createElement('p');
sendKeysLabelEl.textContent = i18n.get('sendKeysLabel');
sendKeysLabelEl.style.fontWeight = '500'; sendKeysLabelEl.style.marginTop = '0.7rem'; sendKeysLabelEl.style.marginBottom = '0.3rem';
container.appendChild(sendKeysLabelEl);
const keysContainer = document.createElement('div');
keysContainer.id = `${siteName}-custom-keys-container`;
container.appendChild(keysContainer);
const modifierCheckboxesSetup = [
{ keyName: 'ctrlOrCmd', labelKey: 'keyCtrlCmd' },
{ keyName: 'shift', labelKey: 'keyShift' },
{ keyName: 'alt', labelKey: 'keyAlt' },
];
modifierCheckboxesSetup.forEach(opt => {
const div = document.createElement('div');
const input = document.createElement('input');
input.type = 'checkbox'; input.name = `${siteName}SendKey`; input.id = `${siteName}-key-${opt.keyName}`; input.value = opt.keyName;
if (siteConfig.keys && typeof siteConfig.keys[opt.keyName] === 'boolean') {
input.checked = siteConfig.keys[opt.keyName];
}
const label = document.createElement('label');
label.htmlFor = input.id; label.textContent = i18n.get(opt.labelKey);
div.appendChild(input); div.appendChild(label);
keysContainer.appendChild(div);
});
const previewTextEl = document.createElement('p');
previewTextEl.className = 'settings-preview-text';
keysContainer.appendChild(previewTextEl);
const updatePreviewText = () => {
const currentMode = container.querySelector(`input[name="${siteName}KeyModeBehavior"]:checked`)?.value;
if (currentMode === SITE_MODES.NATIVE) {
previewTextEl.textContent = i18n.get('hintPreviewNative'); return;
}
const selectedKeys = [];
modifierCheckboxesSetup.forEach(opt => {
const checkbox = container.querySelector(`#${siteName}-key-${opt.keyName}`);
if (checkbox && checkbox.checked) selectedKeys.push(i18n.get(opt.labelKey));
});
if (selectedKeys.length > 0) {
previewTextEl.textContent = `${i18n.get('hintPreviewWillUse')} ${selectedKeys.join(` ${i18n.get('hintOr')} `)} + Enter ${i18n.get('hintPreviewToSend')}`;
} else { previewTextEl.textContent = i18n.get('hintPreviewNoKeySelected');}
};
const customModeRadio = container.querySelector(`input[value="${SITE_MODES.CUSTOM}"]`);
const toggleCheckboxesAndPreview = () => {
const currentMode = container.querySelector(`input[name="${siteName}KeyModeBehavior"]:checked`)?.value;
const useCustomBehavior = currentMode === SITE_MODES.CUSTOM;
sendKeysLabelEl.style.color = useCustomBehavior ? '' : (document.body.classList.contains('userscript-dark-mode') ? '#718096':'#9ca3af');
keysContainer.style.opacity = useCustomBehavior ? '1' : '0.5';
previewTextEl.style.display = 'block';
modifierCheckboxesSetup.forEach(opt => {
const checkbox = container.querySelector(`#${siteName}-key-${opt.keyName}`);
if (checkbox) {
checkbox.disabled = !useCustomBehavior;
checkbox.addEventListener('change', updatePreviewText);
}
});
updatePreviewText();
};
container.querySelectorAll(`input[name="${siteName}KeyModeBehavior"]`).forEach(radio => {
radio.addEventListener('change', toggleCheckboxesAndPreview);
});
toggleCheckboxesAndPreview();
};
const geminiSectionTitleEl = geminiOptionsDiv.previousElementSibling;
if (geminiSectionTitleEl && geminiSectionTitleEl.tagName === 'H3') geminiSectionTitleEl.textContent = i18n.get('geminiSectionTitle');
createSiteSettingSection(geminiOptionsDiv, currentGeminiConfig, 'gemini');
const aiStudioSectionTitleEl = aistudioOptionsDiv.previousElementSibling;
if (aiStudioSectionTitleEl && aiStudioSectionTitleEl.tagName === 'H3') aiStudioSectionTitleEl.textContent = i18n.get('aiStudioSectionTitle');
createSiteSettingSection(aistudioOptionsDiv, currentAIStudioConfig, 'aistudio');
}
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);
}
}
function loadConfigForSite(storageKey, defaultConfig, siteName) {
const savedConfigString = GM_getValue(storageKey);
let configToReturn = JSON.parse(JSON.stringify(defaultConfig));
if (savedConfigString) {
try {
const savedConfig = JSON.parse(savedConfigString);
if (siteName === 'aistudio' && savedConfig.mode === 'site_specific') {
logDebug(`Migrating AI Studio mode from 'site_specific' to '${DEFAULT_AISTUDIO_CONFIG.mode}' for key ${storageKey}`);
configToReturn.mode = DEFAULT_AISTUDIO_CONFIG.mode;
configToReturn.keys = {
ctrlOrCmd: typeof savedConfig.keys?.ctrlOrCmd === 'boolean' ? savedConfig.keys.ctrlOrCmd : DEFAULT_AISTUDIO_CONFIG.keys.ctrlOrCmd,
shift: typeof savedConfig.keys?.shift === 'boolean' ? savedConfig.keys.shift : DEFAULT_AISTUDIO_CONFIG.keys.shift,
alt: typeof savedConfig.keys?.alt === 'boolean' ? savedConfig.keys.alt : DEFAULT_AISTUDIO_CONFIG.keys.alt,
};
} else {
configToReturn.mode = Object.values(SITE_MODES).includes(savedConfig.mode) ? savedConfig.mode : defaultConfig.mode;
configToReturn.keys = {
ctrlOrCmd: typeof savedConfig.keys?.ctrlOrCmd === 'boolean' ? savedConfig.keys.ctrlOrCmd : defaultConfig.keys.ctrlOrCmd,
shift: typeof savedConfig.keys?.shift === 'boolean' ? savedConfig.keys.shift : defaultConfig.keys.shift,
alt: typeof savedConfig.keys?.alt === 'boolean' ? savedConfig.keys.alt : defaultConfig.keys.alt,
};
}
} catch (e) { logDebug(`Error parsing config from ${storageKey}, using defaults.`, e); }
}
return configToReturn;
}
function loadSettings() {
isScriptGloballyEnabled = GM_getValue(GM_GLOBAL_ENABLE_KEY_STORAGE, true);
currentGeminiConfig = loadConfigForSite(GM_GEMINI_CONFIG_STORAGE, DEFAULT_GEMINI_CONFIG, 'gemini');
currentAIStudioConfig = loadConfigForSite(GM_AISTUDIO_CONFIG_STORAGE, DEFAULT_AISTUDIO_CONFIG, 'aistudio');
}
function saveConfigForSite(storageKey, newConfig, defaultConfig) {
if (!Object.values(SITE_MODES).includes(newConfig.mode)) newConfig.mode = defaultConfig.mode;
newConfig.keys = {
ctrlOrCmd: typeof newConfig.keys?.ctrlOrCmd === 'boolean' ? newConfig.keys.ctrlOrCmd : false,
shift: typeof newConfig.keys?.shift === 'boolean' ? newConfig.keys.shift : false,
alt: typeof newConfig.keys?.alt === 'boolean' ? newConfig.keys.alt : false,
};
GM_setValue(storageKey, JSON.stringify(newConfig));
return newConfig;
}
function saveSettingsFromUI() {
const selectedGeminiMode = document.querySelector('input[name="geminiKeyModeBehavior"]:checked')?.value || DEFAULT_GEMINI_CONFIG.mode;
const newGeminiKeys = {
ctrlOrCmd: document.getElementById('gemini-key-ctrlOrCmd')?.checked || false,
shift: document.getElementById('gemini-key-shift')?.checked || false,
alt: document.getElementById('gemini-key-alt')?.checked || false,
};
currentGeminiConfig = saveConfigForSite(GM_GEMINI_CONFIG_STORAGE, { mode: selectedGeminiMode, keys: newGeminiKeys }, DEFAULT_GEMINI_CONFIG);
const selectedAIStudioMode = document.querySelector('input[name="aistudioKeyModeBehavior"]:checked')?.value || DEFAULT_AISTUDIO_CONFIG.mode;
const newAIStudioKeys = {
ctrlOrCmd: document.getElementById('aistudio-key-ctrlOrCmd')?.checked || false,
shift: document.getElementById('aistudio-key-shift')?.checked || false,
alt: document.getElementById('aistudio-key-alt')?.checked || false,
};
currentAIStudioConfig = saveConfigForSite(GM_AISTUDIO_CONFIG_STORAGE, { mode: selectedAIStudioMode, keys: newAIStudioKeys }, DEFAULT_AISTUDIO_CONFIG);
updateActiveInputListener(); updateAIStudioButtonModifierHint(); registerMenuCommand();
showNotification(i18n.get('notifySettingsSaved')); closeSettings();
}
function updateAIStudioButtonModifierHint() {
if (!window.location.hostname.includes('aistudio.google.com')) return;
const mainSendButton = document.querySelector(AISTUDIO_SEND_BUTTON_SELECTORS.join(', '));
if (!mainSendButton) return;
const hintSpan = mainSendButton.querySelector(AISTUDIO_SEND_BUTTON_MODIFIER_HINT_SELECTOR);
if (!hintSpan) return;
if (activeInputInfo.element && activeInputInfo.type === INPUT_TYPES.EDIT) {
hintSpan.style.display = 'none'; return;
}
hintSpan.style.display = 'inline'; let hintText = '';
if (currentAIStudioConfig.mode === SITE_MODES.NATIVE) {
hintText = i18n.get('hintCtrlCmd');
} else if (currentAIStudioConfig.mode === SITE_MODES.CUSTOM) {
const keyLabels = [];
if (currentAIStudioConfig.keys.ctrlOrCmd) keyLabels.push(i18n.get('hintCtrlCmd'));
if (currentAIStudioConfig.keys.shift) keyLabels.push(i18n.get('hintShift'));
if (currentAIStudioConfig.keys.alt) keyLabels.push(i18n.get('hintAlt'));
if (keyLabels.length > 0) hintText = keyLabels.join(` ${i18n.get('hintOr')} `);
else hintSpan.style.display = 'none';
} else {
hintSpan.style.display = 'none';
}
hintSpan.textContent = hintSpan.style.display !== 'none' ? hintText + ' ' : '';
}
function insertNewline(element) {
if (!element) return;
if (element.isContentEditable) {
element.focus(); let success = false;
try { success = document.execCommand('insertParagraph', false, null); } catch (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);
range.setStartAfter(br); range.collapse(true);
selection.removeAllRanges(); selection.addRange(range); element.focus();
} else { document.execCommand('insertHTML', false, '<br>');}
} catch (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 clickSubmitButtonForInput(targetElement, inputType) {
const button = findSubmitButtonForInput({element: targetElement, type: inputType });
logDebug('[clickSubmitButtonForInput] Target Element:', targetElement, 'Input Type:', inputType, 'Found Button:', button);
if (button && !button.disabled) {
button.click();
} else {
logDebug('[clickSubmitButtonForInput] Button not found or disabled. Fallback to form submit if applicable.');
const form = targetElement?.closest('form');
if (form) { if (typeof form.requestSubmit === 'function') form.requestSubmit(); else form.submit(); }
}
}
function processEnterKeyForSite(event, configuredSendKeys, isCtrlOrCmdPressed, isShiftPressed, isAltPressed, isPlainEnter, isAnyModifierPressed, activeInput) {
let shouldSend = false;
if (configuredSendKeys.ctrlOrCmd && isCtrlOrCmdPressed) shouldSend = true;
else if (configuredSendKeys.shift && isShiftPressed) shouldSend = true;
else if (configuredSendKeys.alt && isAltPressed) shouldSend = true;
logDebug('[processEnterKeyForSite] Should Send:', shouldSend, 'Plain Enter:', isPlainEnter, 'Any Modifier:', isAnyModifierPressed, 'Config:', configuredSendKeys, 'Active Input:', activeInput);
if (shouldSend) {
event.preventDefault(); event.stopImmediatePropagation();
clickSubmitButtonForInput(activeInput.element, activeInput.type);
} else if (isPlainEnter) {
event.preventDefault(); event.stopImmediatePropagation();
insertNewline(activeInput.element);
} else if (isAnyModifierPressed) {
event.preventDefault(); event.stopImmediatePropagation();
}
}
function handleKeydown(event) {
logDebug('[handleKeydown] Event on:', event.target, 'Active Input Element:', activeInputInfo.element, 'Type:', activeInputInfo.type);
if (event.isComposing) return;
if (!isScriptGloballyEnabled) return;
if (!activeInputInfo.element || (event.target !== activeInputInfo.element && !activeInputInfo.element.contains(event.target))) {
return;
}
const currentHost = window.location.hostname;
const isCtrlOrCmdPressed = (event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey;
const isShiftPressed = event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey;
const isAltPressed = event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey;
const plainEnter = !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey;
const anyModifierPressed = event.ctrlKey || event.shiftKey || event.altKey || event.metaKey;
if (event.key === 'Enter') {
logDebug('[handleKeydown] Enter pressed. Ctrl/Cmd:', isCtrlOrCmdPressed, 'Shift:', isShiftPressed, 'Alt:', isAltPressed, 'Plain:', plainEnter);
if (currentHost.includes('gemini.google.com')) {
if (currentGeminiConfig.mode === SITE_MODES.NATIVE) return;
if (currentGeminiConfig.mode === SITE_MODES.CUSTOM) {
processEnterKeyForSite(event, currentGeminiConfig.keys, isCtrlOrCmdPressed, isShiftPressed, isAltPressed, plainEnter, anyModifierPressed, activeInputInfo);
}
} else if (currentHost.includes('aistudio.google.com')) {
if (currentAIStudioConfig.mode === SITE_MODES.NATIVE) return;
if (currentAIStudioConfig.mode === SITE_MODES.CUSTOM && activeInputInfo.type === INPUT_TYPES.MAIN) {
logDebug('[handleKeydown] AI Studio CUSTOM mode for MAIN input.');
processEnterKeyForSite(event, currentAIStudioConfig.keys, isCtrlOrCmdPressed, isShiftPressed, isAltPressed, plainEnter, anyModifierPressed, activeInputInfo);
}
// AI Studio edit mode is no longer specifically handled by custom logic in this simplified version
}
}
}
function updateActiveInputListener() {
if (activeInputInfo.element) {
const listenerAttached = activeInputInfo.element.dataset.keydownListenerAttached === 'true';
let shouldCurrentlyBeAttached = false;
if (isScriptGloballyEnabled) {
const onGemini = window.location.hostname.includes('gemini.google.com');
const onAIStudio = window.location.hostname.includes('aistudio.google.com');
if (onGemini && currentGeminiConfig.mode === SITE_MODES.CUSTOM) {
shouldCurrentlyBeAttached = true;
} else if (onAIStudio && currentAIStudioConfig.mode === SITE_MODES.CUSTOM && activeInputInfo.type === INPUT_TYPES.MAIN) {
shouldCurrentlyBeAttached = true;
}
}
logDebug('[updateActiveInputListener] Element:', activeInputInfo.element, 'Type:', activeInputInfo.type, 'Listener Attached:', listenerAttached, 'Should be attached:', shouldCurrentlyBeAttached);
if (listenerAttached && !shouldCurrentlyBeAttached) {
activeInputInfo.element.removeEventListener('keydown', handleKeydown, true);
delete activeInputInfo.element.dataset.keydownListenerAttached;
logDebug('[updateActiveInputListener] Listener REMOVED from:', activeInputInfo.element);
} else if (!listenerAttached && shouldCurrentlyBeAttached) {
activeInputInfo.element.addEventListener('keydown', handleKeydown, true);
activeInputInfo.element.dataset.keydownListenerAttached = 'true';
logDebug('[updateActiveInputListener] Listener ADDED to:', activeInputInfo.element);
}
} else {
logDebug('[updateActiveInputListener] No active input element to update listener for.');
}
}
function toggleScriptGlobally() {
isScriptGloballyEnabled = !isScriptGloballyEnabled;
GM_setValue(GM_GLOBAL_ENABLE_KEY_STORAGE, isScriptGloballyEnabled);
if (activeInputInfo.element) {
updateActiveInputListener();
}
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) {} });
menuCommandIds = [];
try { const sId = GM_registerMenuCommand(i18n.get('openSettingsMenu'), openSettings, 's'); if (sId) menuCommandIds.push(sId); } catch (e) { console.error(`[${SCRIPT_ID}] Error registering 'Open Settings' menu command:`, e); }
try { const tId = GM_registerMenuCommand(isScriptGloballyEnabled ? i18n.get('disableScriptMenu') : i18n.get('enableScriptMenu'), toggleScriptGlobally, 't'); if (tId) menuCommandIds.push(tId); } catch (e) { console.error(`[${SCRIPT_ID}] Error registering toggle script menu command:`, e); }
}
function findActiveInputElement() {
const host = window.location.hostname;
let el;
logDebug('[findActiveInputElement] Searching on host:', host);
const focusedElement = document.activeElement;
if (focusedElement && (focusedElement.tagName === 'TEXTAREA' || focusedElement.isContentEditable)) {
logDebug('[findActiveInputElement] Focused element:', focusedElement, 'Tag:', focusedElement.tagName, 'ContentEditable:', focusedElement.isContentEditable);
if (host.includes('gemini.google.com')) {
const geminiEditParent = focusedElement.closest('user-query-content.edit-mode');
if (geminiEditParent && GEMINI_EDIT_INPUT_SELECTORS.some(s => focusedElement.matches(s))) {
logDebug('[findActiveInputElement] Found Gemini Edit Input via document.activeElement:', focusedElement);
return { element: focusedElement, type: INPUT_TYPES.EDIT };
}
}
// AI Studio edit detection via activeElement removed due to complexity and unreliability
}
if (host.includes('gemini.google.com')) {
for (const selector of GEMINI_EDIT_INPUT_SELECTORS) {
el = document.querySelector(selector);
if (el && (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0)) {
logDebug('[findActiveInputElement] Found Gemini Edit Input (Selector):', el, 'Selector:', selector);
return { element: el, type: INPUT_TYPES.EDIT };
}
}
}
// AI Studio edit input search via general selectors removed
if (host.includes('gemini.google.com')) {
el = document.querySelector(GEMINI_INPUT_SELECTOR_PRIMARY);
if (el && (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0)) {
logDebug('[findActiveInputElement] Found Gemini Main Input (Primary):', el);
return { element: el, type: INPUT_TYPES.MAIN };
}
for (const selector of GEMINI_INPUT_SELECTORS_FALLBACK) {
el = document.querySelector(selector);
if (el && (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0)) {
logDebug('[findActiveInputElement] Found Gemini Main Input (Fallback):', el, 'Selector:', selector);
return { element: el, type: INPUT_TYPES.MAIN };
}
}
} else if (host.includes('aistudio.google.com')) {
for (const selector of AISTUDIO_INPUT_SELECTORS) {
el = document.querySelector(selector);
if (el && (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0)) {
logDebug('[findActiveInputElement] Found AI Studio Main Input:', el, 'Selector:', selector);
return { element: el, type: INPUT_TYPES.MAIN };
}
}
}
logDebug('[findActiveInputElement] No specific active input found by selectors.');
return { element: null, type: INPUT_TYPES.UNKNOWN };
}
function findSubmitButtonForInput(inputInfo) {
if (!inputInfo || !inputInfo.element) {
logDebug('[findSubmitButtonForInput] No inputInfo or element provided.');
return null;
}
const host = window.location.hostname;
let selectors = [];
let searchContext = document;
if (host.includes('gemini.google.com')) {
selectors = inputInfo.type === INPUT_TYPES.EDIT ? GEMINI_SAVE_EDIT_BUTTON_SELECTORS : GEMINI_SEND_BUTTON_SELECTORS;
if (inputInfo.type === INPUT_TYPES.EDIT) {
const userQueryContent = inputInfo.element.closest('user-query-content.edit-mode');
if (userQueryContent) searchContext = userQueryContent;
}
} else if (host.includes('aistudio.google.com')) {
// Only main send buttons for AI Studio in this simplified version
if (inputInfo.type === INPUT_TYPES.MAIN) {
selectors = AISTUDIO_SEND_BUTTON_SELECTORS;
} else {
logDebug('[findSubmitButtonForInput] AI Studio: Edit mode buttons no longer specifically targeted by script.');
return null; // No button to find for AI Studio edit mode via script
}
}
logDebug('[findSubmitButtonForInput] Input Type:', inputInfo.type, 'Host:', host, 'Attempting selectors:', selectors, 'Search Context:', searchContext.tagName || 'document');
for (const selector of selectors) {
let button = null;
try {
button = searchContext.querySelector(selector);
} catch (e) {
logDebug('[findSubmitButtonForInput] Error using selector:', selector, e);
continue;
}
if (button && (button.offsetWidth > 0 || button.offsetHeight > 0 || button.getClientRects().length > 0)) {
logDebug('[findSubmitButtonForInput] Found visible button with selector:', selector, button);
return button;
} else if (button) {
logDebug('[findSubmitButtonForInput] Found button with selector but it is NOT VISIBLE:', selector, button);
} else {
logDebug('[findSubmitButtonForInput] No button found for selector:', selector, 'in context:', searchContext.tagName || 'document');
}
}
logDebug('[findSubmitButtonForInput] No submit button found for input type:', inputInfo.type, 'on host:', host);
return null;
}
const mutationObserverCallback = (mutationsList, observer) => {
logDebug('[MutationObserver] Generic DOM change detected, triggering debounced handler.');
debouncedDOMChangeHandler();
};
const debouncedDOMChangeHandler = debounce(() => {
logDebug('[debouncedDOMChangeHandler] Running...');
const oldActiveElement = activeInputInfo.element;
const oldActiveType = activeInputInfo.type;
const newActiveInfo = findActiveInputElement();
if (oldActiveElement !== newActiveInfo.element || oldActiveType !== newActiveInfo.type) {
logDebug('[debouncedDOMChangeHandler] Active input changed. Old:', {el: oldActiveElement?.tagName, type: oldActiveType}, 'New:', {el: newActiveInfo.element?.tagName, type: newActiveInfo.type});
if (oldActiveElement && oldActiveElement.dataset.keydownListenerAttached === 'true') {
oldActiveElement.removeEventListener('keydown', handleKeydown, true);
delete oldActiveElement.dataset.keydownListenerAttached;
logDebug('[debouncedDOMChangeHandler] Removed listener from old active input:', oldActiveElement);
}
activeInputInfo = newActiveInfo;
updateActiveInputListener();
} else if (newActiveInfo.element && newActiveInfo.element.dataset.keydownListenerAttached !== 'true' && isScriptGloballyEnabled) {
logDebug('[debouncedDOMChangeHandler] Same element, but listener needs re-evaluation for:', newActiveInfo.element);
updateActiveInputListener();
} else if (!newActiveInfo.element && oldActiveElement) {
logDebug('[debouncedDOMChangeHandler] Active element disappeared. Clearing activeInputInfo.');
if (oldActiveElement && oldActiveElement.dataset.keydownListenerAttached === 'true') {
oldActiveElement.removeEventListener('keydown', handleKeydown, true);
delete oldActiveElement.dataset.keydownListenerAttached;
}
activeInputInfo = {element: null, type: INPUT_TYPES.UNKNOWN};
} else {
logDebug('[debouncedDOMChangeHandler] No change in active input or listener status seems fine.');
}
if (window.location.hostname.includes('aistudio.google.com')) {
updateAIStudioButtonModifierHint();
}
}, 250);
const observeDOM = function() {
const observer = new MutationObserver(mutationObserverCallback);
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style', 'disabled', 'aria-hidden', 'contenteditable'] });
logDebug('[observeDOM] MutationObserver started.');
};
function init() {
i18n.detectLanguage(); loadSettings(); createSettingsUI(); registerMenuCommand(); observeDOM();
activeInputInfo = findActiveInputElement();
updateActiveInputListener();
if (window.location.hostname.includes('aistudio.google.com')) setTimeout(updateAIStudioButtonModifierHint, 500);
logDebug(`Initialized. Global Enable: ${isScriptGloballyEnabled}. Gemini: ${JSON.stringify(currentGeminiConfig)}. AI Studio: ${JSON.stringify(currentAIStudioConfig)}.`);
logDebug('Initial active input element:', activeInputInfo.element, 'Type:', activeInputInfo.type);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();