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();
})();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址