// ==UserScript== // @name 多功能整合:滚动记忆 + 多功能按钮 + 视频增强 // @namespace https://github.com/merged-allinone // @version 3.3.0 // @description 记录滚动位置、回顶/返回/刷新/DeepSeek按钮、视频控制(全屏/3倍速/快进/重播/手势)——根据视频有无自动切换(黑名单仅对视频增强生效,优化上滑全屏手势) // @author Assistant // @match *://*/* // @run-at document-start // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // ==================== 全局配置 ==================== const CONFIG = { // 视频增强黑名单(命中则禁用视频增强功能,多功能按钮等其他功能仍正常工作) VIDEO_BLACKLIST: [ 'manhuabika.com', ], // 通用按钮样式 BUTTON_SIZE: 33, // 按钮直径(px) BUTTON_RIGHT: 6, // 距右侧距离(px) BUTTON_GAP: 22, // 垂直间距(px) START_TOP: 66.6, // 第一个按钮距顶部的 vh 值 // 多功能按钮配置 ENABLE_DEEPSEEK_BTN: true, // 是否显示 DeepSeek 按钮 DEEPSEEK_URL: 'https://chat.deepseek.com/', SCROLL_THRESHOLD: 200, // 滚动超过此值显示回顶按钮 LONG_PRESS_DELAY: 500, // 长按返回首页的触发时间(毫秒) // 视频增强配置 SEEK_STEP: 20, // 快进步长(秒) ENABLE_DOUBLE_TAP: true, // 是否启用双击暂停/播放 ENABLE_SWIPE_FULLSCREEN: true, // 是否启用上滑全屏/下滑退出全屏 DOUBLE_TAP_DELAY: 300, // 双击检测间隔(毫秒) SWIPE_THRESHOLD: 40, // 滑动触发阈值(像素) SWIPE_HORIZONTAL_TOLERANCE: 70, // 水平滑动容差(像素) SHOW_SWIPE_HINT: true, // 是否显示滑动提示遮罩 // 视频进度缓存 CACHE_PREFIX: 'video_progress_', AUTO_SAVE_INTERVAL: 5000, // 自动保存间隔(毫秒) CACHE_EXPIRE_DAYS: 7, // 缓存有效期(天) RESTORE_MIN_REMAINING: 30, // 剩余时间大于此值才恢复进度(秒) }; // ==================== 视频增强黑名单检查 ==================== const currentHost = window.location.hostname; const currentUrl = window.location.href; const videoBlacklist = CONFIG.VIDEO_BLACKLIST.filter(rule => rule && rule.trim() !== ''); const isVideoDisabled = videoBlacklist.some(rule => currentHost.toLowerCase().includes(rule.trim().toLowerCase()) || currentUrl.toLowerCase().includes(rule.trim().toLowerCase()) ); // ==================== 工具函数 ==================== const throttle = (func, wait) => { let timeout = null, lastRun = 0; return function(...args) { const now = Date.now(); const later = () => { lastRun = Date.now(); timeout = null; func.apply(this, args); }; if (now - lastRun >= wait) later(); else if (!timeout) timeout = setTimeout(later, wait - (now - lastRun)); }; }; const debounce = (func, wait) => { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; }; const styleRegistry = new Set(); const addStyle = (css, id) => { if (styleRegistry.has(id)) return; const style = document.createElement('style'); if (id) style.id = id; style.textContent = css; (document.head || document.documentElement).appendChild(style); if (id) styleRegistry.add(id); }; const fullscreenAPI = { request: (el) => { const m = ['requestFullscreen','webkitRequestFullscreen','mozRequestFullScreen','msRequestFullscreen']; for (let method of m) if (el[method]) { el[method](); return true; } return false; }, exit: () => { const m = ['exitFullscreen','webkitExitFullscreen','mozCancelFullScreen','msExitFullscreen']; for (let method of m) if (document[method]) { document[method](); return true; } return false; }, getElement: () => document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement, isEnabled: () => document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled }; const showToast = (message, duration = 2000) => { let toast = document.getElementById('allinone-toast'); if (!toast) { if (!document.body) return; toast = document.createElement('div'); toast.id = 'allinone-toast'; toast.style.cssText = ` position:fixed; bottom:20vh; left:50%; transform:translateX(-50%); background:rgba(60,60,60,0.8); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); color:#fff; padding:10px 20px; border-radius:30px; font-size:14px; z-index:2147483647; opacity:0; transition:opacity 0.2s; pointer-events:none; white-space:nowrap; box-shadow:0 2px 8px rgba(0,0,0,0.15); border:1px solid rgba(255,255,255,0.2); `; document.body.appendChild(toast); } toast.textContent = message; toast.style.opacity = '1'; clearTimeout(toast.hideTimer); toast.hideTimer = setTimeout(() => toast.style.opacity = '0', duration); }; const getActiveVideo = () => { const videos = Array.from(document.querySelectorAll('video')).filter(v => v.videoWidth > 0 && v.videoHeight > 0); if (videos.length === 0) return null; const scored = videos.map(v => { let score = 0; if (!v.paused && v.readyState >= 2) score += 5; if (v.src || v.currentSrc) score += 3; const rect = v.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) score += 2; return { video: v, score }; }); scored.sort((a,b) => b.score - a.score); return scored[0].video; }; // ==================== 主类 ==================== class AllInOne { constructor() { this.currentVideo = null; this.isSpeed3x = false; this.videoHandlers = new WeakMap(); this.gesture = { startY:0, startX:0, target:null, hintTimer:null, lastTap:0 }; // 滚动记忆 this.STORAGE_KEY = 'lemonScrollBox_v3'; this.currentBoxObj = {}; // 视频增强是否启用(由黑名单决定) this.videoEnabled = !isVideoDisabled; this.injectStyles(); this.createAllButtons(); this.bindEvents(); this.bindGestures(); this.startObservers(); this.initScrollMemory(); this.updateUI(); } // ---------- 样式注入 ---------- injectStyles() { const { BUTTON_SIZE, BUTTON_RIGHT, BUTTON_GAP, START_TOP, SEEK_STEP } = CONFIG; const vIds = { FULLSCREEN:'ve-fullscreen', SPEED:'ve-speed', FORWARD:'ve-forward', RESTART:'ve-restart' }; const mIds = { TOP:'multi-top', BACK:'multi-back', REFRESH:'multi-refresh', DEEPSEEK:'multi-deepseek' }; let css = ` #${vIds.FULLSCREEN}, #${vIds.SPEED}, #${vIds.FORWARD}, #${vIds.RESTART}, #${mIds.TOP}, #${mIds.BACK}, #${mIds.REFRESH}, #${mIds.DEEPSEEK} { position: fixed; right: ${BUTTON_RIGHT}px; width: ${BUTTON_SIZE}px; height: ${BUTTON_SIZE}px; border-radius: 50%; background-color: rgba(60, 60, 60, 0.5); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); box-shadow: 0 2px 8px rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 2147483647; transition: opacity 0.25s ease, transform 0.2s ease, background-color 0.2s; border: 1px solid rgba(255,255,255,0.2); color: white; font-weight: 600; font-family: Arial, sans-serif; } `; const vPos = [ { id: vIds.FULLSCREEN, offset: 0 }, { id: vIds.SPEED, offset: 1 }, { id: vIds.FORWARD, offset: 2 }, { id: vIds.RESTART, offset: 3 } ]; vPos.forEach(({ id, offset }) => css += `#${id} { top: calc(${START_TOP}vh + ${offset * (BUTTON_SIZE + BUTTON_GAP)}px); }\n`); const mPos = [ { id: mIds.TOP, offset: 0 }, { id: mIds.BACK, offset: 1 }, { id: mIds.REFRESH, offset: 2 }, { id: mIds.DEEPSEEK, offset: 3 } ]; mPos.forEach(({ id, offset }) => css += `#${id} { top: calc(${START_TOP}vh + ${offset * (BUTTON_SIZE + BUTTON_GAP)}px); }\n`); css += ` #${mIds.TOP} { opacity: 0; transform: scale(0.8); pointer-events: none; } #${mIds.TOP}.visible { opacity: 1; transform: scale(1); pointer-events: auto; } #${vIds.FULLSCREEN}, #${vIds.SPEED}, #${vIds.FORWARD}, #${vIds.RESTART} { opacity: 0; transform: scale(0.8); pointer-events: none; } .video-mode #${vIds.FULLSCREEN}, .video-mode #${vIds.SPEED}, .video-mode #${vIds.FORWARD}, .video-mode #${vIds.RESTART} { opacity: 1; transform: scale(1); pointer-events: auto; } .video-mode #${mIds.TOP}, .video-mode #${mIds.BACK}, .video-mode #${mIds.REFRESH}, .video-mode #${mIds.DEEPSEEK} { opacity: 0; transform: scale(0.8); pointer-events: none; } #${vIds.FULLSCREEN}::before { content: '⛶'; font-size: 22px; } #${vIds.SPEED}::before { content: '3×'; } #${vIds.SPEED}.speed-active { background-color: rgba(0, 150, 200, 0.7) !important; } #${vIds.FORWARD}::before { content: '↷${SEEK_STEP}s'; font-size: 12px; } #${vIds.RESTART}::before { content: '↺'; font-size: 20px; } #${mIds.TOP}::before { content: ''; width: 10px; height: 10px; border-left: 2.2px solid white; border-bottom: 2.2px solid white; transform: rotate(135deg); margin-top: 4px; } #${mIds.BACK} { font-size: 24px; font-weight: 400; line-height: 1; } #${mIds.BACK}::before { content: '<'; margin-top: -2px; } #${mIds.REFRESH}::before { content: '↻'; font-size: 26px; line-height: 1; } #${mIds.DEEPSEEK} { font-size: 16px; } #${mIds.DEEPSEEK}::before { content: 'DS'; line-height: 1; text-shadow: 0 1px 2px rgba(0,0,0,0.2); } #${vIds.FULLSCREEN}:active, #${vIds.SPEED}:active, #${vIds.FORWARD}:active, #${vIds.RESTART}:active, #${mIds.TOP}:active, #${mIds.BACK}:active, #${mIds.REFRESH}:active, #${mIds.DEEPSEEK}:active { background-color: rgba(80, 80, 80, 0.6); transform: scale(0.9); } @media (prefers-color-scheme: dark) { #${vIds.FULLSCREEN}, #${vIds.SPEED}, #${vIds.FORWARD}, #${vIds.RESTART}, #${mIds.TOP}, #${mIds.BACK}, #${mIds.REFRESH}, #${mIds.DEEPSEEK} { background-color: rgba(220, 220, 220, 0.2); border-color: rgba(255,255,255,0.1); } #${vIds.SPEED}.speed-active { background-color: rgba(0, 150, 200, 0.5) !important; } } .ve-swipe-hint { position: relative; } .ve-swipe-hint::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 150, 200, 0.3); pointer-events: none; z-index: 999; animation: veHintFade 0.2s ease; } @keyframes veHintFade { from { opacity: 0; } to { opacity: 1; } } `; addStyle(css, 'allinone-styles'); } // ---------- 创建所有按钮 ---------- createAllButtons() { const vIds = { FULLSCREEN:'ve-fullscreen', SPEED:'ve-speed', FORWARD:'ve-forward', RESTART:'ve-restart' }; const mIds = { TOP:'multi-top', BACK:'multi-back', REFRESH:'multi-refresh', DEEPSEEK:'multi-deepseek' }; Object.entries(vIds).forEach(([key, id]) => { const btn = document.createElement('div'); btn.id = id; btn.setAttribute('aria-label', key.toLowerCase()); document.body.appendChild(btn); btn.addEventListener('click', (e) => { e.stopPropagation(); this.handleVideoAction(key); }); }); Object.entries(mIds).forEach(([key, id]) => { const btn = document.createElement('div'); btn.id = id; btn.setAttribute('aria-label', key.toLowerCase()); document.body.appendChild(btn); }); document.getElementById(mIds.TOP).addEventListener('click', (e) => { e.stopPropagation(); window.scrollTo({ top: 0, behavior: 'smooth' }); }); document.getElementById(mIds.REFRESH).addEventListener('click', (e) => { e.stopPropagation(); location.reload(); }); if (CONFIG.ENABLE_DEEPSEEK_BTN) { document.getElementById(mIds.DEEPSEEK).addEventListener('click', (e) => { e.stopPropagation(); window.location.href = CONFIG.DEEPSEEK_URL; }); } else { document.getElementById(mIds.DEEPSEEK).style.display = 'none'; } const backBtn = document.getElementById(mIds.BACK); let pressTimer = null, isLongPress = false; const clearTimer = () => { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; } }; const goBack = () => { if (document.referrer && document.referrer !== location.href) { window.location.href = document.referrer; return; } const before = location.href; try { history.back(); setTimeout(() => { if (location.href === before) showToast('没有上一页,可长按返回首页', 2000); }, 500); } catch { showToast('没有上一页,可长按返回首页', 2000); } }; const goHome = () => { window.location.href = 'v://home'; }; backBtn.addEventListener('touchstart', (e) => { e.stopPropagation(); isLongPress = false; clearTimer(); pressTimer = setTimeout(() => { isLongPress = true; goHome(); }, CONFIG.LONG_PRESS_DELAY); }); backBtn.addEventListener('touchend', (e) => { e.stopPropagation(); clearTimer(); if (!isLongPress) goBack(); }); backBtn.addEventListener('touchcancel', clearTimer); backBtn.addEventListener('mousedown', (e) => { e.stopPropagation(); isLongPress = false; clearTimer(); pressTimer = setTimeout(() => { isLongPress = true; goHome(); }, CONFIG.LONG_PRESS_DELAY); }); backBtn.addEventListener('mouseup', (e) => { e.stopPropagation(); clearTimer(); if (!isLongPress) goBack(); }); backBtn.addEventListener('mouseleave', clearTimer); } // ---------- 视频按钮动作 ---------- handleVideoAction(action) { if (!this.videoEnabled) { alert('视频增强已禁用'); return; } const video = getActiveVideo(); if (!video) { alert('未检测到视频'); return; } switch (action) { case 'FULLSCREEN': fullscreenAPI.getElement() ? fullscreenAPI.exit() : fullscreenAPI.request(video); break; case 'SPEED': this.isSpeed3x = !this.isSpeed3x; video.playbackRate = this.isSpeed3x ? 3.0 : 1.0; document.getElementById('ve-speed')?.classList.toggle('speed-active', this.isSpeed3x); break; case 'FORWARD': video.currentTime = Math.min(video.currentTime + CONFIG.SEEK_STEP, video.duration); this.saveProgress(video); break; case 'RESTART': video.currentTime = 0; video.play(); this.saveProgress(video); break; } } // ---------- 视频进度管理 ---------- getCacheKey(video) { const src = video.src || video.currentSrc || location.href; try { return CONFIG.CACHE_PREFIX + btoa(encodeURIComponent(src)).replace(/[^a-zA-Z0-9]/g, ''); } catch { return CONFIG.CACHE_PREFIX + src.replace(/\W/g, ''); } } saveProgress(video) { if (!video || !isFinite(video.duration)) return; try { localStorage.setItem(this.getCacheKey(video), JSON.stringify({ currentTime: video.currentTime, duration: video.duration, playbackRate: video.playbackRate, timestamp: Date.now() })); } catch(e) {} } restoreProgress(video) { if (!video) return; try { const saved = localStorage.getItem(this.getCacheKey(video)); if (!saved) return; const p = JSON.parse(saved); if (Date.now() - p.timestamp > CONFIG.CACHE_EXPIRE_DAYS * 86400000) { localStorage.removeItem(this.getCacheKey(video)); return; } const apply = () => { if (video.readyState >= 1 && isFinite(video.duration)) { const t = Math.min(p.currentTime, video.duration - CONFIG.RESTORE_MIN_REMAINING); if (t > 0) video.currentTime = t; video.playbackRate = p.playbackRate || 1.0; this.isSpeed3x = (video.playbackRate === 3.0); document.getElementById('ve-speed')?.classList.toggle('speed-active', this.isSpeed3x); return true; } return false; }; if (!apply()) { const check = setInterval(() => { if (apply() || !video.isConnected) clearInterval(check); }, 100); setTimeout(() => clearInterval(check), 5000); } } catch(e) {} } clearProgress(video) { if (!video) return; try { localStorage.removeItem(this.getCacheKey(video)); } catch(e) {} } attachVideo(video) { if (!video || this.currentVideo === video) return; this.detachVideo(); this.currentVideo = video; this.isSpeed3x = (video.playbackRate === 3.0); document.getElementById('ve-speed')?.classList.toggle('speed-active', this.isSpeed3x); this.restoreProgress(video); const throttledSave = throttle(() => this.saveProgress(video), CONFIG.AUTO_SAVE_INTERVAL); const onPause = () => this.saveProgress(video); const onEnded = () => this.clearProgress(video); video.addEventListener('timeupdate', throttledSave); video.addEventListener('pause', onPause); video.addEventListener('ended', onEnded); this.videoHandlers.set(video, { throttledSave, onPause, onEnded }); } detachVideo() { if (!this.currentVideo) return; const video = this.currentVideo; const h = this.videoHandlers.get(video); if (h) { video.removeEventListener('timeupdate', h.throttledSave); video.removeEventListener('pause', h.onPause); video.removeEventListener('ended', h.onEnded); this.videoHandlers.delete(video); } this.saveProgress(video); this.currentVideo = null; } // ---------- UI 更新 ---------- updateUI = debounce(() => { const video = this.videoEnabled ? getActiveVideo() : null; const hasVideo = video && video.readyState >= 1 && isFinite(video.duration) && video.duration > 0; document.body.classList.toggle('video-mode', hasVideo); const topBtn = document.getElementById('multi-top'); if (topBtn) { const scrollY = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0; topBtn.classList.toggle('visible', scrollY > CONFIG.SCROLL_THRESHOLD); } if (hasVideo && video !== this.currentVideo) { this.attachVideo(video); } else if (!hasVideo && this.currentVideo) { this.detachVideo(); } }, 100); // ---------- 事件监听 ---------- bindEvents() { window.addEventListener('scroll', this.updateUI, { passive: true }); document.addEventListener('play', this.updateUI, true); document.addEventListener('pause', this.updateUI, true); document.addEventListener('loadedmetadata', this.updateUI, true); document.addEventListener('fullscreenchange', this.updateUI); document.addEventListener('webkitfullscreenchange', this.updateUI); window.addEventListener('beforeunload', () => { if (this.currentVideo) this.saveProgress(this.currentVideo); }); } // ---------- 手势(优化:视频外上滑也可全屏,下滑仅全屏时生效) ---------- bindGestures() { if (!CONFIG.ENABLE_DOUBLE_TAP && !CONFIG.ENABLE_SWIPE_FULLSCREEN) return; const isValidVideoTarget = (el) => el && el.tagName === 'VIDEO' && el.videoWidth > 0 && el.videoHeight > 0; // 上滑全屏允许在任意位置触发,只要页面有视频且视频增强启用 const canSwipeToFullscreen = () => this.videoEnabled && getActiveVideo() !== null; if (CONFIG.ENABLE_DOUBLE_TAP) { document.addEventListener('touchstart', (e) => { const t = e.target; if (!isValidVideoTarget(t)) return; const now = Date.now(); if (now - this.gesture.lastTap < CONFIG.DOUBLE_TAP_DELAY) { e.preventDefault(); t.paused ? t.play() : t.pause(); } this.gesture.lastTap = now; }, { passive: false }); } if (CONFIG.ENABLE_SWIPE_FULLSCREEN) { const g = this.gesture; const clearHint = () => { if (g.target) g.target.classList.remove('ve-swipe-hint'); if (g.hintTimer) clearTimeout(g.hintTimer); g.hintTimer = null; }; document.addEventListener('touchstart', (e) => { const touch = e.touches[0]; if (!touch) return; const isFS = !!fullscreenAPI.getElement(); // 全屏时仅当触摸视频才处理,非全屏时任意位置都可准备上滑 if (isFS) { const t = e.target; if (!isValidVideoTarget(t)) return; e.preventDefault(); g.target = t; } else { // 非全屏时,只要视频增强启用且有视频,就允许上滑进入全屏 if (!canSwipeToFullscreen()) return; // 不阻止默认事件,让页面正常滚动,直到 touchmove 判断意图 g.target = document.body; // 虚拟目标,实际全屏时使用活跃视频 } g.startY = touch.clientY; g.startX = touch.clientX; }, { passive: false }); document.addEventListener('touchmove', (e) => { if (g.startY === 0) return; const touch = e.touches[0]; if (!touch) return; const deltaY = g.startY - touch.clientY; const deltaX = Math.abs(g.startX - touch.clientX); const isFS = !!fullscreenAPI.getElement(); if (!isFS) { if (!canSwipeToFullscreen()) { clearHint(); g.startY = g.startX = 0; g.target = null; return; } if (deltaX > CONFIG.SWIPE_HORIZONTAL_TOLERANCE) { clearHint(); g.startY = g.startX = 0; g.target = null; return; } if (deltaY > CONFIG.SWIPE_THRESHOLD) { e.preventDefault(); // 阻止页面滚动,准备进入全屏 if (CONFIG.SHOW_SWIPE_HINT) { const video = getActiveVideo(); if (video) { video.classList.add('ve-swipe-hint'); if (g.hintTimer) clearTimeout(g.hintTimer); g.hintTimer = setTimeout(() => { video.classList.remove('ve-swipe-hint'); g.hintTimer = null; }, 300); } } } return; } // 全屏状态 e.preventDefault(); if (!g.target || !isValidVideoTarget(g.target)) { clearHint(); g.startY = g.startX = 0; g.target = null; return; } if (deltaX > CONFIG.SWIPE_HORIZONTAL_TOLERANCE) { clearHint(); g.startY = g.startX = 0; g.target = null; return; } if (deltaY < -CONFIG.SWIPE_THRESHOLD && CONFIG.SHOW_SWIPE_HINT) { g.target.classList.add('ve-swipe-hint'); if (g.hintTimer) clearTimeout(g.hintTimer); g.hintTimer = setTimeout(() => { g.target.classList.remove('ve-swipe-hint'); g.hintTimer = null; }, 300); } }, { passive: false }); document.addEventListener('touchend', (e) => { if (g.startY === 0) return; const touch = e.changedTouches[0]; if (!touch) { clearHint(); g.startY = g.startX = 0; g.target = null; return; } const deltaY = g.startY - touch.clientY; const deltaX = Math.abs(g.startX - touch.clientX); const isFS = !!fullscreenAPI.getElement(); if (!isFS) { if (!canSwipeToFullscreen()) { clearHint(); g.startY = g.startX = 0; g.target = null; return; } if (deltaX <= CONFIG.SWIPE_HORIZONTAL_TOLERANCE && deltaY > CONFIG.SWIPE_THRESHOLD) { const video = getActiveVideo(); if (video) fullscreenAPI.request(video); } } else { if (g.target && isValidVideoTarget(g.target) && deltaX <= CONFIG.SWIPE_HORIZONTAL_TOLERANCE && deltaY < -CONFIG.SWIPE_THRESHOLD) { fullscreenAPI.exit(); } } clearHint(); g.startY = g.startX = 0; g.target = null; }); document.addEventListener('touchcancel', () => { clearHint(); g.startY = g.startX = 0; g.target = null; }); } } // ---------- 滚动记忆 ---------- getXPath(element) { if (!element || element.nodeType !== 1) return ''; if (element.id) return `//*[@id="${element.id}"]`; let paths = []; for (; element && element.nodeType === 1; element = element.parentNode) { let index = 0; for (let sibling = element.previousSibling; sibling; sibling = sibling.previousSibling) { if (sibling.nodeType === 1 && sibling.nodeName === element.nodeName) index++; } const tagName = element.nodeName.toLowerCase(); const pathIndex = index ? `[${index + 1}]` : ''; paths.unshift(`${tagName}${pathIndex}`); } return '/' + paths.join('/'); } getScrollContainer() { return document.scrollingElement || document.documentElement; } saveScrollPosition() { try { const container = this.getScrollContainer(); if (!container) return; const url = location.href; const xpath = this.getXPath(container); this.currentBoxObj[url] = { xpath, pos: container.scrollTop, id: container.id || '', className: container.className || '' }; localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.currentBoxObj)); } catch(e) {} } restoreScrollPosition() { try { const saved = localStorage.getItem(this.STORAGE_KEY); if (!saved) return; this.currentBoxObj = JSON.parse(saved); const url = location.href; const data = this.currentBoxObj[url]; if (!data) return; const container = this.getScrollContainer(); if (container && data.pos) setTimeout(() => { container.scrollTop = data.pos; }, 100); } catch(e) {} } initScrollMemory() { if (!localStorage.getItem(this.STORAGE_KEY)) { localStorage.setItem(this.STORAGE_KEY, '{}'); this.currentBoxObj = {}; } else { try { this.currentBoxObj = JSON.parse(localStorage.getItem(this.STORAGE_KEY)) || {}; } catch(e) { this.currentBoxObj = {}; } } const throttledSave = throttle(() => this.saveScrollPosition(), 500); document.addEventListener('scroll', throttledSave, { passive: true }); this.restoreScrollPosition(); let lastUrl = location.href; new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; this.restoreScrollPosition(); } }).observe(document, { subtree: true, childList: true }); window.addEventListener('popstate', () => this.restoreScrollPosition()); } startObservers() { new MutationObserver(() => this.updateUI()).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); new MutationObserver(() => { if (this.currentVideo && !this.currentVideo.isConnected) { this.detachVideo(); this.updateUI(); } }).observe(document.body, { childList: true, subtree: true }); } } // 启动 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => new AllInOne()); } else { new AllInOne(); } })();