fix(gemini-scrubber): fix untrust on chromium
This commit is contained in:
parent
a33b941e2a
commit
5f233f3a4b
1 changed files with 84 additions and 132 deletions
|
|
@ -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);
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue