您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a 📃 button that opens the menu, clicks “Export to…”, then highlights and clicks the “Export to Docs” button when it appears.
// ==UserScript== // @name Gemini Export Button // @namespace https://x.com/TakashiSasaki/tampermonkey/gemini-export // @version 0.9.3 // @description Adds a 📃 button that opens the menu, clicks “Export to…”, then highlights and clicks the “Export to Docs” button when it appears. // @author Takashi Sasaki // @homepage https://x.com/TakashiSasaki // @supportURL https://x.com/TakashiSasaki // @license MIT // @match https://gemini.google.com/app/* // @icon https://x.com/TakashiSasaki/path/to/icon.png // @compatible tampermonkey // @compatible violentmonkey // @grant none // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const BUTTON_CLASS = 'tm-cascade-highlight-click-button'; /** * Dispatches a click event on the given element. * @param {Element} el */ function simulateClick(el) { if (!el) return; el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); } /** * Waits for an element matching `selector` to appear in the DOM, * then calls `callback` with that element. * Polls every 100ms, gives up after `timeout` ms. * @param {string} selector * @param {function(Element):void} callback * @param {number} timeout */ function waitForSelector(selector, callback, timeout = 5000) { const interval = 100; let elapsed = 0; const handle = setInterval(() => { const el = document.querySelector(selector); if (el) { clearInterval(handle); callback(el); } else if ((elapsed += interval) >= timeout) { clearInterval(handle); console.warn(`waitForSelector timed out: ${selector}`); } }, interval); } /** * Handles the cascade: * 1) Open the “︙” menu * 2) Click “Export to…” * 3) Highlight and click “Export to Docs” * @param {Element} menuBtn */ function handleCascade(menuBtn) { // Step 1: open the menu simulateClick(menuBtn); // Step 2: wait for “Export to…” button and click it waitForSelector('button[data-test-id="export-button"]', exportBtn => { simulateClick(exportBtn); // Step 3: wait for “Export to Docs” button, highlight it, and click it const docsSelector = '[id^="cdk-dialog-"] actions-bottom-sheet > div > div.options.ng-star-inserted > div > button:nth-child(1)'; waitForSelector(docsSelector, docsBtn => { // Highlight the button itself docsBtn.style.setProperty('background-color', 'yellow', 'important'); docsBtn.style.setProperty('border', '2px solid red', 'important'); docsBtn.style.setProperty('outline', '2px solid orange', 'important'); // Highlight its content container const content = docsBtn.querySelector('.item-button-content'); if (content) { content.style.setProperty('background-color', 'yellow', 'important'); content.style.setProperty('border', '1px dashed orange', 'important'); } // Step 4: click the highlighted button simulateClick(docsBtn); }); }); } /** * Creates the custom 📃 button next to the existing menu button. * @param {Element} menuButtonElement * @returns {HTMLButtonElement} */ function createCustomButton(menuButtonElement) { const btn = document.createElement('button'); btn.innerText = '📃'; btn.className = BUTTON_CLASS; Object.assign(btn.style, { marginLeft: '8px', padding: '4px 8px', border: 'none', borderRadius: '4px', backgroundColor: '#e8f0fe', color: '#202124', cursor: 'pointer', fontSize: '14px' }); btn.title = 'Export cascade: menu → export → highlight & click Docs'; btn.addEventListener('click', e => { e.stopPropagation(); handleCascade(menuButtonElement); }); return btn; } /** * Injects the custom button into each response header. */ function addButtons() { document.querySelectorAll('div.menu-button-wrapper').forEach(wrapper => { if (wrapper.nextSibling?.classList?.contains(BUTTON_CLASS)) return; const menuBtn = wrapper.querySelector('button'); if (menuBtn) { const customBtn = createCustomButton(menuBtn); wrapper.parentNode.insertBefore(customBtn, wrapper.nextSibling); } }); } // Observe the page for dynamic content changes new MutationObserver(addButtons).observe(document.body, { childList: true, subtree: true, }); // Initial injection addButtons(); })();