userscripts/gemini-scrubber.js

167 lines
6.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ==UserScript==
// @name Gemini Minimal Scrubber
// @namespace https://git.js0ny.net
// @version 1.8.1
// @description Sidebar navigation with text preview. (Trusted Types Compliant)
// @author js0ny
// @match https://gemini.google.com/*
// @grant none
// @run-at document-idle
// @icon https://www.google.com/s2/favicons?sz=64&domain=gemini.google.com
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
debounceTime: 500,
gap: '8px',
tooltipWidth: '380px',
hitAreaHeight: '32px',
baseColor: 'rgba(255, 255, 255, 0.25)', // 浅色模式请改 rgba(0,0,0,0.2)
hoverColor: '#a8c7fa',
};
// 1. 注入 CSS (使用标准 API避开 CSP 拦截)
const style = document.createElement('style');
style.textContent = `
#gemini-scrubber {
position: fixed; right: 16px; top: 50%; transform: translateY(-50%);
display: flex; flex-direction: column; gap: ${CONFIG.gap};
z-index: 99999; padding: 10px 8px; pointer-events: none;
}
.scrub-item {
position: relative; height: ${CONFIG.hitAreaHeight}; width: 32px;
display: flex; align-items: center; justify-content: flex-end;
cursor: pointer; pointer-events: auto;
}
.scrub-visual {
width: 6px; height: 24px; background-color: ${CONFIG.baseColor};
border-radius: 99px; transition: all 0.2s; box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.scrub-item:hover .scrub-visual {
background-color: ${CONFIG.hoverColor}; width: 10px; height: 32px;
box-shadow: 0 0 10px ${CONFIG.hoverColor};
}
.scrub-tooltip {
position: absolute; right: 36px; top: 50%; transform: translateY(-50%) translateX(10px);
width: ${CONFIG.tooltipWidth}; background-color: #1e1e1e; border: 1px solid #444;
border-radius: 12px; padding: 16px; opacity: 0; visibility: hidden;
pointer-events: none; transition: all 0.2s; color: #e3e3e3; font-size: 14px;
font-family: sans-serif; z-index: 100000;
}
.scrub-item:hover .scrub-tooltip {
opacity: 1; visibility: visible; transform: translateY(-50%) translateX(0);
}
.tooltip-block { margin-bottom: 12px; }
.tooltip-block:last-child { margin-bottom: 0; }
.tooltip-label { font-size: 11px; color: #8e918f; font-weight: 700; text-transform: uppercase; margin-bottom: 4px; }
.tooltip-text { display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; overflow: hidden; word-wrap: break-word; }
.tooltip-text.user { color: #d2e3fc; }
`;
document.head.appendChild(style);
let container = null;
let debounceTimer = null;
// 辅助函数:安全创建元素 (代替 innerHTML)
function createEl(tag, className, text) {
const el = document.createElement(tag);
if (className) el.className = className;
if (text) el.textContent = text;
return el;
}
function getCleanText(element) {
if (!element) return "";
try {
const p = element.querySelector("p");
const raw = (p && p.textContent) ? p.textContent : (element.textContent || "");
return raw.replace(/\s+/g, ' ').trim();
} catch (e) {
return "";
}
}
function createTooltipBlock(labelStr, textStr, isUser) {
const block = createEl('div', 'tooltip-block');
const label = createEl('div', 'tooltip-label', labelStr);
const text = createEl('div', isUser ? 'tooltip-text user' : 'tooltip-text model', textStr);
block.appendChild(label);
block.appendChild(text);
return block;
}
function renderScrubber() {
if (!container) {
container = document.createElement('div');
container.id = 'gemini-scrubber';
document.body.appendChild(container);
}
const uNodes = document.querySelectorAll("user-query-content");
const rNodes = document.querySelectorAll("model-response");
// 清理多余节点
while (container.children.length > uNodes.length) {
container.removeChild(container.lastChild);
}
uNodes.forEach((uNode, idx) => {
const rNode = rNodes[idx];
const uText = getCleanText(uNode);
const rText = rNode ? getCleanText(rNode) : "...";
let item = container.children[idx];
// 创建新的 Scrub Item
if (!item) {
item = createEl('div', 'scrub-item');
// 1. Tooltip 容器
const tooltip = createEl('div', 'scrub-tooltip');
item.appendChild(tooltip);
// 2. 视觉胶囊
const visual = createEl('div', 'scrub-visual');
item.appendChild(visual);
// 点击事件
item.addEventListener('click', (e) => {
e.stopPropagation();
uNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
container.appendChild(item);
}
// 检查是否需要更新 Tooltip (减少 DOM 操作)
if (item.dataset.uText !== uText || item.dataset.rText !== rText) {
const tooltip = item.querySelector('.scrub-tooltip');
tooltip.textContent = ''; // 安全清空
// 安全构建 User 部分
tooltip.appendChild(createTooltipBlock('User', uText, true));
// 安全构建 Gemini 部分
if (rText) {
tooltip.appendChild(createTooltipBlock('Gemini', rText, false));
}
item.dataset.uText = uText;
item.dataset.rText = rText;
}
});
}
const observer = new MutationObserver(() => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(renderScrubber, CONFIG.debounceTime);
});
observer.observe(document.body, { childList: true, subtree: true });
// 启动
setTimeout(renderScrubber, 1000);
})();