fix(gemini-scrubber): fix untrust on chromium

This commit is contained in:
js0ny 2025-12-08 07:40:27 +00:00
parent a33b941e2a
commit 5f233f3a4b

View file

@ -1,133 +1,95 @@
// ==UserScript== // ==UserScript==
// @name Gemini Minimal Scrubber // @name Gemini Minimal Scrubber
// @namespace https://git.js0ny.net // @namespace https://git.js0ny.net
// @version 1.6 // @version 1.8.1
// @description Sidebar navigation with text preview. No scroll-spy, just click-to-jump. // @description Sidebar navigation with text preview. (Trusted Types Compliant)
// @author js0ny // @author js0ny
// @match https://gemini.google.com/* // @match https://gemini.google.com/*
// @grant GM_addStyle // @grant none
// @run-at document-idle // @run-at document-idle
// ==/UserScript== // ==/UserScript==
(function() { (function() {
'use strict'; 'use strict';
const CONFIG = { const CONFIG = {
debounceTime: 200, debounceTime: 500,
gap: '8px', gap: '8px',
tooltipWidth: '380px', tooltipWidth: '380px',
hitAreaHeight: '32px', hitAreaHeight: '32px',
baseColor: 'rgba(255, 255, 255, 0.25)', baseColor: 'rgba(255, 255, 255, 0.25)', // 浅色模式请改 rgba(0,0,0,0.2)
hoverColor: '#a8c7fa', hoverColor: '#a8c7fa',
}; };
GM_addStyle(` // 1. 注入 CSS (使用标准 API避开 CSP 拦截)
const style = document.createElement('style');
style.textContent = `
#gemini-scrubber { #gemini-scrubber {
position: fixed; position: fixed; right: 16px; top: 50%; transform: translateY(-50%);
right: 16px; display: flex; flex-direction: column; gap: ${CONFIG.gap};
top: 50%; z-index: 99999; padding: 10px 8px; pointer-events: none;
transform: translateY(-50%);
display: flex;
flex-direction: column;
gap: ${CONFIG.gap};
z-index: 99999;
padding: 10px 8px;
pointer-events: none; /* 容器穿透 */
} }
/* 1. 交互层 (热区) */
.scrub-item { .scrub-item {
position: relative; position: relative; height: ${CONFIG.hitAreaHeight}; width: 32px;
height: ${CONFIG.hitAreaHeight}; display: flex; align-items: center; justify-content: flex-end;
width: 32px; /* 稍微加宽热区 */ cursor: pointer; pointer-events: auto;
display: flex;
align-items: center;
justify-content: flex-end;
cursor: pointer;
pointer-events: auto; /* 恢复点击 */
} }
/* 2. 视觉层 (胶囊) */
.scrub-visual { .scrub-visual {
width: 6px; width: 6px; height: 24px; background-color: ${CONFIG.baseColor};
height: 24px; border-radius: 99px; transition: all 0.2s; box-shadow: 0 2px 5px rgba(0,0,0,0.2);
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;
} }
/* Hover 效果:变宽、变色、变高 */
.scrub-item:hover .scrub-visual { .scrub-item:hover .scrub-visual {
background-color: ${CONFIG.hoverColor}; background-color: ${CONFIG.hoverColor}; width: 10px; height: 32px;
width: 10px;
height: 32px;
opacity: 1;
box-shadow: 0 0 10px ${CONFIG.hoverColor}; box-shadow: 0 0 10px ${CONFIG.hoverColor};
} }
/* 3. 预览框 (Tooltip) */
.scrub-tooltip { .scrub-tooltip {
position: absolute; position: absolute; right: 36px; top: 50%; transform: translateY(-50%) translateX(10px);
right: 36px; width: ${CONFIG.tooltipWidth}; background-color: #1e1e1e; border: 1px solid #444;
top: 50%; border-radius: 12px; padding: 16px; opacity: 0; visibility: hidden;
transform: translateY(-50%) translateX(10px); pointer-events: none; transition: all 0.2s; color: #e3e3e3; font-size: 14px;
width: ${CONFIG.tooltipWidth}; font-family: sans-serif; z-index: 100000;
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;
} }
.scrub-item:hover .scrub-tooltip { .scrub-item:hover .scrub-tooltip {
opacity: 1; opacity: 1; visibility: visible; transform: translateY(-50%) translateX(0);
visibility: visible;
transform: translateY(-50%) translateX(0);
} }
.tooltip-block { margin-bottom: 12px; } .tooltip-block { margin-bottom: 12px; }
.tooltip-block:last-child { margin-bottom: 0; } .tooltip-block:last-child { margin-bottom: 0; }
.tooltip-label { .tooltip-label { font-size: 11px; color: #8e918f; font-weight: 700; text-transform: uppercase; margin-bottom: 4px; }
font-size: 11px; .tooltip-text { display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; overflow: hidden; word-wrap: break-word; }
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-text.user { color: #d2e3fc; } .tooltip-text.user { color: #d2e3fc; }
`); `;
document.head.appendChild(style);
let container = null; let container = null;
let debounceTimer = 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) { function getCleanText(element) {
if (!element) return ""; if (!element) return "";
const p = element.querySelector("p"); try {
// 优先取 p否则取纯文本替换多余空白 const p = element.querySelector("p");
const raw = (p && p.textContent.trim()) ? p.textContent : element.textContent; const raw = (p && p.textContent) ? p.textContent : (element.textContent || "");
return raw.replace(/\s+/g, ' ').trim(); 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() { function renderScrubber() {
@ -140,12 +102,6 @@
const uNodes = document.querySelectorAll("user-query-content"); const uNodes = document.querySelectorAll("user-query-content");
const rNodes = document.querySelectorAll("model-response"); const rNodes = document.querySelectorAll("model-response");
// 没内容时清空
if (uNodes.length === 0) {
container.innerHTML = '';
return;
}
// 清理多余节点 // 清理多余节点
while (container.children.length > uNodes.length) { while (container.children.length > uNodes.length) {
container.removeChild(container.lastChild); container.removeChild(container.lastChild);
@ -158,40 +114,19 @@
let item = container.children[idx]; let item = container.children[idx];
const newTooltipContent = ` // 创建新的 Scrub Item
<div class="tooltip-block"> if (!item) {
<div class="tooltip-label">User</div> item = createEl('div', 'scrub-item');
<div class="tooltip-text user">${uText}</div>
</div>
${rText ? `
<div class="tooltip-block">
<div class="tooltip-label">Gemini</div>
<div class="tooltip-text model">${rText}</div>
</div>` : ''}
`;
if (item) { // 1. Tooltip 容器
// diff: update only on change const tooltip = createEl('div', 'scrub-tooltip');
const currentU = item.dataset.uText; item.appendChild(tooltip);
const currentR = item.dataset.rText;
if (currentU !== uText || currentR !== rText) { // 2. 视觉胶囊
const tooltip = item.querySelector('.scrub-tooltip'); const visual = createEl('div', 'scrub-visual');
if (tooltip) tooltip.innerHTML = newTooltipContent; item.appendChild(visual);
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 = `
<div class="scrub-tooltip">${newTooltipContent}</div>
<div class="scrub-visual"></div>
`;
// 点击事件
item.addEventListener('click', (e) => { item.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
uNode.scrollIntoView({ behavior: 'smooth', block: 'center' }); uNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
@ -199,6 +134,23 @@
container.appendChild(item); 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;
}
}); });
} }
@ -209,6 +161,6 @@
observer.observe(document.body, { childList: true, subtree: true }); observer.observe(document.body, { childList: true, subtree: true });
// 启动
setTimeout(renderScrubber, 1000); setTimeout(renderScrubber, 1000);
})(); })();