您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Uses Ctrl+Shift+D to export a Gemini message to Google Docs, visually highlighting each interacted UI element. Handles UI variations.
当前为
// ==UserScript== // @name Save a Gemini message to Google Docs // @namespace https://x.com/TakashiSasaki/greasyfork/gemini-message-options-shortcut // @version 0.2.3 // @description Uses Ctrl+Shift+D to export a Gemini message to Google Docs, visually highlighting each interacted UI element. Handles UI variations. // @author Takashi Sasasaki // @license MIT // @homepageURL https://x.com/TakashiSasaki // @match https://gemini.google.com/app/* // @match https://gemini.google.com/app // @icon https://www.gstatic.com/lamda/images/gemini_favicon_f069958c85030456e93de685481c559f160ea06b.png // @grant none // ==/UserScript== (function() { 'use strict'; // --- Configuration --- const USE_CTRL_KEY = true; const USE_SHIFT_KEY = true; const TRIGGER_KEY_D = 'D'; const SELECTOR_MESSAGE_MENU_BUTTON = '[data-test-id="more-menu-button"]'; const SELECTOR_EXPORT_BUTTON = '[data-test-id="export-button"]'; const SELECTOR_SHARE_AND_EXPORT_BUTTON = '[data-test-id="share-and-export-menu-button"]'; const WAIT_BEFORE_CLICK_HIGHLIGHT_MS = 150; // Time to see highlight before click const WAIT_AFTER_MENU_CLICK_MS = 200; const WAIT_AFTER_EXPORT_MENU_CLICK_MS = 200; const WAIT_AFTER_SHARE_BUTTON_CLICK_MS = 200; const WAIT_AFTER_ESC_MS = 150; // Time for UI to react after Esc const POLLING_INTERVAL_MS = 50; const MAX_POLLING_TIME_MS = 3000; const SCRIPT_NAME = 'Save a Gemini message to Google Docs'; const HIGHLIGHT_STYLE = { backgroundColor: 'rgba(255, 255, 0, 0.7)', // Yellowish border: '2px solid orange', outline: '2px dashed red', zIndex: '99999', transition: 'background-color 0.1s, border 0.1s, outline 0.1s' }; const DEFAULT_STYLE_KEYS = ['backgroundColor', 'border', 'outline', 'zIndex', 'transition']; let lastHighlightedElement = null; let lastOriginalStyles = {}; function applyHighlight(element) { if (!element) return; removeCurrentHighlight(); // Remove highlight from any previously highlighted element lastOriginalStyles = {}; DEFAULT_STYLE_KEYS.forEach(key => { lastOriginalStyles[key] = element.style[key] || ''; }); Object.assign(element.style, HIGHLIGHT_STYLE); lastHighlightedElement = element; } function removeCurrentHighlight() { if (lastHighlightedElement && lastOriginalStyles) { Object.assign(lastHighlightedElement.style, lastOriginalStyles); // Forcing reflow might be needed if transition is stuck, but usually not. // lastHighlightedElement.offsetHeight; } lastHighlightedElement = null; lastOriginalStyles = {}; } // --- Utility to check basic visibility (not display:none, etc.) --- function isElementBasicallyVisible(el) { if (!el) return false; const style = window.getComputedStyle(el); if (style.display === 'none') return false; if (style.visibility === 'hidden') return false; if (style.opacity === '0') return false; return el.offsetParent !== null; } // --- Utility to check if an element is truly in the viewport --- function isElementInViewport(el) { if (!isElementBasicallyVisible(el)) { return false; } const rect = el.getBoundingClientRect(); return ( rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0 ); } // --- Utility to sleep --- function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // --- Utility to poll for an element (uses isElementInViewport, finds first in DOM that's in viewport) --- async function pollForElement(selector, maxTime, interval) { const startTime = Date.now(); while (Date.now() - startTime < maxTime) { const candidates = document.querySelectorAll(selector); for (const candidate of candidates) { if (isElementInViewport(candidate)) { return candidate; } } await sleep(interval); } console.warn(`[${SCRIPT_NAME}] pollForElement timed out for selector: ${selector}`); return null; } // --- Utility to find "Export to Docs" button (generalized for different menu types, uses isElementInViewport) --- async function findExportToDocsButton(maxTime, interval) { const startTime = Date.now(); while (Date.now() - startTime < maxTime) { const buttons = document.querySelectorAll( 'button.mat-ripple.option, button[matripple].option, button.mat-mdc-menu-item' ); for (const button of buttons) { const labelElement = button.querySelector('span.item-label, span.mat-mdc-menu-item-text'); const iconElement = button.querySelector('mat-icon[data-mat-icon-name="docs"]'); if (labelElement && labelElement.textContent.trim() === 'Export to Docs' && iconElement && isElementInViewport(button)) { return button; } } await sleep(interval); } return null; } // --- Main function to perform the export sequence --- async function performExportSequence() { removeCurrentHighlight(); // Clear any previous highlight before starting try { // Step 1 (Common): Find, highlight, and click the topmost IN VIEWPORT message menu button const allMenuButtons = document.querySelectorAll(SELECTOR_MESSAGE_MENU_BUTTON); let inViewportMenuButtons = []; for (const button of allMenuButtons) { if (isElementInViewport(button)) { inViewportMenuButtons.push(button); } } if (inViewportMenuButtons.length === 0) { console.warn(`[${SCRIPT_NAME}] Step 1: No message menu buttons (${SELECTOR_MESSAGE_MENU_BUTTON}) found in viewport.`); // alert('画面内にメッセージオプションのメニューボタンが見つかりませんでした。'); return; } let targetMenuButton; if (inViewportMenuButtons.length === 1) { targetMenuButton = inViewportMenuButtons[0]; } else { inViewportMenuButtons.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top); targetMenuButton = inViewportMenuButtons[0]; } console.log(`[${SCRIPT_NAME}] Step 1: Found topmost in-viewport message menu button. Highlighting and clicking:`, targetMenuButton); applyHighlight(targetMenuButton); await sleep(WAIT_BEFORE_CLICK_HIGHLIGHT_MS); targetMenuButton.click(); // Highlight on targetMenuButton will be cleared if we proceed and highlight another element, or if an error occurs and finally block clears it. console.log(`[${SCRIPT_NAME}] Waiting ${WAIT_AFTER_MENU_CLICK_MS}ms after clicking message menu...`); await sleep(WAIT_AFTER_MENU_CLICK_MS); // Attempt Primary Sequence: Step 2 (Poll for 'Export to...' button) console.log(`[${SCRIPT_NAME}] Attempting Primary Sequence: Polling for 'Export to...' button (${SELECTOR_EXPORT_BUTTON})`); let exportButton = await pollForElement(SELECTOR_EXPORT_BUTTON, MAX_POLLING_TIME_MS, POLLING_INTERVAL_MS); if (exportButton) { // Primary Path console.log(`[${SCRIPT_NAME}] Primary Path: Found 'Export to...' button. Highlighting and clicking:`, exportButton); applyHighlight(exportButton); await sleep(WAIT_BEFORE_CLICK_HIGHLIGHT_MS); exportButton.click(); console.log(`[${SCRIPT_NAME}] Waiting ${WAIT_AFTER_EXPORT_MENU_CLICK_MS}ms after clicking 'Export to...'...`); await sleep(WAIT_AFTER_EXPORT_MENU_CLICK_MS); console.log(`[${SCRIPT_NAME}] Primary Path: Polling for 'Export to Docs' button...`); const primaryExportToDocsButton = await findExportToDocsButton(MAX_POLLING_TIME_MS, POLLING_INTERVAL_MS); if (primaryExportToDocsButton) { console.log(`[${SCRIPT_NAME}] Primary Path: Found 'Export to Docs' button. Highlighting and clicking:`, primaryExportToDocsButton); applyHighlight(primaryExportToDocsButton); await sleep(WAIT_BEFORE_CLICK_HIGHLIGHT_MS); primaryExportToDocsButton.click(); console.log(`[${SCRIPT_NAME}] 'Export to Docs' button clicked successfully (Primary Path).`); // alert('Google Docsへのエクスポート処理を開始しました。(Primary Path)'); } else { console.warn(`[${SCRIPT_NAME}] Primary Path: 'Export to Docs' button not found after clicking 'Export to...'.`); // alert('「Export to Docs」ボタンが見つかりませんでした。(Primary Path)'); } } else { // Primary Path failed, try Alternative Path console.warn(`[${SCRIPT_NAME}] Primary Path: 'Export to...' button (${SELECTOR_EXPORT_BUTTON}) not found. Attempting Alternative Path.`); removeCurrentHighlight(); // Clear highlight from targetMenuButton console.log(`[${SCRIPT_NAME}] Emulating Escape key press to close any potentially open menu from Step 1.`); document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true, cancelable: true })); await sleep(WAIT_AFTER_ESC_MS); console.log(`[${SCRIPT_NAME}] Alternative Path: Polling for 'Share & export' button (${SELECTOR_SHARE_AND_EXPORT_BUTTON})`); const shareAndExportButton = await pollForElement(SELECTOR_SHARE_AND_EXPORT_BUTTON, MAX_POLLING_TIME_MS, POLLING_INTERVAL_MS); if (shareAndExportButton) { console.log(`[${SCRIPT_NAME}] Alternative Path: Found 'Share & export' button. Highlighting and clicking:`, shareAndExportButton); applyHighlight(shareAndExportButton); await sleep(WAIT_BEFORE_CLICK_HIGHLIGHT_MS); shareAndExportButton.click(); console.log(`[${SCRIPT_NAME}] Waiting ${WAIT_AFTER_SHARE_BUTTON_CLICK_MS}ms after clicking 'Share & export'...`); await sleep(WAIT_AFTER_SHARE_BUTTON_CLICK_MS); console.log(`[${SCRIPT_NAME}] Alternative Path: Polling for 'Export to Docs' button...`); const altExportToDocsButton = await findExportToDocsButton(MAX_POLLING_TIME_MS, POLLING_INTERVAL_MS); if (altExportToDocsButton) { console.log(`[${SCRIPT_NAME}] Alternative Path: Found 'Export to Docs' button. Highlighting and clicking:`, altExportToDocsButton); applyHighlight(altExportToDocsButton); await sleep(WAIT_BEFORE_CLICK_HIGHLIGHT_MS); altExportToDocsButton.click(); console.log(`[${SCRIPT_NAME}] 'Export to Docs' button clicked successfully (Alternative Path).`); // alert('Google Docsへのエクスポート処理を開始しました。(Alternative Path)'); } else { console.warn(`[${SCRIPT_NAME}] Alternative Path: 'Export to Docs' button not found after clicking 'Share & export'.`); // alert('「Export to Docs」ボタンが見つかりませんでした。(Alternative Path)'); } } else { console.error(`[${SCRIPT_NAME}] Alternative Path: 'Share & export' button (${SELECTOR_SHARE_AND_EXPORT_BUTTON}) also not found. Cannot proceed.`); // alert('「Share & export」ボタンも見つからず、処理を続行できません。'); } } } catch (error) { console.error(`[${SCRIPT_NAME}] An error occurred during the export sequence:`, error); // alert('エクスポート処理中にエラーが発生しました。コンソールを確認してください。'); } finally { // Ensure any active highlight is cleared when the sequence completes or errors out. removeCurrentHighlight(); } } // --- Keyboard shortcut listener --- document.addEventListener('keydown', function(event) { if (event.ctrlKey === USE_CTRL_KEY && event.shiftKey === USE_SHIFT_KEY && (event.key.toUpperCase() === TRIGGER_KEY_D) ) { event.preventDefault(); event.stopPropagation(); console.log(`[${SCRIPT_NAME}] Ctrl+Shift+D pressed.`); performExportSequence(); } }, true); // Log script load and version if (typeof GM_info !== 'undefined' && GM_info.script) { console.log(`[${SCRIPT_NAME}] v${GM_info.script.version} loaded and active.`); } else { console.log(`[${SCRIPT_NAME}] loaded and active. (GM_info not available)`); } })();