diff --git a/gemini-scrubber.js b/gemini-scrubber.js index 263f7be..8e0696b 100644 --- a/gemini-scrubber.js +++ b/gemini-scrubber.js @@ -1,133 +1,95 @@ // ==UserScript== -// @name Gemini Minimal Scrubber +// @name Gemini Minimal Scrubber // @namespace https://git.js0ny.net -// @version 1.6 -// @description Sidebar navigation with text preview. No scroll-spy, just click-to-jump. -// @author js0ny +// @version 1.8.1 +// @description Sidebar navigation with text preview. (Trusted Types Compliant) +// @author js0ny // @match https://gemini.google.com/* -// @grant GM_addStyle +// @grant none // @run-at document-idle // ==/UserScript== (function() { 'use strict'; + const CONFIG = { - debounceTime: 200, + debounceTime: 500, gap: '8px', tooltipWidth: '380px', hitAreaHeight: '32px', - baseColor: 'rgba(255, 255, 255, 0.25)', + baseColor: 'rgba(255, 255, 255, 0.25)', // 浅色模式请改 rgba(0,0,0,0.2) hoverColor: '#a8c7fa', }; - GM_addStyle(` + // 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; /* 容器穿透 */ + 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; } - - /* 1. 交互层 (热区) */ .scrub-item { - position: relative; - height: ${CONFIG.hitAreaHeight}; - width: 32px; /* 稍微加宽热区 */ - display: flex; - align-items: center; - justify-content: flex-end; - cursor: pointer; - pointer-events: auto; /* 恢复点击 */ + position: relative; height: ${CONFIG.hitAreaHeight}; width: 32px; + display: flex; align-items: center; justify-content: flex-end; + cursor: pointer; pointer-events: auto; } - - /* 2. 视觉层 (胶囊) */ .scrub-visual { - width: 6px; - height: 24px; - background-color: ${CONFIG.baseColor}; - border-radius: 99px; - transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); - box-shadow: 0 2px 5px rgba(0,0,0,0.2); - opacity: 0.6; + 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); } - - /* Hover 效果:变宽、变色、变高 */ .scrub-item:hover .scrub-visual { - background-color: ${CONFIG.hoverColor}; - width: 10px; - height: 32px; - opacity: 1; + background-color: ${CONFIG.hoverColor}; width: 10px; height: 32px; box-shadow: 0 0 10px ${CONFIG.hoverColor}; } - - /* 3. 预览框 (Tooltip) */ .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; - box-shadow: 0 8px 32px rgba(0,0,0,0.6); - opacity: 0; - visibility: hidden; - pointer-events: none; - transition: all 0.2s ease-out; - - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - font-family: "Google Sans", system-ui, sans-serif; - font-size: 14px; - line-height: 1.6; - color: #e3e3e3; - z-index: 100000; + 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); + 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; - letter-spacing: 0.5px; - margin-bottom: 4px; - text-transform: uppercase; - } - .tooltip-text { - word-wrap: break-word; - overflow: hidden; - display: -webkit-box; - -webkit-line-clamp: 6; - -webkit-box-orient: vertical; - } + .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 ""; - const p = element.querySelector("p"); - // 优先取 p,否则取纯文本,替换多余空白 - const raw = (p && p.textContent.trim()) ? p.textContent : element.textContent; - return raw.replace(/\s+/g, ' ').trim(); + 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() { @@ -140,12 +102,6 @@ const uNodes = document.querySelectorAll("user-query-content"); const rNodes = document.querySelectorAll("model-response"); - // 没内容时清空 - if (uNodes.length === 0) { - container.innerHTML = ''; - return; - } - // 清理多余节点 while (container.children.length > uNodes.length) { container.removeChild(container.lastChild); @@ -158,40 +114,19 @@ let item = container.children[idx]; - const newTooltipContent = ` -
-
User
-
${uText}
-
- ${rText ? ` -
-
Gemini
-
${rText}
-
` : ''} - `; + // 创建新的 Scrub Item + if (!item) { + item = createEl('div', 'scrub-item'); + + // 1. Tooltip 容器 + const tooltip = createEl('div', 'scrub-tooltip'); + item.appendChild(tooltip); - if (item) { - // diff: update only on change - const currentU = item.dataset.uText; - const currentR = item.dataset.rText; - - if (currentU !== uText || currentR !== rText) { - const tooltip = item.querySelector('.scrub-tooltip'); - if (tooltip) tooltip.innerHTML = newTooltipContent; - item.dataset.uText = uText; - item.dataset.rText = rText; - } - } else { - item = document.createElement('div'); - item.className = 'scrub-item'; - item.dataset.uText = uText; - item.dataset.rText = rText; - - item.innerHTML = ` -
${newTooltipContent}
-
- `; + // 2. 视觉胶囊 + const visual = createEl('div', 'scrub-visual'); + item.appendChild(visual); + // 点击事件 item.addEventListener('click', (e) => { e.stopPropagation(); uNode.scrollIntoView({ behavior: 'smooth', block: 'center' }); @@ -199,6 +134,23 @@ 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; + } }); } @@ -208,7 +160,7 @@ }); observer.observe(document.body, { childList: true, subtree: true }); - + + // 启动 setTimeout(renderScrubber, 1000); - })();