diff --git a/src/main/resources/static/js/reader.js b/src/main/resources/static/js/reader.js index ba19b05..2a890b2 100644 --- a/src/main/resources/static/js/reader.js +++ b/src/main/resources/static/js/reader.js @@ -9,11 +9,24 @@ const Reader = (function() { let toolbarVisible = false; let toolbarTimer = null; + // 翻页模式相关状态 + let currentReadingMode = 'page'; // 'scroll' 或 'page' + let currentPage = 0; + let totalPages = 0; + let pageContents = []; // 分页后的内容 + let touchStartX = 0; + let touchStartY = 0; + let touchEndX = 0; + let touchEndY = 0; + let isSwiping = false; + // ========== 初始化阅读器 ========== async function init(novelId, chapterIndex = 0) { currentNovelId = novelId; currentChapterIndex = chapterIndex; + currentReadingMode = Store.getReadingMode(); + currentPage = 0; // 隐藏导航栏 $('#app-header').addClass('header-hidden'); @@ -98,29 +111,169 @@ const Reader = (function() { } const $content = $('#app-content'); - $content.html(` -
-
-

${chapter.title}

-
${content}
-
- - + + if (currentReadingMode === 'scroll') { + // 上下滚动模式 + $content.html(` +
+
+

${chapter.title}

+
${content}
+
+ + +
-
- `); + `); + } else { + // 左右翻页模式 + $content.html(` +
+
+
+

${chapter.title}

+
${content}
+
+
+
+ 1 / 1 +
+
+
+
+
+
+
+ `); + + // 等待DOM渲染后进行分页计算 + setTimeout(() => { + calculatePages(); + showPage(currentPage); + }, 100); + } // 应用设置 applySettings(); } + // ========== 翻页模式相关函数 ========== + + function calculatePages() { + if (currentReadingMode !== 'page') return; + + const $container = $('.page-container'); + const $content = $('.page-content'); + + if (!$container.length || !$content.length) return; + + const containerHeight = $container.height(); + const contentHeight = $content[0].scrollHeight; + + // 计算总页数 + totalPages = Math.max(1, Math.ceil(contentHeight / containerHeight)); + pageContents = []; + + // 更新页码显示 + $('.page-total').text(totalPages); + + // 恢复之前的阅读位置 + const position = Store.getReadingPosition(currentNovelId); + if (position && position.chapterIndex === currentChapterIndex && position.pageIndex !== undefined) { + currentPage = Math.min(position.pageIndex, totalPages - 1); + } else { + currentPage = 0; + } + } + + function showPage(pageIndex) { + if (currentReadingMode !== 'page') return; + + const $container = $('.page-container'); + const $content = $('.page-content'); + + if (!$container.length || !$content.length) return; + + // 确保页码在有效范围内 + pageIndex = Math.max(0, Math.min(pageIndex, totalPages - 1)); + currentPage = pageIndex; + + const containerHeight = $container.height(); + const scrollTop = pageIndex * containerHeight; + + // 使用transform进行平滑翻页 + $content.css('transform', `translateY(-${scrollTop}px)`); + + // 更新页码显示 + $('.page-current').text(currentPage + 1); + + // 保存阅读位置 + saveReadingProgress(); + + // 更新进度条 + updateProgressIndicator(); + } + + function nextPage() { + if (currentReadingMode !== 'page') return; + + if (currentPage < totalPages - 1) { + showPage(currentPage + 1); + } else { + // 已是最后一页,跳转下一章 + nextChapter(); + } + } + + function prevPage() { + if (currentReadingMode !== 'page') return; + + if (currentPage > 0) { + showPage(currentPage - 1); + } else { + // 已是第一页,跳转上一章 + if (currentChapterIndex > 0) { + // 跳转上一章并显示最后一页 + goToPrevChapterLastPage(); + } else { + Toast.show('已是第一章'); + } + } + } + + async function goToPrevChapterLastPage() { + hideToolbar(); + try { + const chapter = await API.getPrevChapter(currentNovelId, currentChapterIndex); + if (chapter) { + currentChapter = chapter; + currentChapterIndex = chapter.index; + renderChapter(chapter); + saveReadingProgress(); + updateHistory(currentNovelId, chapter.id); + $('#reader-title').text(chapter.title); + + // 等待分页计算完成后跳到最后一页 + setTimeout(() => { + if (totalPages > 0) { + showPage(totalPages - 1); + } + }, 150); + } else { + Toast.show('已是第一章'); + } + } catch (error) { + Toast.show('加载失败'); + } + } + // ========== 章节导航 ========== async function nextChapter() { @@ -130,6 +283,7 @@ const Reader = (function() { if (chapter) { currentChapter = chapter; currentChapterIndex = chapter.index; + currentPage = 0; // 重置页码 renderChapter(chapter); scrollToTop(); saveReadingProgress(); @@ -154,6 +308,7 @@ const Reader = (function() { if (chapter) { currentChapter = chapter; currentChapterIndex = chapter.index; + currentPage = 0; // 重置页码 renderChapter(chapter); scrollToTop(); saveReadingProgress(); @@ -170,11 +325,13 @@ const Reader = (function() { async function goToChapter(index) { hideToolbar(); hideCatalog(); + currentPage = 0; // 重置页码 await loadChapter(currentNovelId, index); scrollToTop(); } function reload() { + currentPage = 0; loadChapter(currentNovelId, currentChapterIndex); } @@ -221,6 +378,7 @@ const Reader = (function() { function applySettings() { const fontSize = Store.getFontSize(); const lineHeight = Store.getLineHeight(); + const readingMode = Store.getReadingMode(); $('.reader-text').css({ '--reader-font-size': fontSize + 'px', @@ -239,6 +397,26 @@ const Reader = (function() { const theme = Store.getTheme(); $('.theme-btn').removeClass('active'); $(`.theme-btn[data-theme="${theme}"]`).addClass('active'); + + // 更新阅读模式按钮状态 + $('.reading-mode-btn').removeClass('active'); + $(`.reading-mode-btn[data-mode="${readingMode}"]`).addClass('active'); + } + + function changeReadingMode(mode) { + if (mode === currentReadingMode) return; + + Store.setReadingMode(mode); + currentReadingMode = mode; + currentPage = 0; + + // 重新渲染当前章节 + if (currentChapter) { + renderChapter(currentChapter); + } + + applySettings(); + Toast.show(mode === 'page' ? '已切换为翻页模式' : '已切换为滚动模式'); } function changeFontSize(delta) { @@ -365,19 +543,31 @@ const Reader = (function() { function saveReadingProgress() { if (!currentNovelId) return; - Store.setReadingPosition(currentNovelId, { - chapterIndex: currentChapterIndex, - scrollTop: window.scrollY - }); + if (currentReadingMode === 'page') { + Store.setReadingPosition(currentNovelId, { + chapterIndex: currentChapterIndex, + pageIndex: currentPage, + readingMode: 'page' + }); + } else { + Store.setReadingPosition(currentNovelId, { + chapterIndex: currentChapterIndex, + scrollTop: window.scrollY, + readingMode: 'scroll' + }); + } } function restoreScrollPosition() { const position = Store.getReadingPosition(currentNovelId); - if (position && position.chapterIndex === currentChapterIndex && position.scrollTop) { + if (!position || position.chapterIndex !== currentChapterIndex) return; + + if (currentReadingMode === 'scroll' && position.scrollTop) { setTimeout(() => { window.scrollTo(0, position.scrollTop); }, 100); } + // 翻页模式的位置恢复在 calculatePages 中处理 } async function updateHistory(novelId, chapterId) { @@ -400,25 +590,99 @@ const Reader = (function() { } function updateProgressIndicator() { - const scrollTop = window.scrollY; - const docHeight = document.documentElement.scrollHeight - window.innerHeight; - const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0; + let progress = 0; + + if (currentReadingMode === 'page') { + // 翻页模式:基于页码计算进度 + progress = totalPages > 1 ? (currentPage / (totalPages - 1)) * 100 : 100; + } else { + // 滚动模式:基于滚动位置计算进度 + const scrollTop = window.scrollY; + const docHeight = document.documentElement.scrollHeight - window.innerHeight; + progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0; + } + $('.reading-progress-indicator .progress-bar').css('width', progress + '%'); } // ========== 事件绑定 ========== function bindEvents() { - // 点击内容区域切换工具栏 - $(document).off('click.reader').on('click.reader', '.reader-page', function(e) { + // 滚动模式:点击内容区域切换工具栏 + $(document).off('click.reader').on('click.reader', '.reader-page.scroll-mode', function(e) { // 排除按钮点击 if ($(e.target).closest('button, a').length) return; toggleToolbar(); }); - // 滚动事件 + // 翻页模式:点击区域处理 + $(document).off('click.tapprev').on('click.tapprev', '.tap-prev', function(e) { + e.stopPropagation(); + prevPage(); + }); + + $(document).off('click.tapcenter').on('click.tapcenter', '.tap-center', function(e) { + e.stopPropagation(); + toggleToolbar(); + }); + + $(document).off('click.tapnext').on('click.tapnext', '.tap-next', function(e) { + e.stopPropagation(); + nextPage(); + }); + + // 翻页模式:触摸滑动事件 + $(document).off('touchstart.reader').on('touchstart.reader', '.reader-page.page-mode', function(e) { + if ($(e.target).closest('button, a, .tap-zone').length) return; + touchStartX = e.originalEvent.touches[0].clientX; + touchStartY = e.originalEvent.touches[0].clientY; + isSwiping = false; + }); + + $(document).off('touchmove.reader').on('touchmove.reader', '.reader-page.page-mode', function(e) { + if (!touchStartX) return; + touchEndX = e.originalEvent.touches[0].clientX; + touchEndY = e.originalEvent.touches[0].clientY; + + const diffX = touchEndX - touchStartX; + const diffY = touchEndY - touchStartY; + + // 判断是否为水平滑动 + if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 30) { + isSwiping = true; + } + }); + + $(document).off('touchend.reader').on('touchend.reader', '.reader-page.page-mode', function(e) { + if (!isSwiping) { + touchStartX = 0; + touchStartY = 0; + return; + } + + const diffX = touchEndX - touchStartX; + const threshold = 50; // 滑动阈值 + + if (diffX > threshold) { + // 向右滑动 - 上一页 + prevPage(); + } else if (diffX < -threshold) { + // 向左滑动 - 下一页 + nextPage(); + } + + touchStartX = 0; + touchStartY = 0; + touchEndX = 0; + touchEndY = 0; + isSwiping = false; + }); + + // 滚动模式:滚动事件 let scrollTimer; $(window).off('scroll.reader').on('scroll.reader', function() { + if (currentReadingMode !== 'scroll') return; + // 更新进度条 updateProgressIndicator(); @@ -437,7 +701,7 @@ const Reader = (function() { $('#reader-settings').off('click').on('click', showSettings); $('#reader-bookmark').off('click').on('click', showBookmarkModal); - // 章节内导航按钮 + // 章节内导航按钮(滚动模式) $(document).off('click.navprev').on('click.navprev', '#nav-prev', prevChapter); $(document).off('click.navnext').on('click.navnext', '#nav-next', nextChapter); @@ -445,17 +709,48 @@ const Reader = (function() { $('#close-settings').off('click').on('click', hideSettings); $('.settings-overlay').off('click').on('click', hideSettings); - $('#font-decrease').off('click').on('click', () => changeFontSize(-2)); - $('#font-increase').off('click').on('click', () => changeFontSize(2)); + $('#font-decrease').off('click').on('click', () => { + changeFontSize(-2); + // 翻页模式下重新计算分页 + if (currentReadingMode === 'page') { + setTimeout(() => { + calculatePages(); + showPage(0); + }, 100); + } + }); + $('#font-increase').off('click').on('click', () => { + changeFontSize(2); + // 翻页模式下重新计算分页 + if (currentReadingMode === 'page') { + setTimeout(() => { + calculatePages(); + showPage(0); + }, 100); + } + }); $('.line-height-btn').off('click').on('click', function() { changeLineHeight($(this).data('value')); + // 翻页模式下重新计算分页 + if (currentReadingMode === 'page') { + setTimeout(() => { + calculatePages(); + showPage(0); + }, 100); + } }); $('.theme-btn').off('click').on('click', function() { changeTheme($(this).data('theme')); }); + // 阅读模式切换按钮 + $(document).off('click.readingmode').on('click.readingmode', '.reading-mode-btn', function() { + const mode = $(this).data('mode'); + changeReadingMode(mode); + }); + // 目录面板 $('#close-catalog').off('click').on('click', hideCatalog); $('.catalog-overlay').off('click').on('click', hideCatalog); @@ -477,14 +772,32 @@ const Reader = (function() { const note = $('#bookmark-note').val(); addBookmark(note); }); + + // 窗口大小变化时重新计算分页 + $(window).off('resize.reader').on('resize.reader', function() { + if (currentReadingMode === 'page') { + setTimeout(() => { + calculatePages(); + showPage(currentPage); + }, 100); + } + }); } function unbindEvents() { $(document).off('click.reader'); + $(document).off('click.tapprev'); + $(document).off('click.tapcenter'); + $(document).off('click.tapnext'); + $(document).off('touchstart.reader'); + $(document).off('touchmove.reader'); + $(document).off('touchend.reader'); $(window).off('scroll.reader'); + $(window).off('resize.reader'); $(document).off('click.navprev'); $(document).off('click.navnext'); $(document).off('click.catalogitem'); + $(document).off('click.readingmode'); } // ========== 退出阅读器 ========== @@ -534,7 +847,10 @@ const Reader = (function() { goToChapter, showCatalog, showSettings, - showBookmarkModal + showBookmarkModal, + changeReadingMode, + nextPage, + prevPage }; })(); diff --git a/src/main/resources/static/js/store.js b/src/main/resources/static/js/store.js index 3d00ed5..c2c4641 100644 --- a/src/main/resources/static/js/store.js +++ b/src/main/resources/static/js/store.js @@ -8,7 +8,7 @@ const Store = (function() { THEME: 'novel_reader_theme', FONT_SIZE: 'novel_reader_font_size', LINE_HEIGHT: 'novel_reader_line_height', - READ_MODE: 'novel_reader_read_mode', // 阅读模式: 'page'(翻页) | 'scroll'(滚动) + READING_MODE: 'novel_reader_reading_mode', // 阅读模式:scroll(上下滚动) / page(左右翻页) READING_POSITION: 'novel_reader_position_', SHELF_CACHE: 'novel_reader_shelf_cache', HISTORY_CACHE: 'novel_reader_history_cache', @@ -23,7 +23,7 @@ const Store = (function() { theme: 'dark', fontSize: 18, lineHeight: 1.8, - readMode: 'page' // 默认左右翻页模式 + readingMode: 'page' // 默认左右翻页模式 }; // ========== 基础存储方法 ========== @@ -102,16 +102,15 @@ const Store = (function() { // ========== 阅读模式设置 ========== - function getReadMode() { - return get(KEYS.READ_MODE, DEFAULTS.readMode); + function getReadingMode() { + return get(KEYS.READING_MODE, DEFAULTS.readingMode); } - function setReadMode(mode) { - // mode: 'page' (左右翻页) | 'scroll' (上下滚动) - if (mode === 'page' || mode === 'scroll') { - set(KEYS.READ_MODE, mode); + function setReadingMode(mode) { + // mode: 'scroll' (上下滚动) 或 'page' (左右翻页) + if (mode === 'scroll' || mode === 'page') { + set(KEYS.READING_MODE, mode); } - return getReadMode(); } // ========== 阅读位置缓存 ========== @@ -227,6 +226,8 @@ const Store = (function() { setFontSize, getLineHeight, setLineHeight, + getReadingMode, + setReadingMode, // 阅读位置 getReadingPosition, diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 9ab6b17..175dd87 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -198,6 +198,19 @@
+
+ +
+ + +
+