您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
image preview for Danbooru
// ==UserScript== // @name Danbooru Hover Preview // @namespace http://tampermonkey.net/ // @version 1.6 // @description image preview for Danbooru // @author Claude 3.5 Sonnet & GPT-4o // @match https://danbooru.donmai.us/posts* // @match https://danbooru.donmai.us/ // @grant GM_getValue // @grant GM_setValue // ==/UserScript== (function() { 'use strict'; // Single preview element const preview = { container: null, img: null, loading: null, currentId: null, visible: false, targetX: 0, targetY: 0, currentX: 0, currentY: 0, animationFrame: null, opacity: 0, lastEvent: null, resizeObserver: null, initialPositionSet: false, init() { this.container = document.createElement('div'); this.container.style.cssText = ` position: fixed; z-index: 10000; background: white; box-shadow: 0 0 10px rgba(0,0,0,0.5); opacity: 0; max-width: 800px; max-height: 800px; pointer-events: none; transform: translate3d(0,0,0); will-change: transform, opacity; transition: opacity 0.2s ease; display: none; `; this.img = new Image(); this.img.style.cssText = 'max-width: 800px; max-height: 800px;'; this.loading = document.createElement('div'); this.loading.textContent = 'Loading...'; this.loading.style.cssText = 'padding: 20px; text-align: center;'; // 创建 ResizeObserver 来监听尺寸变化 this.resizeObserver = new ResizeObserver(entries => { if (this.lastEvent && !this.initialPositionSet) { const pos = this.calculatePosition(this.lastEvent); this.currentX = this.targetX = pos.left; this.currentY = this.targetY = pos.top; this.container.style.transform = `translate3d(${pos.left}px,${pos.top}px,0)`; this.initialPositionSet = true; } }); this.resizeObserver.observe(this.container); document.body.appendChild(this.container); this.startAnimation(); }, startAnimation() { const animate = () => { if (this.visible) { this.currentX += (this.targetX - this.currentX) * 0.3; this.currentY += (this.targetY - this.currentY) * 0.3; this.container.style.transform = `translate3d(${this.currentX}px,${this.currentY}px,0)`; } this.animationFrame = requestAnimationFrame(animate); }; animate(); }, show(e) { this.lastEvent = e; this.initialPositionSet = false; this.container.style.display = 'block'; // 初始位置设置 requestAnimationFrame(() => { const pos = this.calculatePosition(e); this.currentX = this.targetX = pos.left; this.currentY = this.targetY = pos.top; this.container.style.transform = `translate3d(${pos.left}px,${pos.top}px,0)`; this.container.style.opacity = '1'; this.visible = true; }); }, hide() { this.container.style.opacity = '0'; this.visible = false; this.currentId = null; this.lastEvent = null; this.initialPositionSet = false; setTimeout(() => { if (!this.visible) { this.container.style.display = 'none'; } }, 200); }, setLoading() { this.container.innerHTML = ''; this.container.appendChild(this.loading); }, setImage(img) { this.container.innerHTML = ''; this.container.appendChild(img); this.initialPositionSet = false; // 重置位置标志 }, calculatePosition(e) { const rect = this.container.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // 根据可用空间自动判断显示位置 let left; const spaceRight = viewportWidth - e.clientX - 20; const spaceLeft = e.clientX - rect.width - 20; // 优先选择右侧,如果右侧空间不够则选择左侧 if (spaceRight >= rect.width) { left = e.clientX + 20; } else if (spaceLeft >= 0) { left = spaceLeft; } else { // 如果两侧都没有足够空间,选择空间较大的一侧 left = (spaceRight > e.clientX) ? viewportWidth - rect.width - 10 : 10; } // 确保顶部位置在视窗内 let top = e.clientY; if (top + rect.height > viewportHeight) { top = Math.max(10, viewportHeight - rect.height - 10); } return { left, top }; }, updatePosition(e) { if (!this.visible) return; const pos = this.calculatePosition(e); this.targetX = pos.left; this.targetY = pos.top; this.lastEvent = e; } }; // Request manager const requestManager = { queue: [], active: false, currentId: null, async process() { if (this.active || this.queue.length === 0) return; this.active = true; const task = this.queue.shift(); try { await task(); } catch (error) { console.error('Request error:', error); } this.active = false; this.process(); }, add(task) { this.queue.push(task); this.process(); }, clear() { this.queue.length = 0; this.active = false; } }; // Cache manager const cache = { urls: new Map(), images: new Map(), loadFromStorage() { try { const stored = GM_getValue('urlCache', '{}'); const data = JSON.parse(stored); const now = Date.now(); const DAY = 24 * 60 * 60 * 1000; for (const [key, entry] of Object.entries(data)) { if (now - entry.timestamp < DAY) { this.urls.set(key, entry.url); } } } catch (error) { console.error('Cache load error:', error); } }, saveToStorage: debounce(() => { const data = {}; cache.urls.forEach((url, key) => { data[key] = { url, timestamp: Date.now() }; }); GM_setValue('urlCache', JSON.stringify(data)); }, 5000) }; // Utilities function debounce(func, wait) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } // Image loader async function loadImage(imgId, baseUrl) { if (cache.urls.has(imgId)) { return cache.urls.get(imgId); } for (const ext of ['jpg', 'jpeg', 'png']) { const url = `${baseUrl}.${ext}`; try { const response = await fetch(url, { method: 'HEAD' }); if (response.ok) { cache.urls.set(imgId, url); cache.saveToStorage(); return url; } } catch (error) { continue; } } return null; } // Preview handler function handlePreview(e, thumbnail) { const img = thumbnail.querySelector('img'); if (!img) return; const match = img.src.match(/\/(\w+)\.\w+$/); if (!match) return; const imgId = match[1]; if (preview.currentId === imgId) { preview.updatePosition(e); return; } preview.currentId = imgId; preview.setLoading(); preview.show(e); if (cache.images.has(imgId)) { preview.setImage(cache.images.get(imgId).cloneNode(true)); return; } requestManager.clear(); requestManager.add(async () => { const folder1 = imgId.substring(0, 2); const folder2 = imgId.substring(2, 4); const baseUrl = `https://cdn.donmai.us/original/${folder1}/${folder2}/${imgId}`; try { const url = await loadImage(imgId, baseUrl); if (!url || preview.currentId !== imgId) return; const img = new Image(); img.style.cssText = 'max-width: 800px; max-height: 800px;'; img.onload = () => { if (preview.currentId === imgId) { cache.images.set(imgId, img.cloneNode(true)); preview.setImage(img); } }; img.src = url; } catch (error) { console.error('Image load error:', error); } }); } // Event handler setup function setupHandlers(thumbnail) { if (thumbnail.dataset.previewInitialized) return; thumbnail.dataset.previewInitialized = 'true'; thumbnail.addEventListener('mouseenter', e => handlePreview(e, thumbnail)); thumbnail.addEventListener('mouseleave', () => preview.hide()); thumbnail.addEventListener('mousemove', e => preview.updatePosition(e)); } // Initialize function init() { preview.init(); cache.loadFromStorage(); document.querySelectorAll('.post-preview-link').forEach(setupHandlers); new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.classList?.contains('post-preview-link')) { setupHandlers(node); } }); }); }).observe(document.body, { childList: true, subtree: true }); } // Start the script init(); })();