// ==UserScript== // @name LANraragi 阅读模式 // @namespace https://github.com/Kelcoin // @version 4.6 // @description 为 LANraragi 阅读器添加阅读模式 // @author Kelcoin // @match *://*/reader?id=* // @run-at document-end // @grant none // @icon https://github.com/Difegue/LANraragi/raw/dev/public/favicon.ico // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/560895/LANraragi%20%E9%98%85%E8%AF%BB%E6%A8%A1%E5%BC%8F.user.js // @updateURL https://update.greasyfork.icu/scripts/560895/LANraragi%20%E9%98%85%E8%AF%BB%E6%A8%A1%E5%BC%8F.meta.js // ==/UserScript== (function () { 'use strict'; // -------- 配置与常量 -------- const BUTTON_ID = 'lrr-reading-toggle-btn'; const THUMB_BUTTON_ID = 'lrr-reading-thumb-btn'; const AUTO_BTN_ID = 'lrr-reading-auto-btn'; const PAGE_INDICATOR_ID = 'lrr-reading-page-indicator'; const HITAREA_ID = 'lrr-reading-hitarea'; const STYLE_ID = 'lrr-reading-style-v4'; const BODY_READING_CLASS = 'lrr-reading-mode'; const SETTINGS_MODAL_ID = 'lrr-reading-settings-modal'; const LOADING_MASK_ID = 'lrr-reading-loading-mask'; const PROGRESS_BAR_ID = 'lrr-reading-auto-progress'; // 交互参数 const MAX_DRAG_RATIO = 1.0; const CLICK_THRESHOLD_RATIO = 0.01; //点击翻页灵敏度 const SWIPE_THRESHOLD_RATIO = 0.1; //滑动翻页灵敏度 const CORNER_HEIGHT_RATIO = 0.15; const DOUBLE_TAP_DELAY = 250; // 双击判定最大间隔 (毫秒) const ZOOM_ANIM_SPEED = '0.2s'; // 缩放动画耗时 (秒) // 默认设置 const DEFAULT_SETTINGS = { autoEnter: false, btnPosition: 'right', pageGap: 0, expandDirection: 'up', // 默认向上展开 autoTurnInterval: 5, autoProgressPos: 'none' }; let userSettings = { ...DEFAULT_SETTINGS }; // 状态管理 let dragState = { active: false, startX: 0, currentX: 0, targetImg: null, rafId: null, isTouch: false }; let btnHideTimer = null; let lastScrollTop = 0; let originalApplyContainerWidth = null; let originalGoToPage = null; let readingSessionId = 0; let indicatorHideTimer = null; let inputBlockUntil = 0; let imgResizeObserver = null; let lastTurnTime = 0; // 自动翻页状态 let autoTurnState = { active: false, timer: null }; // 缩放状态管理 let zoomState = { scale: 1, panX: 0, panY: 0, initialDistance: 0, initialScale: 1 }; // 数据缓存 let archiveData = { pages: [], loaded: false }; // ========================================== // 设置管理 // ========================================== function loadSettings() { try { const stored = localStorage.getItem('lrr_reading_settings'); if (stored) { userSettings = { ...DEFAULT_SETTINGS, ...JSON.parse(stored) }; } else { const oldAuto = localStorage.getItem('lrr_auto_read'); if (oldAuto) userSettings.autoEnter = oldAuto === '1'; } } catch (e) { console.error("Load Settings Error", e); } } function saveSettings() { try { localStorage.setItem('lrr_reading_settings', JSON.stringify(userSettings)); localStorage.setItem('lrr_auto_read', userSettings.autoEnter ? '1' : '0'); } catch (e) { } applyLayoutSettings(); // 如果正在自动翻页,更新间隔 if (autoTurnState.active) { stopAutoTurn(); startAutoTurn(); } } // 应用布局设置 function applyLayoutSettings() { const mainBtn = document.getElementById(BUTTON_ID); const thumbBtn = document.getElementById(THUMB_BUTTON_ID); const autoBtn = document.getElementById(AUTO_BTN_ID); const display = document.getElementById('display'); const hitArea = document.getElementById(HITAREA_ID); const oldAutoHit = document.getElementById('lrr-reading-auto-hitarea'); if (oldAutoHit) oldAutoHit.style.display = 'none'; const isRight = userSettings.btnPosition === 'right'; // 1. 设置按钮锚点 [mainBtn, thumbBtn, autoBtn].forEach(btn => { if (!btn) return; btn.style.left = ''; btn.style.right = ''; btn.style.top = ''; btn.style.bottom = ''; btn.style.transform = ''; btn.style.bottom = '30px'; if (isRight) { btn.style.right = '15px'; } else { btn.style.left = '15px'; } }); // 2. 动态设置热区位置 if (hitArea) { hitArea.style.left = 'auto'; hitArea.style.right = 'auto'; if (isRight) { hitArea.style.right = '0'; } else { hitArea.style.left = '0'; } } if (display) { display.style.gap = `${userSettings.pageGap}px`; } // 3. [新增] 进度条位置设置 let progressBar = document.getElementById(PROGRESS_BAR_ID); if (!progressBar) { progressBar = document.createElement('div'); progressBar.id = PROGRESS_BAR_ID; document.body.appendChild(progressBar); } // 重置样式 progressBar.style.top = ''; progressBar.style.bottom = ''; progressBar.style.left = ''; progressBar.style.right = ''; progressBar.style.width = ''; progressBar.style.height = ''; progressBar.style.display = 'none'; const pos = userSettings.autoProgressPos || 'none'; if (pos !== 'none') { progressBar.style.display = 'block'; // 根据位置设置固定边和初始尺寸 if (pos === 'top') { progressBar.style.top = '0'; progressBar.style.left = '0'; progressBar.style.height = '3px'; progressBar.style.width = '0%'; } else if (pos === 'bottom') { progressBar.style.bottom = '0'; progressBar.style.left = '0'; progressBar.style.height = '3px'; progressBar.style.width = '0%'; } else if (pos === 'left') { progressBar.style.left = '0'; progressBar.style.top = '0'; progressBar.style.width = '3px'; progressBar.style.height = '0%'; } else if (pos === 'right') { progressBar.style.right = '0'; progressBar.style.top = '0'; progressBar.style.width = '3px'; progressBar.style.height = '0%'; } } } // 监听窗口大小变化以调整布局 window.addEventListener('resize', () => { requestAnimationFrame(applyLayoutSettings); }); // ========================================== // 数据获取与环境检测 // ========================================== function getArchiveId() { const params = new URLSearchParams(window.location.search); return params.get("id"); } async function initPageData() { if (typeof Reader !== 'undefined' && Reader.pages && Reader.pages.length > 0) { archiveData.pages = Reader.pages; archiveData.loaded = true; hookReaderFunctions(); return; } const id = getArchiveId(); if (id) { try { const response = await fetch(`/api/archives/${id}/files`); const data = await response.json(); if (data && data.pages) { archiveData.pages = data.pages; archiveData.loaded = true; } } catch (e) { console.error("[LRR Reading Mode] Failed to load page data", e); } } } function hookReaderFunctions(enableHook) { if (typeof Reader === 'undefined') return; if (enableHook === false) { if (originalApplyContainerWidth) { Reader.applyContainerWidth = originalApplyContainerWidth; originalApplyContainerWidth = null; } if (originalGoToPage) { Reader.goToPage = originalGoToPage; originalGoToPage = null; } return; } if (Reader.applyContainerWidth && !originalApplyContainerWidth) { originalApplyContainerWidth = Reader.applyContainerWidth; Reader.applyContainerWidth = function() { if (document.body.classList.contains(BODY_READING_CLASS)) { $(".reader-image, .sni").attr("style", ""); return; } return originalApplyContainerWidth.apply(this, arguments); }; } if (Reader.goToPage && !originalGoToPage) { originalGoToPage = Reader.goToPage.bind(Reader); Reader.goToPage = function(pageIndex) { originalGoToPage(pageIndex); if (!isReadingMode || !isReadingMode()) return; waitForPageImageLoaded(pageIndex).then((loaded) => { if (!isReadingMode || !isReadingMode()) return; if (!loaded) return; const info = getPageInfo(); updatePageIndicator(); updateGhosts(info); }); }; } } function getPageUrl(index) { if (typeof Reader !== 'undefined' && Reader.pages && Reader.pages[index]) return Reader.pages[index]; if (archiveData.loaded && archiveData.pages[index]) return archiveData.pages[index]; return null; } function isMangaMode() { if (typeof Reader !== 'undefined' && typeof Reader.mangaMode !== 'undefined') return Reader.mangaMode; try { return localStorage.getItem('mangaMode') === 'true'; } catch (e) { } return false; } function isDoublePageMode() { if (typeof Reader !== 'undefined' && typeof Reader.doublePageMode !== 'undefined') return Reader.doublePageMode; try { return localStorage.getItem('doublePageMode') === 'true'; } catch (e) { } return false; } function waitForPageImageLoaded(pageIndex, timeout = 5000) { const sessionId = typeof readingSessionId !== 'undefined' ? readingSessionId : 0; return new Promise((resolve) => { // 定义结束回调:无论成功失败,都关闭遮罩 const finish = (result) => { toggleLoadingMask(false); resolve(result); }; // 1. 环境与参数校验 if (!sessionId || sessionId !== readingSessionId || !document.body.classList.contains(BODY_READING_CLASS)) { finish(false); return; } const display = document.getElementById('display'); if (!display) { finish(false); return; } const targetUrl = getPageUrl(pageIndex); // 如果获取不到 URL(可能是最后一页或其他情况),直接结束 if (!targetUrl) { finish(true); return; } const mainImg = document.getElementById('img') || display.querySelector('img.reader-image'); if (!mainImg) { finish(false); return; } // 2. 核心判定逻辑:Src 匹配 + 加载完成 + 有宽度 const isTargetSrc = () => { const src = mainImg.currentSrc || mainImg.src || ''; // 简单的字符串包含匹配,兼容相对路径和绝对路径 return src === targetUrl || src.startsWith(targetUrl) || targetUrl.startsWith(src); }; // 3. 快速路径:如果当前图片已经是目标图片且已加载好(缓存命中) if (isTargetSrc() && mainImg.complete && mainImg.naturalWidth > 0) { finish(true); return; } // 4. 慢速路径:监听 load 事件 let done = false; const cleanup = () => { if (done) return; done = true; mainImg.removeEventListener('load', onLoad); mainImg.removeEventListener('error', onError); }; const onLoad = () => { // 再次检查环境,防止在等待期间退出了阅读模式 if (!sessionId || sessionId !== readingSessionId || !document.body.classList.contains(BODY_READING_CLASS)) { cleanup(); finish(false); return; } // 只有加载完的图片是目标图片才算成功 // 如果 Reader 预加载机制导致 img src 快速变化,这里能确保对齐 if (isTargetSrc()) { cleanup(); finish(true); } }; const onError = () => { cleanup(); finish(false); }; mainImg.addEventListener('load', onLoad); mainImg.addEventListener('error', onError); // 5. 超时保底:防止网络卡死导致遮罩永久不消失 setTimeout(() => { if (done) return; cleanup(); finish(false); }, timeout); }); } function getPageInfo() { if (typeof Reader !== 'undefined' && typeof Reader.currentPage === 'number') { return { current: Reader.currentPage + 1, total: Reader.pages ? Reader.pages.length : (archiveData.pages.length || 0), index: Reader.currentPage }; } const paginator = document.querySelector('.paginator') || document.querySelector('.current-page'); if (!paginator) return { current: 1, total: 1, index: 0 }; const m = (paginator.textContent || '').match(/(\d+)\s*\/\s*(\d+)/); if (m) return { current: parseInt(m[1]), total: parseInt(m[2]), index: parseInt(m[1]) - 1 }; return { current: 1, total: 1, index: 0 }; } // ========================================== // 核心工具函数 // ========================================== function toggleLoadingMask(active) { const mask = document.getElementById(LOADING_MASK_ID); if (!mask) return; if (active) mask.classList.add('visible'); else mask.classList.remove('visible'); } function clamp(val, min, max) { return Math.min(Math.max(val, min), max); } function looksLikeReader() { return /reader|read|id=/.test(location.href) || !!document.querySelector('#reader, .reader'); } function isReadingMode() { return document.body.classList.contains(BODY_READING_CLASS); } function checkIndicatorOverlap() { const indicator = document.getElementById(PAGE_INDICATOR_ID); const display = document.getElementById('display'); const img = display ? (display.querySelector('img.reader-image') || document.getElementById('img')) : null; if (!indicator || !img) return; const r1 = indicator.getBoundingClientRect(); const r2 = img.getBoundingClientRect(); let renderRect = { left: r2.left, right: r2.right, top: r2.top, bottom: r2.bottom }; if (img.naturalWidth && img.naturalHeight) { const imgRatio = img.naturalWidth / img.naturalHeight; const boxRatio = r2.width / r2.height; if (imgRatio < boxRatio) { const renderWidth = r2.height * imgRatio; const gap = (r2.width - renderWidth) / 2; renderRect.left = r2.left + gap; renderRect.right = r2.right - gap; } else if (imgRatio > boxRatio) { const renderHeight = r2.width / imgRatio; const gap = (r2.height - renderHeight) / 2; renderRect.top = r2.top + gap; renderRect.bottom = r2.bottom - gap; } } const noOverlap = ( r1.right < renderRect.left || r1.left > renderRect.right || r1.bottom < renderRect.top || r1.top > renderRect.bottom ); if (noOverlap) { indicator.classList.add('safe-zone'); } else { indicator.classList.remove('safe-zone'); } } function updatePageIndicator() { const info = getPageInfo(); const el = document.getElementById(PAGE_INDICATOR_ID); if (info && el) el.textContent = `${info.current} / ${info.total}`; if (isReadingMode()) { if (zoomState.scale > 1.01) { if (el) { el.classList.remove('visible'); el.classList.remove('safe-zone'); // 确保样式立即生效,防止闪烁 el.style.opacity = '0'; } return; } else { // 非缩放模式,恢复 opacity 控制权给 CSS 类 if (el) el.style.opacity = ''; } const display = document.getElementById('display'); const targetImg = display ? (display.querySelector('img.reader-image') || document.getElementById('img')) : null; if (imgResizeObserver && targetImg) { imgResizeObserver.disconnect(); imgResizeObserver.observe(targetImg); if(display) imgResizeObserver.observe(display); } requestAnimationFrame(checkIndicatorOverlap); // 显示页码 if (el) el.classList.add('visible'); if (indicatorHideTimer) clearTimeout(indicatorHideTimer); indicatorHideTimer = setTimeout(() => { if (el) el.classList.remove('visible'); }, 1000); updateGhosts(info); hookReaderFunctions(); } } // ========================================== // 自动翻页功能 // ========================================== function scheduleNextAutoTurn() { // 先清理旧的 if (autoTurnState.timer) clearTimeout(autoTurnState.timer); if (!autoTurnState.active) return; const intervalSec = Math.max(1, userSettings.autoTurnInterval || 3); const intervalMs = intervalSec * 1000; // 设置进度条动画 const progressBar = document.getElementById(PROGRESS_BAR_ID); const pos = userSettings.autoProgressPos || 'none'; if (progressBar && pos !== 'none') { // 1. 强制重置状态 (移除 transition) progressBar.style.transition = 'none'; // ---- 修复开始:确保在动画开始前,厚度(3px)是存在的 ---- if (pos === 'top' || pos === 'bottom') { progressBar.style.width = '0%'; progressBar.style.height = '3px'; // 强制恢复高度,防止被意外归零 } else { progressBar.style.height = '0%'; progressBar.style.width = '3px'; // 强制恢复宽度 } // ---- 修复结束 ---- // 2. 触发重绘 (Reflow),这是 CSS 动画重置的关键 void progressBar.offsetWidth; // 3. 开始新动画 const prop = (pos === 'top' || pos === 'bottom') ? 'width' : 'height'; progressBar.style.transition = `${prop} ${intervalSec}s linear`; if (pos === 'top' || pos === 'bottom') progressBar.style.width = '100%'; else progressBar.style.height = '100%'; } autoTurnState.timer = setTimeout(() => { if (!autoTurnState.active) return; const info = getPageInfo(); if (info.current < info.total) { executePageTurn('next'); } else { stopAutoTurn(); } }, intervalMs); } function startAutoTurn() { if (autoTurnState.active) return; autoTurnState.active = true; updateAutoTurnBtnState(); const indicator = document.getElementById(PAGE_INDICATOR_ID); if (indicator) { indicator.textContent = "▶ 自动翻页开启"; indicator.classList.add('visible'); setTimeout(() => { if(indicator.textContent === "▶ 自动翻页开启") { updatePageIndicator(); } }, 1500); } scheduleNextAutoTurn(); } function stopAutoTurn() { autoTurnState.active = false; if (autoTurnState.timer) { clearTimeout(autoTurnState.timer); autoTurnState.timer = null; } updateAutoTurnBtnState(); const progressBar = document.getElementById(PROGRESS_BAR_ID); if (progressBar) { progressBar.style.transition = 'none'; const pos = userSettings.autoProgressPos || 'none'; if (pos === 'top' || pos === 'bottom') { progressBar.style.width = '0%'; progressBar.style.height = '3px'; // 保持高度可见 } else if (pos === 'left' || pos === 'right') { progressBar.style.height = '0%'; progressBar.style.width = '3px'; // 保持宽度可见 } else { progressBar.style.width = '0'; progressBar.style.height = '0'; } } const indicator = document.getElementById(PAGE_INDICATOR_ID); if (indicator && isReadingMode()) { indicator.textContent = "■ 自动翻页停止"; indicator.classList.add('visible'); setTimeout(() => { if(indicator.textContent === "■ 自动翻页停止") { updatePageIndicator(); } }, 1000); } } function toggleAutoTurn() { if (autoTurnState.active) stopAutoTurn(); else startAutoTurn(); } function updateAutoTurnBtnState() { const btn = document.getElementById(AUTO_BTN_ID); if (!btn) return; btn.innerHTML = autoTurnState.active ? '■' : '▶'; if (autoTurnState.active) { btn.style.borderColor = '#4CAF50'; btn.style.color = '#4CAF50'; } else { btn.style.borderColor = 'rgba(255, 255, 255, 0.15)'; btn.style.color = 'rgba(255, 255, 255, 0.95)'; } } // ========================================== // 样式注入 // ========================================== function injectStyle() { if (document.getElementById(STYLE_ID)) return; const style = document.createElement('style'); style.id = STYLE_ID; style.textContent = ` body.${BODY_READING_CLASS} div.sni img { border-radius: 0 !important; } body.${BODY_READING_CLASS}, html.${BODY_READING_CLASS} { overflow: hidden !important; background: #000 !important; margin: 0 !important; padding: 0 !important; width: 100% !important; height: 100% !important; touch-action: none !important; overscroll-behavior: none !important; box-sizing: border-box !important; } /* 加载遮罩层样式 */ #${LOADING_MASK_ID} { position: fixed; inset: 0; background: rgba(0,0,0,0.3); /* 灰色遮罩 */ z-index: 199998; /* 低于按钮(200001)和热区(199999),但高于图片 */ display: none; pointer-events: auto; /* 阻止穿透点击触发翻页 */ cursor: wait; } body.${BODY_READING_CLASS} #${LOADING_MASK_ID}.visible { display: block; } #${PROGRESS_BAR_ID} { position: fixed; z-index: 200005; /* 最高层级 */ background-color: #4CAF50; /* 进度条颜色 */ pointer-events: none; opacity: 0.8; width: 0; height: 0; box-shadow: 0 0 4px rgba(76, 175, 80, 0.6); } body.${BODY_READING_CLASS} * { -webkit-tap-highlight-color: transparent !important; -webkit-touch-callout: none !important; -webkit-user-select: none !important; user-select: none !important; -webkit-user-drag: none !important; user-drag: none !important; } body.${BODY_READING_CLASS} header, body.${BODY_READING_CLASS} nav, body.${BODY_READING_CLASS} .navbar, body.${BODY_READING_CLASS} .paginator, body.${BODY_READING_CLASS} #footer, body.${BODY_READING_CLASS} #i4, body.${BODY_READING_CLASS} .id1, body.${BODY_READING_CLASS} .id2, body.${BODY_READING_CLASS} .id4, body.${BODY_READING_CLASS} #overlay-shade, body.${BODY_READING_CLASS} .absolute-options, body.${BODY_READING_CLASS} .file-info, body.${BODY_READING_CLASS} #i5, body.${BODY_READING_CLASS} #i7, body.${BODY_READING_CLASS} .sn { display: none !important; } body.${BODY_READING_CLASS} #archivePagesOverlay { display: none; z-index: 2147483647 !important; position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; pointer-events: auto !important; max-height: 90vh !important; overflow-y: auto !important; } body.${BODY_READING_CLASS} #archivePagesOverlay .quick-thumbnail, body.${BODY_READING_CLASS} #archivePagesOverlay .quick-thumbnail * { pointer-events: auto !important; cursor: pointer !important; } body.${BODY_READING_CLASS} #i3 { position: fixed !important; top: 0; left: 0; width: 100vw !important; height: 100vh !important; background: #000 !important; z-index: 1000 !important; display: flex !important; align-items: center; justify-content: center; overflow: visible !important; margin: 0 !important; padding: 0 !important; } body.${BODY_READING_CLASS} #display { display: flex !important; align-items: center !important; justify-content: center !important; width: 100vw !important; height: 100vh !important; position: relative !important; margin: 0 !important; padding: 0 !important; left: 0 !important; right: 0 !important; top: 0 !important; transform: translateX(0); /* 移除 will-change: transform,避免缩放时的模糊问题 */ cursor: grab; pointer-events: auto !important; overflow: visible !important; transform-origin: center center; /* 确保缩放中心 */ } body.${BODY_READING_CLASS} #display:active { cursor: grabbing; } body.${BODY_READING_CLASS} img.reader-image, body.${BODY_READING_CLASS} .lrr-ghost-img { position: static !important; max-width: 100vw !important; max-height: 100vh !important; width: auto !important; height: auto !important; object-fit: contain !important; margin: 0 !important; pointer-events: auto !important; background: #000; outline: none !important; display: block; transform: none !important; box-shadow: none !important; } body.${BODY_READING_CLASS} .sni { padding: 0 !important; margin: 0 !important; width: auto !important; max-width: none !important; display: contents !important; } .lrr-ghost-side { position: absolute; top: 0; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; background-color: #000; z-index: 1; pointer-events: none; } .lrr-ghost-left { left: -100vw; } .lrr-ghost-right { left: 100vw; } .lrr-ghost-container { display: flex; flex-direction: row; justify-content: center; align-items: center; width: 100%; height: 100%; } .lrr-ghost-container.single-view .lrr-ghost-img { max-width: 100vw !important; max-height: 100vh !important; } .lrr-ghost-container.double-view .lrr-ghost-img { max-width: 50vw !important; max-height: 100vh !important; } .lrr-ghost-text { color: #444; font-size: 20px; font-weight: bold; text-align: center; } /* 按钮通用样式 */ #${BUTTON_ID}, #${THUMB_BUTTON_ID}, #${AUTO_BTN_ID} { position: fixed; z-index: 200001; /* 必须高于热区(199999) */ width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 20px; background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); border: 1px solid rgba(255, 255, 255, 0.2); color: rgba(255, 255, 255, 0.95); text-shadow: 0 1px 2px rgba(0,0,0,0.6); box-shadow: 0 4px 12px rgba(0,0,0,0.3); /* 统一过渡动画 */ transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, visibility 0.3s; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; touch-action: manipulation; -webkit-tap-highlight-color: transparent; } @media (hover: hover) { #${BUTTON_ID}:hover, #${THUMB_BUTTON_ID}:hover, #${AUTO_BTN_ID}:hover { background: rgba(50, 50, 50, 0.9); border-color: rgba(255, 255, 255, 0.6); z-index: 200002; } } #${BUTTON_ID}:active, #${THUMB_BUTTON_ID}:active, #${AUTO_BTN_ID}:active { background: rgba(20, 20, 20, 0.9); transform: scale(0.95); } /* 默认隐藏子按钮,层级稍低但需高于热区 */ #${THUMB_BUTTON_ID}, #${AUTO_BTN_ID} { opacity: 0; pointer-events: none; visibility: hidden; } /* 阅读模式 */ body.${BODY_READING_CLASS} #${BUTTON_ID}, body.${BODY_READING_CLASS} #${THUMB_BUTTON_ID}, body.${BODY_READING_CLASS} #${AUTO_BTN_ID} { opacity: 0; pointer-events: none; visibility: hidden; } /* 非阅读模式 */ body:not(.${BODY_READING_CLASS}) #${BUTTON_ID} { opacity: 1 !important; pointer-events: auto !important; visibility: visible !important; transform: none !important; } body:not(.${BODY_READING_CLASS}) #${BUTTON_ID}.hide-on-scroll { transform: translateY(100px); opacity: 0 !important; } /* 热区样式 */ #${HITAREA_ID} { position: fixed; bottom: 0; z-index: 199999; width: 15vw; height: 15vh; background: transparent; pointer-events: none; } body.${BODY_READING_CLASS} #${HITAREA_ID} { pointer-events: auto; } /* 页码指示器 */ #${PAGE_INDICATOR_ID} { position: fixed; z-index: 200000; left: 50%; transform: translateX(-50%); bottom: calc(env(safe-area-inset-bottom, 0px) + 10px); background: rgba(0, 0, 0, 0.4); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); border: 1px solid rgba(255, 255, 255, 0.15); color: rgba(255, 255, 255, 0.95); text-shadow: 0 1px 2px rgba(0,0,0,0.6); box-shadow: 0 2px 8px rgba(0,0,0,0.3); padding: 1px 8px; border-radius: 20px; font-size: 13px; font-weight: 500; font-variant-numeric: tabular-nums; letter-spacing: 0.5px; pointer-events: none; opacity: 0; visibility: hidden; transition: opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1), visibility 0.5s; } body.${BODY_READING_CLASS} #${PAGE_INDICATOR_ID}.visible { opacity: 1 !important; visibility: visible !important; transition: opacity 0.1s ease-out; } body.${BODY_READING_CLASS} #${PAGE_INDICATOR_ID}.safe-zone { opacity: 1; visibility: visible; transition: opacity 0.3s ease-in; } @media (min-width: 960px) { #${PAGE_INDICATOR_ID} { left: auto; transform: none; right: 10px; bottom: 2px; } } @media (orientation: landscape) and (min-width: 720px) { #${PAGE_INDICATOR_ID} { left: auto; transform: none; right: 10px; bottom: 2px; } } .lrr-thumb-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 2147483646; backdrop-filter: blur(2px); } #${SETTINGS_MODAL_ID} { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.5); z-index: 200001; display: none; align-items: center; justify-content: center; backdrop-filter: blur(6px); -webkit-user-select: none !important; user-select: none !important; } .lrr-settings-content { display: flex; flex-direction: column; gap: 14px; background: var(--glass-bg, rgba(28,30,36,0.96)); color: var(--text-primary, #e3e9f3); width: 320px; max-width: calc(100vw - 32px); padding: 18px 18px 14px; border-radius: var(--radius-lg, 16px); border: 1px solid var(--glass-border, rgba(140,160,190,0.28)); box-shadow: var(--shadow-soft, 0 10px 28px -8px rgba(5,10,25,0.72)); font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; box-sizing: border-box; text-align: left; } .lrr-settings-header { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding-bottom: 10px; border-bottom: 1px solid rgba(120, 135, 160, 0.38); text-align: left; } .lrr-settings-close { cursor: pointer; font-size: 18px; line-height: 1; width: 24px; height: 24px; border-radius: 999px; display: flex; align-items: center; justify-content: center; color: var(--text-secondary, #a7b1c2); background: transparent; transition: var(--transition-smooth, all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)); } .lrr-settings-close:hover { background: rgba(255,255,255,0.06); color: var(--text-primary, #e3e9f3); } .lrr-settings-body { display: grid; grid-template-columns: 1fr; gap: 10px 16px; max-height: min(60vh, 420px); padding-right: 2px; margin-right: -4px; overflow-y: auto; } .lrr-setting-item { display: flex; flex-direction: column; gap: 4px; padding: 6px 0; text-align: left; } .lrr-setting-label-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; text-align: left; } .lrr-setting-label { font-size: 13px; color: var(--text-primary, #e3e9f3); text-align: left; } .lrr-setting-input-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 2px; text-align: left; } .lrr-switch { position: relative; display: inline-flex; align-items: center; width: 40px; height: 20px; } .lrr-switch input { opacity: 0; width: 0; height: 0; } .lrr-slider { position: absolute; cursor: pointer; inset: 0; background-color: #444b57; border-radius: 20px; transition: var(--transition-smooth, all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.4); } .lrr-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: #fafbff; border-radius: 50%; transition: var(--transition-smooth, all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)); box-shadow: 0 2px 6px rgba(0,0,0,0.45); } input:checked + .lrr-slider { background: linear-gradient(135deg, var(--accent-color, #4a9ff0), #6cc9ff); box-shadow: 0 0 0 1px rgba(74,159,240,0.6); } input:checked + .lrr-slider:before { transform: translateX(20px); } .lrr-select, .lrr-input { background: rgba(18, 20, 26, 0.9); color: var(--text-primary, #e3e9f3); border: 1px solid rgba(114, 132, 160, 0.7); padding: 5px 8px; border-radius: var(--radius-sm, 6px); width: 100%; box-sizing: border-box; font-size: 12px; outline: none; transition: var(--transition-smooth, all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)); text-align: left; } .lrr-select { text-align: center; text-align-last: center; } .lrr-select:focus, .lrr-input:focus { border-color: var(--accent-color, #4a9ff0); box-shadow: 0 0 0 1px rgba(74,159,240,0.65); background: rgba(22, 25, 32, 0.98); } .lrr-select::placeholder, .lrr-input::placeholder { color: rgba(160, 172, 192, 0.8); } .lrr-input[type="number"] { -moz-appearance: textfield; } .lrr-input[type="number"]::-webkit-inner-spin-button, .lrr-input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } @media (max-width: 480px) { .lrr-settings-content { width: calc(100vw - 24px); padding: 16px 14px 12px; border-radius: 14px; } .lrr-settings-body { max-height: min(65vh, 460px); grid-template-columns: 1fr; } } `; document.head.appendChild(style); } // ========================================== // 设置面板 // ========================================== function openSettingsModal() { let modal = document.getElementById(SETTINGS_MODAL_ID); const closeSettings = () => { if (modal) { modal.style.display = 'none'; document.body.style.userSelect = ''; document.body.style.webkitUserSelect = ''; } }; if (!modal) { modal = document.createElement('div'); modal.id = SETTINGS_MODAL_ID; modal.innerHTML = `
阅读模式设置 ×
自动进入阅读模式
自动翻页间隔 (秒)
自动翻页进度条位置
双页间距 (px)
按钮展开方向
阅读模式按钮位置
`; document.body.appendChild(modal); modal.querySelector('.lrr-settings-close').addEventListener('click', closeSettings); modal.addEventListener('click', (e) => { if (e.target === modal) closeSettings(); }); const elAuto = document.getElementById('lrr-cfg-auto'); const elPos = document.getElementById('lrr-cfg-pos'); const elGap = document.getElementById('lrr-cfg-gap'); const elExpand = document.getElementById('lrr-cfg-expand'); const elInterval = document.getElementById('lrr-cfg-interval'); const elProgress = document.getElementById('lrr-cfg-progress-pos'); elAuto.addEventListener('change', (e) => { userSettings.autoEnter = e.target.checked; saveSettings(); }); elPos.addEventListener('change', (e) => { userSettings.btnPosition = e.target.value; saveSettings(); }); elGap.addEventListener('change', (e) => { userSettings.pageGap = parseInt(e.target.value) || 0; saveSettings(); }); elInterval.addEventListener('change', (e) => { let val = parseFloat(e.target.value); if (isNaN(val) || val < 1) val = 1; userSettings.autoTurnInterval = val; saveSettings(); }); elExpand.addEventListener('change', (e) => { userSettings.expandDirection = e.target.value; saveSettings(); }); // [新增] 监听进度条位置变更 elProgress.addEventListener('change', (e) => { userSettings.autoProgressPos = e.target.value; saveSettings(); }); } // 同步 UI document.getElementById('lrr-cfg-auto').checked = userSettings.autoEnter; document.getElementById('lrr-cfg-pos').value = userSettings.btnPosition; document.getElementById('lrr-cfg-gap').value = userSettings.pageGap; document.getElementById('lrr-cfg-interval').value = userSettings.autoTurnInterval || 3; document.getElementById('lrr-cfg-expand').value = userSettings.expandDirection || 'up'; document.getElementById('lrr-cfg-progress-pos').value = userSettings.autoProgressPos || 'none'; modal.style.display = 'flex'; document.body.style.userSelect = 'none'; document.body.style.webkitUserSelect = 'none'; } // ========================================== // 交互控制 // ========================================== // 热区触发入口 function handleZoneTrigger() { if (Date.now() < inputBlockUntil) return; showControls(); } // 显示控件并执行展开动画 function showControls() { if (!isReadingMode()) return; const mainBtn = document.getElementById(BUTTON_ID); const thumbBtn = document.getElementById(THUMB_BUTTON_ID); const autoBtn = document.getElementById(AUTO_BTN_ID); const buttons = [mainBtn, thumbBtn, autoBtn]; // 1. 设置可见性、重置动画曲线 buttons.forEach(btn => { if (!btn) return; btn.style.visibility = 'visible'; btn.style.zIndex = '200001'; btn.style.transition = 'transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, visibility 0.3s'; btn.style.pointerEvents = 'auto'; btn.style.opacity = '1'; }); // 2. 计算展开位置 const step = 63; const dir = userSettings.expandDirection; const pos = userSettings.btnPosition; if (mainBtn) mainBtn.style.transform = 'translate(0, 0) scale(1)'; [thumbBtn, autoBtn].forEach((btn, index) => { if (!btn) return; const distance = step * (index + 1); let x = 0, y = 0; if (dir === 'side') { x = (pos === 'right') ? -distance : distance; } else { y = -distance; } btn.style.transform = `translate(${x}px, ${y}px) scale(1)`; }); // 3. 重置隐藏定时器 if (btnHideTimer) clearTimeout(btnHideTimer); btnHideTimer = setTimeout(hideControls, 2500); } // 隐藏控件并收回动画 function hideControls() { if (!isReadingMode()) return; const btns = [ document.getElementById(BUTTON_ID), document.getElementById(THUMB_BUTTON_ID), document.getElementById(AUTO_BTN_ID) ]; btns.forEach(btn => { if (!btn) return; // --- 设置收起专用动画 --- btn.style.transition = 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.15s ease-out, visibility 0.3s'; // 执行收回 btn.style.transform = 'translate(0, 0) scale(0.8)'; btn.style.opacity = '0'; btn.style.pointerEvents = 'none'; }); // 等待最长的动画时间 (0.3s) 结束后完全隐藏 setTimeout(() => { btns.forEach(btn => { if (btn && btn.style.opacity === '0') { btn.style.visibility = 'hidden'; } }); }, 300); } function preventContextMenu(e) { e.preventDefault(); e.stopPropagation(); return false; } function handleTouchCapture(e) { if (!isReadingMode()) return; if (e.target.closest('#archivePagesOverlay') || e.target.closest('.lrr-thumb-backdrop')) return; const t = e.target; if (t && (t.closest('#i3') || t.closest('#display') || t.tagName === 'IMG' || t.closest('.paginator'))) { } } function handleCaptureClick(e) { if (!isReadingMode()) return; if (e.target.closest('#archivePagesOverlay') || e.target.closest('.lrr-thumb-backdrop')) { return; } if (e.target.closest(`#${BUTTON_ID}`) || e.target.closest(`#${THUMB_BUTTON_ID}`) || e.target.closest(`#${AUTO_BTN_ID}`) || e.target.closest(`#${HITAREA_ID}`) || e.target.closest(`#${SETTINGS_MODAL_ID}`)) { return; } const inReaderContainer = e.target.closest('#i3'); const inDisplay = e.target.closest('#display'); const isImg = e.target.tagName === 'IMG'; if (inReaderContainer || inDisplay || isImg) { e.stopPropagation(); } } function handleKeydownBlock(e) { if (!isReadingMode()) return; const blockKeys = ['ArrowLeft', 'ArrowRight', 'PageUp', 'PageDown', ' ', 'Spacebar', 'a', 'A', 'd', 'D']; if (blockKeys.includes(e.key)) { e.preventDefault(); e.stopPropagation(); } } function handleDragStart(e) { if (isReadingMode() && (e.target.closest('#display') || e.target.tagName === 'IMG')) e.preventDefault(); } function handleScroll() { if (isReadingMode()) return; const btn = document.getElementById(BUTTON_ID); if (!btn) return; const st = window.pageYOffset || document.documentElement.scrollTop; if (st > lastScrollTop && st > 100) { btn.classList.add('hide-on-scroll'); } else { btn.classList.remove('hide-on-scroll'); } lastScrollTop = st <= 0 ? 0 : st; } function setupClickBlocker(enable) { if (enable) { window.addEventListener('click', handleCaptureClick, true); window.addEventListener('contextmenu', preventContextMenu, true); window.addEventListener('dragstart', handleDragStart, { passive: false }); window.addEventListener('keydown', handleKeydownBlock, true); window.removeEventListener('scroll', handleScroll); } else { window.removeEventListener('click', handleCaptureClick, true); window.removeEventListener('contextmenu', preventContextMenu, true); window.removeEventListener('dragstart', handleDragStart); window.removeEventListener('touchstart', handleTouchCapture, true); window.removeEventListener('touchend', handleTouchCapture, true); window.removeEventListener('touchcancel', handleTouchCapture, true); window.removeEventListener('keydown', handleKeydownBlock, true); window.addEventListener('scroll', handleScroll); } } // -------- 缩略图面板与遮罩 -------- function ensureThumbBackdrop() { let bd = document.querySelector('.lrr-thumb-backdrop'); if (bd) return bd; bd = document.createElement('div'); bd.className = 'lrr-thumb-backdrop'; bd.addEventListener('click', closeThumbnailOverlay); document.body.appendChild(bd); return bd; } function removeThumbBackdrop() { const bd = document.querySelector('.lrr-thumb-backdrop'); if (bd) bd.remove(); } function closeThumbnailOverlay() { const overlay = document.getElementById('archivePagesOverlay'); if (overlay) overlay.style.display = 'none'; removeThumbBackdrop(); if (typeof LRR !== 'undefined' && LRR.closeOverlay) LRR.closeOverlay(); } function openThumbnailOverlay() { const nativeBtn = document.getElementById('toggle-archive-overlay'); if (nativeBtn) nativeBtn.click(); else document.dispatchEvent(new KeyboardEvent('keydown', { key: 'q', code: 'KeyQ', bubbles: true })); setTimeout(() => { const overlay = document.getElementById('archivePagesOverlay'); if (overlay) { overlay.style.setProperty('display', 'block', 'important'); ensureThumbBackdrop(); if (!overlay._hasDelegatedClick) { overlay.addEventListener('click', function(e) { const thumb = e.target.closest('.quick-thumbnail'); if (thumb) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); const pageAttr = thumb.getAttribute('page'); if (pageAttr !== null) { const pageIndex = parseInt(pageAttr, 10); if (typeof Reader !== 'undefined' && Reader.goToPage) { Reader.goToPage(pageIndex); } closeThumbnailOverlay(); waitForPageImageLoaded(pageIndex).then(() => { const info = getPageInfo(); updatePageIndicator(); updateGhosts(info); }); return; } } if (e.target.closest('.id3') || e.target.tagName === 'A' || e.target.tagName === 'IMG') { if (!thumb) { setTimeout(closeThumbnailOverlay, 50); } } }, true); overlay._hasDelegatedClick = true; } } }, 100); } // -------- 翻页逻辑 -------- function exitFromLastPageWithAnimation(display, diffSign) { const width = window.innerWidth || 1; const finalDiff = diffSign >= 0 ? width : -width; display.style.transition = 'transform 0.25s ease-out, opacity 0.25s ease-out'; display.style.transform = `translateX(${finalDiff}px)`; display.style.opacity = '0.0'; setTimeout(() => { setReadingMode(false); setTimeout(() => { display.style.transform = ''; display.style.opacity = ''; display.style.transition = ''; }, 300); }, 250); } function executePageTurn(intent) { // 1. 时间间隔检查 const now = Date.now(); if (now - lastTurnTime < 150) return; lastTurnTime = now; // [新增] 逻辑核心:如果开启了自动翻页,无论这次是由自动触发还是手动触发, // 都立即重置当前的计时器和进度条,给用户“重置”的反馈。 if (autoTurnState.active) { if (autoTurnState.timer) clearTimeout(autoTurnState.timer); // 立即重置进度条动画到 0 const progressBar = document.getElementById(PROGRESS_BAR_ID); const pos = userSettings.autoProgressPos || 'none'; if (progressBar && pos !== 'none') { progressBar.style.transition = 'none'; if (pos === 'top' || pos === 'bottom') progressBar.style.width = '0%'; else progressBar.style.height = '0%'; } } // 2. 立即开启遮罩,防止后续操作和连点 toggleLoadingMask(true); const manga = isMangaMode(); const canUseReader = (typeof Reader !== 'undefined') && Reader && typeof Reader.changePage === 'function'; // 3. 执行翻页动作 if (canUseReader) { let dir = 0; if (intent === 'next') dir = manga ? -1 : 1; else dir = manga ? 1 : -1; Reader.changePage(dir, true); } else { let action = ''; if (intent === 'next') action = manga ? 'left' : 'right'; else action = manga ? 'right' : 'left'; const link = document.querySelector(`.page-link[value="${action}"]`); if (link) link.click(); else { const key = action === 'left' ? 'ArrowLeft' : 'ArrowRight'; const keyCode = action === 'left' ? 37 : 39; document.dispatchEvent(new KeyboardEvent('keydown', { key: key, keyCode: keyCode, which: keyCode, bubbles: true })); } } // 4. 延迟检测加载状态 setTimeout(() => { updatePageIndicator(); attachDragEvents(); // 获取下一页的索引用于检测 const info = getPageInfo(); // 如果自动翻页开启,继续调度(此时会重新开始进度条动画) if (autoTurnState.active) { scheduleNextAutoTurn(); } // 开始等待图片加载,加载完后会自动关闭遮罩 waitForPageImageLoaded(info.index).then(() => { // 动画优化:确保位置归位 const display = document.getElementById('display'); if (display) { display.style.transition = 'none'; display.style.transform = 'translateX(0px)'; void display.offsetWidth; display.style.transition = 'transform 0.2s ease-out'; } }); }, 150); } function handleClickFlip(imgEl, startX, info) { let rectWidth = window.innerWidth; let rectLeft = 0; if (imgEl && imgEl.tagName === 'IMG') { const rect = imgEl.getBoundingClientRect(); rectWidth = rect.width; rectLeft = rect.left; } const ratio = (startX - rectLeft) / rectWidth; let intent = null; const manga = isMangaMode(); if (ratio > 0.65) intent = manga ? 'prev' : 'next'; else if (ratio < 0.35) intent = manga ? 'next' : 'prev'; else return false; if (intent === 'prev' && info.current === 1) return false; if (intent === 'next' && info.current === info.total) { exitFromLastPageWithAnimation(document.getElementById('display'), 1); return true; } executePageTurn(intent); return true; } function getCurrentPageIndices(info) { const pages = (typeof Reader !== 'undefined' && Reader.pages) || archiveData.pages || []; const imgs = document.querySelectorAll('#display img.reader-image'); const result = []; if (pages.length === 0) { if (typeof info.index === 'number') return [info.index]; return []; } imgs.forEach(img => { const src = img.currentSrc || img.src || ''; let found = -1; for (let i = 0; i < pages.length; i++) { const pageUrl = pages[i]; if (!pageUrl) continue; if (src === pageUrl || src.startsWith(pageUrl) || pageUrl.startsWith(src)) { found = i; break; } } if (found >= 0 && !result.includes(found)) { result.push(found); } }); if (result.length === 0 && typeof info.index === 'number') { return [info.index]; } return result.sort((a, b) => a - b); } function resolveGroupStart(minIdx) { if (minIdx <= 0) return 0; if (minIdx === 1 || minIdx === 2) return 1; if (minIdx % 2 === 0) return minIdx - 1; return minIdx; } function updateGhosts(info) { const display = document.getElementById('display'); if (!display) return; let leftGhost = display.querySelector('.lrr-ghost-left'); let rightGhost = display.querySelector('.lrr-ghost-right'); if (!leftGhost) { leftGhost = document.createElement('div'); leftGhost.className = 'lrr-ghost-side lrr-ghost-left'; display.appendChild(leftGhost); } if (!rightGhost) { rightGhost = document.createElement('div'); rightGhost.className = 'lrr-ghost-side lrr-ghost-right'; display.appendChild(rightGhost); } leftGhost.innerHTML = ''; rightGhost.innerHTML = ''; const mainImg = document.getElementById('img') || display.querySelector('img.reader-image'); let inheritedStyle = ''; let baseWidth = null; let baseHeight = null; if (mainImg) { inheritedStyle = mainImg.getAttribute('style') || ''; const rect = mainImg.getBoundingClientRect(); baseWidth = rect.width; baseHeight = rect.height; } const manga = isMangaMode(); const double = isDoublePageMode(); const idx = info.index; const total = info.total; let logicalNextIndices = []; let logicalPrevIndices = []; let nextLimit = 10; try { const storedLimit = parseInt(localStorage.getItem('preloadCount') || '10', 10); if (!isNaN(storedLimit)) { nextLimit = Math.min(10, Math.max(1, storedLimit)); } } catch(e) {} if (double) { const currentIndices = getCurrentPageIndices(info); if (currentIndices.length === 0) { if (idx === 0) { let nextCursor = 1; while (logicalNextIndices.length < nextLimit && nextCursor < total) { logicalNextIndices.push(nextCursor); nextCursor++; } } else if (idx === 1) { logicalPrevIndices = [0]; let nextCursor = idx + 2; while (logicalNextIndices.length < nextLimit && nextCursor < total) { logicalNextIndices.push(nextCursor); nextCursor++; } } else { logicalPrevIndices = [idx - 2, idx - 1].filter(i => i >= 0); let nextCursor = idx + 2; while (logicalNextIndices.length < nextLimit && nextCursor < total) { logicalNextIndices.push(nextCursor); nextCursor++; } } } else { const minIdx = currentIndices[0]; const groupStart = resolveGroupStart(minIdx); if (groupStart > 0) { const prevStart = groupStart - 2; if (prevStart <= 0) { logicalPrevIndices = [0]; } else { const p1 = prevStart; const p2 = prevStart + 1; if (p1 >= 0 && p1 < total) logicalPrevIndices.push(p1); if (p2 >= 0 && p2 < total) logicalPrevIndices.push(p2); } } const nextStart = groupStart === 0 ? 1 : groupStart + 2; if (nextStart < total) { if (nextStart === 0) { logicalNextIndices = [0]; } else { let cursor = nextStart; while(logicalNextIndices.length < nextLimit && cursor < total) { if (cursor >= 0) logicalNextIndices.push(cursor); cursor++; } } } } } else { let cursor = idx + 1; while(logicalNextIndices.length < nextLimit && cursor < total) { logicalNextIndices.push(cursor); cursor++; } if (idx - 1 >= 0) logicalPrevIndices = [idx - 1]; } let leftPages = manga ? logicalNextIndices : logicalPrevIndices; let rightPages = manga ? logicalPrevIndices : logicalNextIndices; const createGhostContent = (indices) => { const validIndices = indices.filter(i => i >= 0 && i < total); if (validIndices.length === 0) return `
`; let html = ''; const visibleCount = double ? 2 : 1; const isVisualDouble = double && validIndices.length > 1; const containerClass = isVisualDouble ? 'double-view' : 'single-view'; html += `
`; let sortedIndices = [...validIndices]; if (manga && isVisualDouble) { sortedIndices.sort((a, b) => b - a); } else { sortedIndices.sort((a, b) => a - b); } sortedIndices.forEach((pageIdx, loopIndex) => { const url = getPageUrl(pageIdx); if (!url) return; let sizeStyle = ''; if (baseWidth && baseHeight) { sizeStyle = `width:${baseWidth}px; height:${baseHeight}px; max-width:none; max-height:none;`; } let visibilityStyle = ''; if (loopIndex >= visibleCount) { visibilityStyle = 'display: none !important;'; } const onErrorScript = "this.onerror=null; setTimeout(()=>{ const curr = this.src; this.src = ''; this.src = curr; }, 1000);"; html += ``; }); html += `
`; return html; }; leftGhost.innerHTML = createGhostContent(leftPages); rightGhost.innerHTML = createGhostContent(rightPages); } // ========================================== // 拖拽事件处理 // ========================================== // 应用缩放和位移 function applyZoomTransform(display, tempDiffX = 0, tempDiffY = 0) { if (!display) return; // 获取当前视口尺寸 const vw = window.innerWidth || document.documentElement.clientWidth; const vh = window.innerHeight || document.documentElement.clientHeight; // 计算当前缩放下的最大允许位移量 (边界限制) const maxPanX = Math.max(0, (vw * zoomState.scale - vw) / 2); const maxPanY = Math.max(0, (vh * zoomState.scale - vh) / 2); // 计算目标位置(基础偏移 + 临时拖拽偏移) let targetX = zoomState.panX + tempDiffX; let targetY = zoomState.panY + tempDiffY; // 强制限制在边界内 targetX = clamp(targetX, -maxPanX, maxPanX); targetY = clamp(targetY, -maxPanY, maxPanY); if (zoomState.scale > 1.01) { display.style.transform = `translate(${targetX}px, ${targetY}px) scale(${zoomState.scale})`; } else { if (tempDiffX !== 0 && zoomState.scale === 1) { display.style.transform = `translateX(${tempDiffX}px)`; } else { display.style.transform = `translateX(0px)`; } } return { x: targetX, y: targetY }; // 返回修正后的坐标供状态更新 } function resetZoom() { zoomState.scale = 1; zoomState.panX = 0; zoomState.panY = 0; const display = document.getElementById('display'); if (display) { display.style.transition = `transform ${ZOOM_ANIM_SPEED} ease-out`; display.style.transform = 'translateX(0px)'; } const el = document.getElementById(PAGE_INDICATOR_ID); if (el) { el.style.opacity = ''; } } function handleDblClick(e) { if (!isReadingMode()) return; // 兼容 TouchEvent 和 MouseEvent 获取坐标 let clientX; if (e.type === 'touchstart' && e.touches.length > 0) { clientX = e.touches[0].clientX; } else if (e.clientX) { clientX = e.clientX; } else { clientX = window.innerWidth / 2; } // 逻辑:已经放大时 -> 缩小 if (zoomState.scale > 1.01) { if (e.preventDefault) e.preventDefault(); resetZoom(); return; } let rectWidth = window.innerWidth; let rectLeft = 0; const display = document.getElementById('display'); const imgEl = display ? (display.querySelector('img.reader-image') || document.getElementById('img')) : null; if (imgEl && imgEl.tagName === 'IMG') { const rect = imgEl.getBoundingClientRect(); rectWidth = rect.width; rectLeft = rect.left; } const ratio = (clientX - rectLeft) / rectWidth; // 逻辑:双击中间 -> 放大 if (ratio >= 0.35 && ratio <= 0.65) { if (e.preventDefault) e.preventDefault(); zoomState.scale = 2.5; zoomState.panX = 0; zoomState.panY = 0; const ind = document.getElementById(PAGE_INDICATOR_ID); if (ind) { ind.classList.remove('visible'); ind.style.opacity = '0'; } requestAnimationFrame(() => { if (display) display.style.transition = `transform ${ZOOM_ANIM_SPEED} ease-out`; applyZoomTransform(display); }); } } // 桌面端滚轮缩放 function handleWheel(e) { if (!isReadingMode()) return; e.preventDefault(); const display = document.getElementById('display'); if (!display) return; const delta = e.deltaY * -0.002; let newScale = zoomState.scale + delta; if (newScale < 1) newScale = 1; if (newScale > 5) newScale = 5; if (Math.abs(newScale - zoomState.scale) > 0.01) { zoomState.scale = newScale; if (zoomState.scale <= 1.01) { zoomState.scale = 1; zoomState.panX = 0; zoomState.panY = 0; resetZoom(); } else { const ind = document.getElementById(PAGE_INDICATOR_ID); if (ind) { ind.classList.remove('visible'); ind.style.opacity = '0'; } } display.style.transition = 'transform 0.1s ease-out'; const corrected = applyZoomTransform(display); zoomState.panX = corrected.x; zoomState.panY = corrected.y; } } // 拖拽与交互事件绑定 function attachDragEvents() { const display = document.getElementById('display'); if (!display) return; let lastTapTime = 0; if (display._lrrDragBound) { display.removeEventListener('wheel', handleWheel); display.addEventListener('wheel', handleWheel, { passive: false }); display.removeEventListener('dblclick', handleDblClick); display.addEventListener('dblclick', handleDblClick); return; } display.addEventListener('wheel', handleWheel, { passive: false }); display.addEventListener('dblclick', handleDblClick); const start = (e) => { if (!isReadingMode()) return; if (e.touches && e.touches.length > 2) return; const overlay = document.getElementById('archivePagesOverlay'); if (overlay && overlay.style.display === 'block') return; if (!e.target.closest('#display')) return; // --- 兼容变量:Touch 双击检测 --- if (e.type === 'touchstart' && e.touches.length === 1) { const currentTime = Date.now(); const tapLength = currentTime - lastTapTime; // 使用 DOUBLE_TAP_DELAY 变量 if (tapLength < DOUBLE_TAP_DELAY && tapLength > 0) { e.preventDefault(); handleDblClick(e); lastTapTime = 0; dragState.active = false; return; } lastTapTime = currentTime; } if (autoTurnState.active && autoTurnState.timer) { clearTimeout(autoTurnState.timer); } dragState.active = true; dragState.targetImg = e.target; dragState.isTouch = e.type.startsWith('touch'); // 拖拽开始时移除动画,保证跟手 display.style.transition = 'none'; if (dragState.isTouch && e.touches.length === 2) { const t1 = e.touches[0]; const t2 = e.touches[1]; zoomState.initialDistance = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY); zoomState.initialScale = zoomState.scale; return; } const clientX = dragState.isTouch ? e.touches[0].clientX : e.clientX; const clientY = dragState.isTouch ? e.touches[0].clientY : e.clientY; dragState.startX = clientX; dragState.currentX = clientX; dragState.startY = clientY; dragState.currentY = clientY; dragState.startPanX = zoomState.panX; dragState.startPanY = zoomState.panY; if (dragState.rafId) cancelAnimationFrame(dragState.rafId); dragState.rafId = null; }; const move = (e) => { if (!dragState.active) return; if (dragState.isTouch && e.cancelable && (!e.touches || e.touches.length < 2)) { e.preventDefault(); } if (dragState.isTouch && e.touches && e.touches.length === 2) { if (e.cancelable) e.preventDefault(); const t1 = e.touches[0]; const t2 = e.touches[1]; const currentDist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY); if (zoomState.initialDistance > 0) { let newScale = zoomState.initialScale * (currentDist / zoomState.initialDistance); if (newScale < 1) newScale = 1; if (newScale > 5) newScale = 5; zoomState.scale = newScale; if (newScale > 1.01) { const ind = document.getElementById(PAGE_INDICATOR_ID); if (ind) { ind.classList.remove('visible'); ind.style.opacity = '0'; } } requestAnimationFrame(() => { const corrected = applyZoomTransform(display); }); } return; } const clientX = dragState.isTouch ? e.touches[0].clientX : e.clientX; const clientY = dragState.isTouch ? e.touches[0].clientY : e.clientY; dragState.currentX = clientX; dragState.currentY = clientY; if (!dragState.rafId) { dragState.rafId = requestAnimationFrame(() => { const diffX = dragState.currentX - dragState.startX; const diffY = dragState.currentY - dragState.startY; if (zoomState.scale > 1.05) { const estimatedPanX = dragState.startPanX + diffX; const estimatedPanY = dragState.startPanY + diffY; applyZoomTransform(display, estimatedPanX - zoomState.panX, estimatedPanY - zoomState.panY); } else { const width = window.innerWidth || 1; let ratio = diffX / width; if (ratio > MAX_DRAG_RATIO) ratio = MAX_DRAG_RATIO; if (ratio < -MAX_DRAG_RATIO) ratio = -MAX_DRAG_RATIO; display.style.transform = `translateX(${ratio * width}px)`; } dragState.rafId = null; }); } }; const end = (e) => { if (!dragState.active) return; dragState.active = false; if (dragState.rafId) cancelAnimationFrame(dragState.rafId); dragState.rafId = null; if (autoTurnState.active) startAutoTurn(); // --- 放大模式结束逻辑 --- if (zoomState.scale > 1.05) { const diffX = dragState.currentX - dragState.startX; const diffY = dragState.currentY - dragState.startY; let finalX = dragState.startPanX + diffX; let finalY = dragState.startPanY + diffY; const vw = window.innerWidth; const vh = window.innerHeight; const maxPanX = Math.max(0, (vw * zoomState.scale - vw) / 2); const maxPanY = Math.max(0, (vh * zoomState.scale - vh) / 2); zoomState.panX = clamp(finalX, -maxPanX, maxPanX); zoomState.panY = clamp(finalY, -maxPanY, maxPanY); display.style.transition = `transform ${ZOOM_ANIM_SPEED} cubic-bezier(0.25, 0.46, 0.45, 0.94)`; applyZoomTransform(display); return; } if (zoomState.scale !== 1) { resetZoom(); } const width = window.innerWidth || 1; const gesture = dragState.currentX - dragState.startX; const absRatio = Math.abs(gesture) / width; const info = getPageInfo(); const resetPosition = () => { requestAnimationFrame(() => { display.style.transition = 'transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1)'; display.style.transform = 'translateX(0px)'; }); }; if (absRatio < CLICK_THRESHOLD_RATIO) { resetPosition(); const didFlip = handleClickFlip(dragState.targetImg, dragState.startX, info); if (dragState.isTouch && didFlip && e.cancelable) e.preventDefault(); return; } if (absRatio < SWIPE_THRESHOLD_RATIO) { resetPosition(); return; } let intent; if (isMangaMode()) { intent = gesture > 0 ? 'next' : 'prev'; } else { intent = gesture < 0 ? 'next' : 'prev'; } if (intent === 'prev' && info.current === 1) { resetPosition(); return; } if (intent === 'next' && info.current === info.total) { exitFromLastPageWithAnimation(display, gesture < 0 ? -1 : 1); return; } requestAnimationFrame(() => { display.style.transition = 'transform 0.2s ease-out'; const exitX = (gesture < 0 ? -1 : 1) * width; display.style.transform = `translateX(${exitX}px)`; }); setTimeout(() => { const nextIndex = intent === 'next' ? info.index + 1 : info.index - 1; executePageTurn(intent); waitForPageImageLoaded(nextIndex, 2000).then(() => { display.style.transition = 'none'; display.style.transform = 'translateX(0px)'; void display.offsetWidth; display.style.transition = 'transform 0.2s ease-out'; }); }, 200); }; display.addEventListener('mousedown', start); window.addEventListener('mousemove', move); window.addEventListener('mouseup', end); display.addEventListener('touchstart', start, { passive: false }); window.addEventListener('touchmove', move, { passive: false }); window.addEventListener('touchend', end); display._lrrDragBound = true; display._lrrDragHandlers = { start, move, end }; } function detachDragEvents() { const display = document.getElementById('display'); if (!display || !display._lrrDragBound || !display._lrrDragHandlers) return; const h = display._lrrDragHandlers; display.removeEventListener('mousedown', h.start); window.removeEventListener('mousemove', h.move); window.removeEventListener('mouseup', h.end); display.removeEventListener('touchstart', h.start); window.removeEventListener('touchmove', h.move); window.removeEventListener('touchend', h.end); display.removeEventListener('wheel', handleWheel); display.removeEventListener('dblclick', handleDblClick); // 解绑双击 display.style.transform = ''; display.style.transition = ''; display._lrrDragBound = false; delete display._lrrDragHandlers; // 重置缩放状态 zoomState = { scale: 1, panX: 0, panY: 0, initialDistance: 0, initialScale: 1 }; const ghosts = display.querySelectorAll('.lrr-ghost-side'); ghosts.forEach(el => el.remove()); } function setReadingMode(enable) { document.body.classList.toggle(BODY_READING_CLASS, enable); document.documentElement.classList.toggle(BODY_READING_CLASS, enable); const btn = document.getElementById(BUTTON_ID); const tb = document.getElementById(THUMB_BUTTON_ID); const autoBtn = document.getElementById(AUTO_BTN_ID); if (btn) { btn.innerHTML = enable ? '✕' : '⌘'; btn.style.borderColor = userSettings.autoEnter ? '#4CAF50' : 'rgba(255,255,255,0.3)'; btn.classList.remove('hide-on-scroll'); } if (enable) { inputBlockUntil = Date.now() + 500; hookReaderFunctions(true); applyLayoutSettings(); initPageData(); // 进入阅读模式时 [btn, tb, autoBtn].forEach(el => { if (el) { el.style.transition = 'transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, visibility 0.3s'; el.style.opacity = '0'; el.style.pointerEvents = 'none'; el.style.visibility = 'hidden'; el.style.transform = 'translate(0, 0) scale(1)'; } }); if (btnHideTimer) clearTimeout(btnHideTimer); updatePageIndicator(); attachDragEvents(); setupClickBlocker(true); const mainImg = document.getElementById('img'); const dblImg = document.getElementById('img_doublepage'); [mainImg, dblImg].forEach(el => { if (el) el.removeAttribute('style'); }); if (imgResizeObserver) imgResizeObserver.disconnect(); const display = document.getElementById('display'); const targetImg = display ? (display.querySelector('img.reader-image') || document.getElementById('img')) : null; if (window.ResizeObserver && targetImg) { imgResizeObserver = new ResizeObserver(() => { requestAnimationFrame(checkIndicatorOverlap); }); imgResizeObserver.observe(targetImg); if(display) imgResizeObserver.observe(display); } updateGhosts(getPageInfo()); setTimeout(checkIndicatorOverlap, 50); } else { // --- 退出阅读模式逻辑 --- stopAutoTurn(); if (btnHideTimer) clearTimeout(btnHideTimer); if (imgResizeObserver) { imgResizeObserver.disconnect(); imgResizeObserver = null; } // 1. 主按钮 if (btn) { btn.style.transition = ''; // 清除内联 transition,使用 CSS 默认 btn.style.opacity = '1'; btn.style.pointerEvents = 'auto'; btn.style.visibility = 'visible'; btn.style.transform = ''; } // 2. 子按钮 [tb, autoBtn].forEach(el => { if (el) { el.style.transition = 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.15s ease-out, visibility 0.3s'; // 执行收回动作 el.style.transform = 'translate(0, 0) scale(0.8)'; el.style.opacity = '0'; el.style.pointerEvents = 'none'; // 不立即设置 visibility: hidden,让 CSS transition 处理完 } }); detachDragEvents(); setupClickBlocker(false); closeThumbnailOverlay(); hookReaderFunctions(false); if (typeof Reader !== 'undefined' && typeof Reader.applyContainerWidth === 'function') { Reader.applyContainerWidth(); } } } // -------- 初始化 -------- function createControls() { if (document.getElementById(BUTTON_ID)) return; const btn = document.createElement('div'); btn.id = BUTTON_ID; btn.innerHTML = '⌘'; btn.title = '切换阅读模式 (长按设置)'; btn.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); return false; }); btn.addEventListener('click', (e) => { e.stopPropagation(); setReadingMode(!isReadingMode()); }); let pressTimer; const startPress = () => pressTimer = setTimeout(openSettingsModal, 800); const cancelPress = () => clearTimeout(pressTimer); btn.addEventListener('mousedown', startPress); btn.addEventListener('mouseup', cancelPress); btn.addEventListener('mouseleave', cancelPress); btn.addEventListener('touchstart', startPress); btn.addEventListener('touchend', cancelPress); const thumbBtn = document.createElement('div'); thumbBtn.id = THUMB_BUTTON_ID; thumbBtn.innerHTML = '☰'; thumbBtn.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); return false; }); thumbBtn.addEventListener('click', (e) => { e.stopPropagation(); openThumbnailOverlay(); }); const autoBtn = document.createElement('div'); autoBtn.id = AUTO_BTN_ID; autoBtn.innerHTML = '▶'; autoBtn.title = '开启/关闭自动翻页'; autoBtn.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); return false; }); autoBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleAutoTurn(); }); const indicator = document.createElement('div'); indicator.id = PAGE_INDICATOR_ID; // 唯一的底部热区 const hitArea = document.createElement('div'); hitArea.id = HITAREA_ID; // 创建遮罩层 const mask = document.createElement('div'); mask.id = LOADING_MASK_ID; document.body.append(btn, thumbBtn, autoBtn, indicator, hitArea, mask); // --- 使用元素直接监听替代坐标计算 --- const triggerAction = () => handleZoneTrigger(); // 1. 热区点击事件 (通用) hitArea.addEventListener('click', (e) => { e.stopPropagation(); triggerAction(); }); if (window.matchMedia('(hover: hover)').matches) { hitArea.addEventListener('mouseenter', triggerAction); hitArea.addEventListener('mousemove', triggerAction); [btn, thumbBtn, autoBtn].forEach(el => { el.addEventListener('mouseenter', triggerAction); el.addEventListener('mousemove', triggerAction); }); } window.addEventListener('scroll', handleScroll); } function init() { loadSettings(); injectStyle(); createControls(); initPageData(); applyLayoutSettings(); if (userSettings.autoEnter && !isReadingMode()) setTimeout(() => setReadingMode(true), 500); const paginator = document.querySelector('.paginator') || document.querySelector('.current-page'); if (paginator) { new MutationObserver(updatePageIndicator).observe(paginator, { childList: true, subtree: true, characterData: true }); } } const timer = setInterval(() => { if (looksLikeReader() && document.body) { clearInterval(timer); init(); } }, 200); })();