feat(gemini-scrubber)
This commit is contained in:
parent
bdcbd3d3aa
commit
a33b941e2a
1 changed files with 214 additions and 0 deletions
214
gemini-scrubber.js
Normal file
214
gemini-scrubber.js
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
// ==UserScript==
|
||||
// @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
|
||||
// @match https://gemini.google.com/*
|
||||
// @grant GM_addStyle
|
||||
// @run-at document-idle
|
||||
// ==/UserScript==
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const CONFIG = {
|
||||
debounceTime: 200,
|
||||
gap: '8px',
|
||||
tooltipWidth: '380px',
|
||||
hitAreaHeight: '32px',
|
||||
baseColor: 'rgba(255, 255, 255, 0.25)',
|
||||
hoverColor: '#a8c7fa',
|
||||
};
|
||||
|
||||
GM_addStyle(`
|
||||
#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; /* 容器穿透 */
|
||||
}
|
||||
|
||||
/* 1. 交互层 (热区) */
|
||||
.scrub-item {
|
||||
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;
|
||||
}
|
||||
|
||||
/* Hover 效果:变宽、变色、变高 */
|
||||
.scrub-item:hover .scrub-visual {
|
||||
background-color: ${CONFIG.hoverColor};
|
||||
width: 10px;
|
||||
height: 32px;
|
||||
opacity: 1;
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
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; }
|
||||
`);
|
||||
|
||||
let container = null;
|
||||
let debounceTimer = null;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
// 没内容时清空
|
||||
if (uNodes.length === 0) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 清理多余节点
|
||||
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];
|
||||
|
||||
const newTooltipContent = `
|
||||
<div class="tooltip-block">
|
||||
<div class="tooltip-label">User</div>
|
||||
<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) {
|
||||
// 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 = `
|
||||
<div class="scrub-tooltip">${newTooltipContent}</div>
|
||||
<div class="scrub-visual"></div>
|
||||
`;
|
||||
|
||||
item.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
uNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
});
|
||||
|
||||
container.appendChild(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(renderScrubber, CONFIG.debounceTime);
|
||||
});
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
setTimeout(renderScrubber, 1000);
|
||||
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue