// ==UserScript== // @name 多功能整合:滚动记忆 + 多功能按钮 + 视频增强 // @namespace https://github.com/merged-allinone // @version 3.5.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, BUTTON_RIGHT: 6, BUTTON_GAP: 22, START_TOP: 66.6, ENABLE_DEEPSEEK_BTN: true, 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 = location.hostname; const currentUrl = location.href; const videoBlacklist = CONFIG.VIDEO_BLACKLIST.filter(r => r && r.trim() !== ''); const isVideoDisabled = videoBlacklist.some(rule => currentHost.toLowerCase().includes(rule.trim().toLowerCase()) || currentUrl.toLowerCase().includes(rule.trim().toLowerCase()) ); // ==================== 工具函数 ==================== const throttle = (fn, wait) => { let timeout, lastRun = 0; return function(...args) { const now = Date.now(); const later = () => { lastRun = Date.now(); timeout = null; fn.apply(this, args); }; if (now - lastRun >= wait) later(); else if (!timeout) timeout = setTimeout(later, wait - (now - lastRun)); }; }; const debounce = (fn, wait) => { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => fn.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 methods = ['requestFullscreen','webkitRequestFullscreen','mozRequestFullScreen','msRequestFullscreen']; for (const m of methods) if (el[m]) { el[m](); return true; } return false; }, exit: () => { const methods = ['exitFullscreen','webkitExitFullscreen','mozCancelFullScreen','msExitFullscreen']; for (const m of methods) if (document[m]) { document[m](); return true; } return false; }, getElement: () => document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement, }; const showToast = (msg, 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 = msg; 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) 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.videoEnabled = !isVideoDisabled; this.doubleTap = { lastTap: 0 }; this.swipeState = { startY:0, startX:0, active:false, hintTimer:null }; this.STORAGE_KEY = 'lemonScrollBox_v3'; this.currentBoxObj = {}; this.injectStyles(); this.createAllButtons(); this.bindEvents(); this.initGestures(); 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: 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, transform 0.2s, background 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: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 #fff; border-bottom:2.2px solid #fff; 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: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:rgba(220,220,220,0.2); border-color:rgba(255,255,255,0.1); } #${vIds.SPEED}.speed-active { background: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(); location.href = CONFIG.DEEPSEEK_URL; }); } else { document.getElementById(mIds.DEEPSEEK).style.display = 'none'; } const backBtn = document.getElementById(mIds.BACK); let pressTimer, isLongPress = false; const clearTimer = () => { if (pressTimer) clearTimeout(pressTimer); pressTimer = null; }; const goBack = () => { if (document.referrer && document.referrer !== location.href) { location.href = document.referrer; return; } const before = location.href; try { history.back(); setTimeout(() => { if (location.href === before) showToast('没有上一页,可长按返回首页'); }, 500); } catch { showToast('没有上一页,可长按返回首页'); } }; const goHome = () => { 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); }); } // ---------- 手势系统(完全解耦,支持视频外滑动,修复首次不生效) ---------- initGestures() { // 有效性检查:必须是可见视频元素 const isValidVideo = (el) => el && el.tagName === 'VIDEO' && el.videoWidth > 0 && el.videoHeight > 0; // 是否允许全屏手势(视频增强启用且存在活跃视频) const canSwipe = () => this.videoEnabled && getActiveVideo() !== null; // ---------- 双击暂停/播放(仅视频上) ---------- if (CONFIG.ENABLE_DOUBLE_TAP) { document.addEventListener('touchstart', (e) => { const target = e.target; if (!isValidVideo(target)) return; const now = Date.now(); if (now - this.doubleTap.lastTap < CONFIG.DOUBLE_TAP_DELAY) { e.preventDefault(); target.paused ? target.play() : target.pause(); } this.doubleTap.lastTap = now; }, { passive: false }); } // ---------- 全屏滑动手势(上滑进入,下滑退出) ---------- if (CONFIG.ENABLE_SWIPE_FULLSCREEN) { // 独立状态,避免污染实例 const state = { startY: 0, startX: 0, hintTimer: null, targetVideo: null, // 用于提示遮罩的目标视频 isTracking: false }; const clearHint = () => { if (state.targetVideo) { state.targetVideo.classList.remove('ve-swipe-hint'); } if (state.hintTimer) { clearTimeout(state.hintTimer); state.hintTimer = null; } }; const resetState = () => { clearHint(); state.startY = 0; state.startX = 0; state.targetVideo = null; state.isTracking = false; }; // 获取当前应该操作/提示的视频(活跃视频) const getTargetVideo = () => getActiveVideo(); document.addEventListener('touchstart', (e) => { const touch = e.touches[0]; if (!touch) return; const isFS = !!fullscreenAPI.getElement(); // 全屏时,仅当触摸在有效视频上才跟踪(用于下滑退出) if (isFS) { const target = e.target; if (!isValidVideo(target)) return; e.preventDefault(); // 阻止全屏下的页面滚动 state.startY = touch.clientY; state.startX = touch.clientX; state.targetVideo = target; state.isTracking = true; return; } // 非全屏时:只要视频增强可用,就允许任意位置滑动进入全屏 if (!canSwipe()) return; // 记录起始点,但不立即阻止默认事件,等到touchmove确定意图后再处理 state.startY = touch.clientY; state.startX = touch.clientX; state.targetVideo = getTargetVideo(); // 用于提示遮罩 state.isTracking = true; // 不调用 preventDefault,让页面正常滚动,直到达到阈值 }, { passive: false }); document.addEventListener('touchmove', (e) => { if (!state.isTracking) return; const touch = e.touches[0]; if (!touch) { resetState(); return; } const deltaY = state.startY - touch.clientY; const deltaX = Math.abs(state.startX - touch.clientX); const isFS = !!fullscreenAPI.getElement(); if (!isFS) { // 非全屏:上滑进入全屏 if (!canSwipe()) { resetState(); return; } // 水平偏移过大则放弃本次手势 if (deltaX > CONFIG.SWIPE_HORIZONTAL_TOLERANCE) { resetState(); return; } // 达到上滑阈值,阻止页面滚动,准备进入全屏 if (deltaY > CONFIG.SWIPE_THRESHOLD) { e.preventDefault(); if (CONFIG.SHOW_SWIPE_HINT && state.targetVideo) { state.targetVideo.classList.add('ve-swipe-hint'); if (state.hintTimer) clearTimeout(state.hintTimer); state.hintTimer = setTimeout(() => { state.targetVideo?.classList.remove('ve-swipe-hint'); state.hintTimer = null; }, 300); } } } else { // 全屏:下滑退出 e.preventDefault(); if (!state.targetVideo || !isValidVideo(state.targetVideo)) { resetState(); return; } if (deltaX > CONFIG.SWIPE_HORIZONTAL_TOLERANCE) { resetState(); return; } if (deltaY < -CONFIG.SWIPE_THRESHOLD) { if (CONFIG.SHOW_SWIPE_HINT && state.targetVideo) { state.targetVideo.classList.add('ve-swipe-hint'); if (state.hintTimer) clearTimeout(state.hintTimer); state.hintTimer = setTimeout(() => { state.targetVideo?.classList.remove('ve-swipe-hint'); state.hintTimer = null; }, 300); } } } }, { passive: false }); document.addEventListener('touchend', (e) => { if (!state.isTracking) return; const touch = e.changedTouches[0]; if (!touch) { resetState(); return; } const deltaY = state.startY - touch.clientY; const deltaX = Math.abs(state.startX - touch.clientX); const isFS = !!fullscreenAPI.getElement(); if (!isFS) { if (!canSwipe()) { resetState(); return; } // 非全屏:上滑进入全屏 if (deltaX <= CONFIG.SWIPE_HORIZONTAL_TOLERANCE && deltaY > CONFIG.SWIPE_THRESHOLD) { const video = getActiveVideo(); if (video) { // 关键:在touchend中同步调用全屏API,确保手势有效 const success = fullscreenAPI.request(video); if (!success) { showToast('全屏被浏览器拒绝,请点击全屏按钮', 2000); } } } } else { // 全屏:下滑退出全屏 if (deltaX <= CONFIG.SWIPE_HORIZONTAL_TOLERANCE && deltaY < -CONFIG.SWIPE_THRESHOLD) { fullscreenAPI.exit(); } } resetState(); }); document.addEventListener('touchcancel', () => { resetState(); }); } } // ---------- 滚动记忆 ---------- 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(); } })();