-
Notifications
You must be signed in to change notification settings - Fork 9
Open
Labels
enhancementNew feature or requestNew feature or request
Description
目前的话,对于网页端编程的用户来说,gemini有2个很大的缺点,如果是claude网页端的话,它的那个生成分文件也好,文件列表都是很好的排列,而gemini只有右上角点击才能打开文件列表 排列顺序还不一致, 应该按照时间顺序排列,这样更新了哪些几个编程的分文件,直接在上面按时间顺序一目了然的打开。 而且悬浮就能打开那个文件列表,更方便了
一键加载全部内容的功能就是 对话非常长的时候,网页是只能滑动最上面才能加载一部分,然后继续滑,继续加载,一直到全部加载,这个逻辑的话非常影响那个文件列表,必须每次加载全面的对话,文件列表才能显示全面, 这个功能你这个按顶部也只是加载一部分
我目前写了针对这2个问题的油猴,希望你直接加上
// ==UserScript==
// @name gemini优化
// @namespace http://tampermonkey.net/
// @version 2.1
// @description 优化 Gemini 文件列表体验:悬停自动打开(白底美化),按时间倒序。屏幕右侧新增独立悬浮按钮(支持拖拽+记忆位置+开关设置),智能侦测滚动区域,支持“静默加载全部历史记录”,加载过程显示遮罩,完成后无缝归位。
// @author You
// @match https://gemini.google.com/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @run-at document-idle
// @noframes
// ==/UserScript==
(function() {
'use strict';
// 配置键名
const CONFIG_KEY_FLOAT_BTN = 'gemini_float_btn_enabled';
const CONFIG_KEY_BTN_POS = 'gemini_optimizer_btn_pos';
// =========================================================================
// 1. CSS 样式注入
// =========================================================================
GM_addStyle(`
/* 侧边栏样式 */
context-sidebar {
position: fixed !important;
top: 64px !important;
right: 80px !important;
height: auto !important;
max-height: 80vh !important;
width: 340px !important;
z-index: 10000 !important;
background-color: #ffffff !important;
border: 1px solid #e0e0e0 !important;
border-radius: 16px !important;
box-shadow: 0 10px 30px rgba(0,0,0,0.15) !important;
display: none;
overflow: hidden !important;
transition: opacity 0.2s ease-in-out;
}
context-sidebar:hover { display: block !important; opacity: 1 !important; }
context-sidebar .studio-sidebar-container { height: auto !important; max-height: 75vh !important; overflow-y: auto !important; padding: 8px 0 !important; }
context-sidebar .header button[aria-label="关闭边栏"] { display: none !important; }
context-sidebar .gds-title-l, context-sidebar .immersive-title, context-sidebar .sources-list-row-title { color: #1f1f1f !important; font-weight: 600 !important; }
context-sidebar .immersive-subtitle, context-sidebar .sources-list-row-attribution { color: #5e5e5e !important; }
context-sidebar mat-icon { color: #444746 !important; }
context-sidebar sidebar-immersive-chip .container:hover, context-sidebar .clickable:hover { background-color: #f5f5f5 !important; border-radius: 8px; }
button[data-test-id="studio-sidebar-button"].clicked { background-color: transparent !important; box-shadow: none !important; }
button[data-test-id="studio-sidebar-button"].clicked .mat-mdc-button-persistent-ripple { opacity: 0 !important; display: none !important; }
button[data-test-id="studio-sidebar-button"].clicked mat-icon { color: inherit !important; }
/* Toast & Loading Styles */
#gemini-load-toast {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
background: rgba(30, 31, 32, 0.95);
color: white;
padding: 10px 20px;
border-radius: 24px;
z-index: 20001;
font-size: 14px;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
text-align: center;
font-weight: 500;
}
#gemini-load-toast.show { opacity: 1; }
#gemini-float-load-btn {
position: fixed;
width: 44px;
height: 44px;
border-radius: 50%;
background-color: white;
border: 1px solid #e0e0e0;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
z-index: 20000;
cursor: grab;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.6;
transition: opacity 0.2s, transform 0.1s, box-shadow 0.2s;
touch-action: none;
user-select: none;
}
#gemini-float-load-btn:active {
cursor: grabbing;
transform: scale(0.95);
}
#gemini-float-load-btn:hover {
opacity: 1;
background-color: #f8f9fa;
border-color: #dadce0;
box-shadow: 0 6px 16px rgba(0,0,0,0.15);
}
#gemini-float-load-btn mat-icon { color: #5f6368; font-size: 24px; width: 24px; height: 24px; pointer-events: none; }
@keyframes spin-anim { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.spin-anim { animation: spin-anim 1s linear infinite; }
`);
// =========================================================================
// 2. 工具函数
// =========================================================================
function parseChineseTime(timeStr) {
if (!timeStr) return 0;
try {
const year = new Date().getFullYear();
let cleanStr = timeStr.replace(/月/g, '/').replace(/,/g, '').replace(/下午/g, 'PM').replace(/上午/g, 'AM').trim();
const timestamp = Date.parse(`${year}/${cleanStr}`);
return isNaN(timestamp) ? 0 : timestamp;
} catch (e) { return 0; }
}
function showToast(text, duration = 0) {
let toast = document.getElementById('gemini-load-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'gemini-load-toast';
document.body.appendChild(toast);
}
toast.textContent = text;
toast.classList.add('show');
if (duration > 0) {
setTimeout(() => { toast.classList.remove('show'); }, duration);
}
}
// =========================================================================
// 3. 核心算法:智能侦测滚动容器
// =========================================================================
function findChatScroller() {
const candidates = document.querySelectorAll('main, infinite-scroller, .scrollable-content, .message-content');
for (const el of candidates) {
if (el.scrollHeight > el.clientHeight && el.clientHeight > 100) {
return el;
}
}
const allDivs = document.querySelectorAll('div');
let maxArea = 0;
let bestTarget = null;
for (const el of allDivs) {
const style = window.getComputedStyle(el);
const isScrollable = (style.overflowY === 'auto' || style.overflowY === 'scroll') || (el.scrollHeight > el.clientHeight + 50);
if (isScrollable) {
const rect = el.getBoundingClientRect();
if (rect.height > 200 && rect.width > 300) {
const area = rect.width * rect.height;
if (area > maxArea) {
maxArea = area;
bestTarget = el;
}
}
}
}
return bestTarget || document.scrollingElement || document.body;
}
// =========================================================================
// 4. 核心功能 A:文件列表
// =========================================================================
let hideTimer = null;
let isSorting = false;
function sortFiles(containerContext) {
if (isSorting) return;
const root = containerContext || document.querySelector('context-sidebar');
if (!root) return;
const listContainer = root.querySelector('.source-container');
if (!listContainer) return;
const items = Array.from(listContainer.querySelectorAll('sidebar-immersive-chip'));
if (items.length <= 1) return;
isSorting = true;
items.sort((a, b) => {
const timeA = parseChineseTime(a.querySelector('.immersive-subtitle')?.textContent?.trim());
const timeB = parseChineseTime(b.querySelector('.immersive-subtitle')?.textContent?.trim());
return timeB - timeA;
});
const fragment = document.createDocumentFragment();
items.forEach(item => fragment.appendChild(item));
listContainer.appendChild(fragment);
setTimeout(() => { isSorting = false; }, 50);
}
function showSidebar() {
const sidebar = document.querySelector('context-sidebar');
const button = document.querySelector('button[data-test-id="studio-sidebar-button"]');
if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
if (sidebar) {
sidebar.style.display = 'block';
sortFiles(sidebar);
bindSidebarEvents(sidebar);
} else if (button) {
button.click();
let checkCount = 0;
const waitSidebar = setInterval(() => {
const newSidebar = document.querySelector('context-sidebar');
checkCount++;
if (newSidebar) {
clearInterval(waitSidebar);
newSidebar.style.display = 'block';
bindSidebarEvents(newSidebar);
sortFiles(newSidebar);
} else if (checkCount > 20) clearInterval(waitSidebar);
}, 100);
}
}
function hideSidebarDelayed() {
hideTimer = setTimeout(() => {
const sidebar = document.querySelector('context-sidebar');
if (sidebar && !sidebar.matches(':hover')) {
sidebar.style.display = 'none';
}
}, 300);
}
function bindSidebarEvents(sidebar) {
if (sidebar.dataset.geminiEventsBound) return;
sidebar.addEventListener('mouseenter', () => { if (hideTimer) clearTimeout(hideTimer); });
sidebar.addEventListener('mouseleave', hideSidebarDelayed);
const observer = new MutationObserver(() => { if (!isSorting) setTimeout(() => sortFiles(sidebar), 200); });
observer.observe(sidebar, { childList: true, subtree: true });
sidebar.dataset.geminiEventsBound = 'true';
}
// =========================================================================
// 5. 核心功能 B:静默加载历史
// =========================================================================
window._geminiStopLoading = false;
function createLoadingOverlay() {
if (document.getElementById('gemini-loading-overlay')) return;
const overlay = document.createElement('div');
overlay.id = 'gemini-loading-overlay';
Object.assign(overlay.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100vw',
height: '100vh',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
zIndex: '99999',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
color: '#333'
});
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
overlay.style.backgroundColor = 'rgba(30, 31, 32, 0.95)';
overlay.style.color = '#fff';
}
const icon = document.createElement('mat-icon');
icon.className = 'mat-icon notranslate google-symbols mat-ligature-font spin-anim';
icon.style.cssText = 'font-size: 48px; width: 48px; height: 48px; margin-bottom: 20px; color: #1a73e8;';
icon.textContent = 'sync';
const text = document.createElement('div');
text.id = 'gemini-loading-text';
text.style.cssText = 'font-size: 18px; font-weight: 500; margin-bottom: 8px;';
text.textContent = '正在挖掘历史记录...';
const subtext = document.createElement('div');
subtext.style.cssText = 'font-size: 14px; opacity: 0.7; margin-bottom: 30px;';
subtext.textContent = '保持页面静止,完成后将自动归位';
const stopBtn = document.createElement('button');
stopBtn.textContent = '停止加载';
stopBtn.style.cssText = 'padding: 8px 24px; border-radius: 18px; border: 1px solid rgba(128,128,128,0.3); background: transparent; color: inherit; cursor: pointer; font-size: 14px;';
stopBtn.onmouseover = () => stopBtn.style.background = 'rgba(128,128,128,0.1)';
stopBtn.onmouseout = () => stopBtn.style.background = 'transparent';
stopBtn.onclick = () => { window._geminiStopLoading = true; };
overlay.appendChild(icon);
overlay.appendChild(text);
overlay.appendChild(subtext);
overlay.appendChild(stopBtn);
document.body.appendChild(overlay);
}
function removeLoadingOverlay() {
const overlay = document.getElementById('gemini-loading-overlay');
if (overlay) overlay.remove();
}
function updateLoadingText(str) {
const el = document.getElementById('gemini-loading-text');
if (el) el.textContent = str;
}
function loadAllHistory() {
const scrollContainer = findChatScroller();
if (!scrollContainer || scrollContainer === document.body) {
showToast('⚠️ 未找到聊天滚动区域,请先滚动一下页面再试', 4000);
return;
}
console.log('Gemini优化: 已定位滚动容器', scrollContainer);
showToast(`🎯 已定位滚动区域 (${scrollContainer.tagName})`, 1000);
const startScrollHeight = scrollContainer.scrollHeight;
const startScrollTop = scrollContainer.scrollTop;
createLoadingOverlay();
window._geminiStopLoading = false;
let lastHeight = scrollContainer.scrollHeight;
let noChangeCount = 0;
const maxRetries = 5;
const loadLoop = () => {
if (window._geminiStopLoading) {
removeLoadingOverlay();
showToast('已停止加载');
const currentHeight = scrollContainer.scrollHeight;
scrollContainer.scrollTop = currentHeight - startScrollHeight + startScrollTop;
return;
}
scrollContainer.scrollTop = 0;
scrollContainer.dispatchEvent(new WheelEvent('wheel', { deltaY: -100, bubbles: true }));
setTimeout(() => {
const currentHeight = scrollContainer.scrollHeight;
if (currentHeight > lastHeight) {
lastHeight = currentHeight;
noChangeCount = 0;
updateLoadingText(`已加载... (当前高度: ${currentHeight}px)`);
loadLoop();
} else {
noChangeCount++;
updateLoadingText(`正在触底确认... (${noChangeCount}/${maxRetries})`);
if (noChangeCount >= maxRetries) {
removeLoadingOverlay();
showToast('✅ 历史记录加载完毕!', 3000);
const addedHeight = currentHeight - startScrollHeight;
scrollContainer.scrollTop = startScrollTop + addedHeight;
} else {
loadLoop();
}
}
}, 1200);
};
loadLoop();
}
// =========================================================================
// 6. 悬浮按钮 (拖拽 + 记忆 + 开关)
// =========================================================================
function initFloatingButton() {
// 1. 检查开关状态
const isEnabled = GM_getValue(CONFIG_KEY_FLOAT_BTN, true);
const btn = document.getElementById('gemini-float-load-btn');
// 如果功能被禁用
if (!isEnabled) {
if (btn) btn.remove(); // 如果存在则移除
return;
}
// 如果已存在且功能开启,则不重复创建
if (btn) return;
// 2. 创建按钮
const newBtn = document.createElement('div');
newBtn.id = 'gemini-float-load-btn';
newBtn.title = '自动加载全部历史对话 (可拖拽)';
const icon = document.createElement('mat-icon');
icon.setAttribute('role', 'img');
icon.className = 'mat-icon notranslate google-symbols mat-ligature-font';
icon.setAttribute('aria-hidden', 'true');
icon.textContent = 'vertical_align_top';
newBtn.appendChild(icon);
document.body.appendChild(newBtn);
// 3. 读取位置
const savedPos = localStorage.getItem(CONFIG_KEY_BTN_POS);
if (savedPos) {
try {
const pos = JSON.parse(savedPos);
const safeLeft = Math.min(Math.max(0, pos.left), window.innerWidth - 50);
const safeTop = Math.min(Math.max(0, pos.top), window.innerHeight - 50);
newBtn.style.left = safeLeft + 'px';
newBtn.style.top = safeTop + 'px';
newBtn.style.right = 'auto';
} catch(e) {
newBtn.style.top = '120px';
newBtn.style.right = '24px';
}
} else {
newBtn.style.top = '120px';
newBtn.style.right = '24px';
}
// 4. 拖拽逻辑
let isDragging = false;
let hasMoved = false;
let startX, startY, initialLeft, initialTop;
newBtn.addEventListener('mousedown', (e) => {
isDragging = true;
hasMoved = false;
startX = e.clientX;
startY = e.clientY;
const rect = newBtn.getBoundingClientRect();
initialLeft = rect.left;
initialTop = rect.top;
newBtn.style.right = 'auto';
newBtn.style.left = initialLeft + 'px';
newBtn.style.top = initialTop + 'px';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) hasMoved = true;
newBtn.style.left = (initialLeft + dx) + 'px';
newBtn.style.top = (initialTop + dy) + 'px';
});
document.addEventListener('mouseup', () => {
if (!isDragging) return;
isDragging = false;
if (hasMoved) {
const rect = newBtn.getBoundingClientRect();
localStorage.setItem(CONFIG_KEY_BTN_POS, JSON.stringify({ left: rect.left, top: rect.top }));
}
});
newBtn.addEventListener('click', (e) => {
if (!hasMoved) loadAllHistory();
});
}
// =========================================================================
// 7. 菜单命令 (Menu Command)
// =========================================================================
let menuCmdId = null;
function updateMenuCommand() {
// 如果之前注册过,先移除(防止重复)
if (menuCmdId !== null) {
GM_unregisterMenuCommand(menuCmdId);
}
const isEnabled = GM_getValue(CONFIG_KEY_FLOAT_BTN, true);
// 根据当前状态显示相反的操作
const menuText = isEnabled ? "🚫 隐藏悬浮加载按钮" : "🔘 显示悬浮加载按钮";
menuCmdId = GM_registerMenuCommand(menuText, () => {
const newState = !isEnabled;
GM_setValue(CONFIG_KEY_FLOAT_BTN, newState);
if (newState) {
initFloatingButton();
showToast('✅ 悬浮按钮已开启');
} else {
const btn = document.getElementById('gemini-float-load-btn');
if (btn) btn.remove();
showToast('🚫 悬浮按钮已隐藏');
}
// 点击后更新菜单文字
updateMenuCommand();
});
}
// =========================================================================
// 8. 初始化
// =========================================================================
function init() {
console.log('Gemini优化: V2.1 菜单设置版启动');
// 注册菜单
updateMenuCommand();
// 尝试初始化按钮 (内部会检查开关状态)
initFloatingButton();
setInterval(() => {
// 侧边栏逻辑
const button = document.querySelector('button[data-test-id="studio-sidebar-button"]');
if (button && !button.dataset.geminiOptimized) {
button.dataset.geminiOptimized = 'true';
button.addEventListener('mouseenter', showSidebar);
button.addEventListener('mouseleave', hideSidebarDelayed);
}
const sidebar = document.querySelector('context-sidebar');
if (sidebar) bindSidebarEvents(sidebar);
// 按钮逻辑:持续检查 (如果被删除且配置为开启,则重建;如果配置为关闭且存在,则移除)
const isEnabled = GM_getValue(CONFIG_KEY_FLOAT_BTN, true);
const btn = document.getElementById('gemini-float-load-btn');
if (isEnabled && !btn) {
initFloatingButton();
} else if (!isEnabled && btn) {
btn.remove();
}
}, 1000);
}
init();
})();
urzeye
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request