mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-09 06:46:37 +08:00
优化翻页
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties } from 'react'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import {
|
import {
|
||||||
@@ -109,9 +109,15 @@ const ReaderTextPageBody = memo(function ReaderTextPageBody({
|
|||||||
<>
|
<>
|
||||||
{blocks.map((block, key) => {
|
{blocks.map((block, key) => {
|
||||||
const { text, comments } = block
|
const { text, comments } = block
|
||||||
|
// content-visibility:跳过视口外段落的绘制。长章翻页时否则每次 transform
|
||||||
|
// 都要重绘整章(实测可到数百毫秒)。auto 尺寸会记住上次渲染高度,减少分页抖动。
|
||||||
|
const skipPaint: CSSProperties = {
|
||||||
|
contentVisibility: 'auto',
|
||||||
|
containIntrinsicSize: 'auto 4em',
|
||||||
|
}
|
||||||
if (text.startsWith(IMG_MARK)) {
|
if (text.startsWith(IMG_MARK)) {
|
||||||
return (
|
return (
|
||||||
<p key={key} style={{ marginBottom: paragraphSpacing, textAlign: 'center' }}>
|
<p key={key} style={{ marginBottom: paragraphSpacing, textAlign: 'center', ...skipPaint }}>
|
||||||
<img
|
<img
|
||||||
src={text.slice(IMG_MARK.length)}
|
src={text.slice(IMG_MARK.length)}
|
||||||
alt=""
|
alt=""
|
||||||
@@ -129,6 +135,7 @@ const ReaderTextPageBody = memo(function ReaderTextPageBody({
|
|||||||
textIndent: text ? '2em' : undefined,
|
textIndent: text ? '2em' : undefined,
|
||||||
marginBottom: paragraphSpacing,
|
marginBottom: paragraphSpacing,
|
||||||
textAlign: standalone && comments.every((c) => c.block) ? 'center' : undefined,
|
textAlign: standalone && comments.every((c) => c.block) ? 'center' : undefined,
|
||||||
|
...skipPaint,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
@@ -190,6 +197,21 @@ function settleComicDrag(el: HTMLDivElement, turn: 1 | -1, dx: number, turnPage:
|
|||||||
turnPage()
|
turnPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本滑动松手后的跟手层归位。正文多栏已瞬移,这里只把跟手层从「画面连续」的
|
||||||
|
* 位移滑回 0。不用 void offsetWidth 强刷布局——长章多栏一强制回流就要几百毫秒。
|
||||||
|
* turn=1 下一页,turn=-1 上一页;dx 为松手时的跟手位移。
|
||||||
|
*/
|
||||||
|
function settleTextDrag(dragEl: HTMLDivElement, turn: 1 | -1, dx: number): void {
|
||||||
|
const width = dragEl.clientWidth || 1
|
||||||
|
dragEl.style.transition = 'none'
|
||||||
|
dragEl.style.transform = `translate3d(${turn > 0 ? width + dx : -width + dx}px,0,0)`
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
dragEl.style.transition = 'transform 140ms ease-out'
|
||||||
|
dragEl.style.transform = 'translate3d(0,0,0)'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export default function ReaderViewPage() {
|
export default function ReaderViewPage() {
|
||||||
const { bookId = '' } = useParams()
|
const { bookId = '' } = useParams()
|
||||||
const settings = useReaderSettingsStore()
|
const settings = useReaderSettingsStore()
|
||||||
@@ -232,16 +254,28 @@ export default function ReaderViewPage() {
|
|||||||
const viewportRef = useRef<HTMLDivElement>(null)
|
const viewportRef = useRef<HTMLDivElement>(null)
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
const contentRef = useRef<HTMLDivElement>(null)
|
const contentRef = useRef<HTMLDivElement>(null)
|
||||||
|
/** 文本跟手层:滑动动画也写在这一层,避免给整章多栏做 transition。 */
|
||||||
|
const pageDragRef = useRef<HTMLDivElement>(null)
|
||||||
const [page, setPage] = useState(0)
|
const [page, setPage] = useState(0)
|
||||||
const [pageCount, setPageCount] = useState(1)
|
const [pageCount, setPageCount] = useState(1)
|
||||||
const [vw, setVw] = useState(0)
|
const [vw, setVw] = useState(0)
|
||||||
// 文本翻页位移走命令式 transform(见 applyTextPageTransform):先改 DOM 再 setPage,
|
// 文本翻页:位移与页脚先走 DOM,React 的 page state 等到过渡结束后再同步。
|
||||||
// 避免长章重渲染把翻页动画堵在主线程后面。下面几个 ref 用来去重 / 换章时关掉过渡。
|
// 若在同一次点击/触摸里同步 setPage,整页重渲染会堵住下一次绘制,
|
||||||
|
// 命令式 transform 也要等卡完才开始——点按和滑动都会顿一下。
|
||||||
const vwRef = useRef(0)
|
const vwRef = useRef(0)
|
||||||
|
const pageRef = useRef(0)
|
||||||
|
const pageCountRef = useRef(1)
|
||||||
|
const chapterTitleRef = useRef('')
|
||||||
|
const pageLabelRef = useRef<HTMLDivElement>(null)
|
||||||
const appliedTextPageRef = useRef(-1)
|
const appliedTextPageRef = useRef(-1)
|
||||||
const appliedTextVwRef = useRef(-1)
|
const appliedTextVwRef = useRef(-1)
|
||||||
const snapTextPageRef = useRef(false)
|
const snapTextPageRef = useRef(false)
|
||||||
|
/** 把 pageRef 同步回 React state 的防抖定时器;翻页热路径上绝不同步 setPage。 */
|
||||||
|
const textPageSyncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
/** 首次分页完成后锁住 pageCount,避免 content-visibility 改变 scrollWidth 后反复 setPageCount。 */
|
||||||
|
const pageCountLockedRef = useRef(false)
|
||||||
vwRef.current = vw
|
vwRef.current = vw
|
||||||
|
pageCountRef.current = pageCount
|
||||||
// 音频/漫画媒体状态
|
// 音频/漫画媒体状态
|
||||||
const [media, setMedia] = useState<ReaderChapterContent | null>(null)
|
const [media, setMedia] = useState<ReaderChapterContent | null>(null)
|
||||||
const [restorePos, setRestorePos] = useState(0) // 音频秒数 / 漫画图片序号
|
const [restorePos, setRestorePos] = useState(0) // 音频秒数 / 漫画图片序号
|
||||||
@@ -410,6 +444,8 @@ export default function ReaderViewPage() {
|
|||||||
// 换章瞬移到目标页,不要带着上一章的翻页过渡滑过去。
|
// 换章瞬移到目标页,不要带着上一章的翻页过渡滑过去。
|
||||||
snapTextPageRef.current = true
|
snapTextPageRef.current = true
|
||||||
appliedTextPageRef.current = -1
|
appliedTextPageRef.current = -1
|
||||||
|
pageCountLockedRef.current = false
|
||||||
|
pageRef.current = 0
|
||||||
setPage(0)
|
setPage(0)
|
||||||
setComicPage(0)
|
setComicPage(0)
|
||||||
setCurrentImage(0)
|
setCurrentImage(0)
|
||||||
@@ -475,34 +511,108 @@ export default function ReaderViewPage() {
|
|||||||
const width = vwRef.current > 0 ? vwRef.current : (viewportRef.current?.clientWidth ?? 0)
|
const width = vwRef.current > 0 ? vwRef.current : (viewportRef.current?.clientWidth ?? 0)
|
||||||
const stride = Math.max(width, 1) + COLUMN_GAP
|
const stride = Math.max(width, 1) + COLUMN_GAP
|
||||||
el.style.transition = animate ? PAGE_TRANSITION : 'none'
|
el.style.transition = animate ? PAGE_TRANSITION : 'none'
|
||||||
|
// 正文多栏可能极宽,不要 translate3d / will-change,免得整章被提成巨大合成层。
|
||||||
el.style.transform = `translateX(-${nextPage * stride}px)`
|
el.style.transform = `translateX(-${nextPage * stride}px)`
|
||||||
appliedTextPageRef.current = nextPage
|
appliedTextPageRef.current = nextPage
|
||||||
appliedTextVwRef.current = width
|
appliedTextVwRef.current = width
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const updateTextPageLabel = useCallback((nextPage: number) => {
|
||||||
|
const label = pageLabelRef.current
|
||||||
|
if (!label) return
|
||||||
|
const title = chapterTitleRef.current
|
||||||
|
label.textContent = title ? `${nextPage + 1} / ${pageCountRef.current} · ${title}` : `${nextPage + 1} / ${pageCountRef.current}`
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把 pageRef 写回 React。长章每次 setPage 都会让整页重渲染卡住 1s+,
|
||||||
|
* 所以阅读翻页热路径绝不调用;只在打开菜单 / 换章 / 卸载时同步。
|
||||||
|
*/
|
||||||
|
const flushTextPageReactSync = useCallback(() => {
|
||||||
|
if (textPageSyncTimerRef.current !== null) {
|
||||||
|
clearTimeout(textPageSyncTimerRef.current)
|
||||||
|
textPageSyncTimerRef.current = null
|
||||||
|
}
|
||||||
|
setPage((p) => (p === pageRef.current ? p : pageRef.current))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本翻到指定页(热路径零 setState)。
|
||||||
|
* - 正文多栏瞬移 + 页脚 DOM 更新
|
||||||
|
* - 点按/滚轮:无动画
|
||||||
|
* - 滑动松手:只动画跟手层
|
||||||
|
* dragDx 有值表示来自滑动松手。
|
||||||
|
*/
|
||||||
|
const commitTextPage = useCallback(
|
||||||
|
(nextPage: number, opts?: { dragDx?: number }) => {
|
||||||
|
const from = pageRef.current
|
||||||
|
const dragDx = opts?.dragDx
|
||||||
|
const fromSwipe = typeof dragDx === 'number'
|
||||||
|
const turn: 1 | -1 = nextPage >= from ? 1 : -1
|
||||||
|
|
||||||
|
pageRef.current = nextPage
|
||||||
|
applyTextPageTransform(nextPage, false)
|
||||||
|
updateTextPageLabel(nextPage)
|
||||||
|
|
||||||
|
const dragEl = pageDragRef.current
|
||||||
|
if (fromSwipe && dragEl && nextPage !== from) {
|
||||||
|
settleTextDrag(dragEl, turn, dragDx)
|
||||||
|
} else if (dragEl) {
|
||||||
|
dragEl.style.transition = 'none'
|
||||||
|
dragEl.style.transform = 'translate3d(0,0,0)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[applyTextPageTransform, updateTextPageLabel],
|
||||||
|
)
|
||||||
|
|
||||||
const relayout = useCallback(() => {
|
const relayout = useCallback(() => {
|
||||||
const vp = viewportRef.current
|
const vp = viewportRef.current
|
||||||
if (!vp) return
|
if (!vp) return
|
||||||
const width = vp.clientWidth
|
const width = vp.clientWidth
|
||||||
setVw(width)
|
// 用正文列自己的 scrollWidth;视口在子元素 transform 时 scrollWidth 会抖,
|
||||||
const total = vp.scrollWidth
|
// 导致翻一页就重新 setPageCount → 整页重渲染卡 1s+。
|
||||||
|
const total = contentRef.current?.scrollWidth ?? vp.scrollWidth
|
||||||
const count = Math.max(1, Math.ceil((total + COLUMN_GAP) / (width + COLUMN_GAP)))
|
const count = Math.max(1, Math.ceil((total + COLUMN_GAP) / (width + COLUMN_GAP)))
|
||||||
|
|
||||||
|
const pendingEnd = pendingEndRef.current && count > 0
|
||||||
|
const pendingPos = pendingPosRef.current > 0
|
||||||
|
const clamped = Math.min(pageRef.current, Math.max(0, count - 1))
|
||||||
|
const nextPage = pendingEnd
|
||||||
|
? count - 1
|
||||||
|
: pendingPos
|
||||||
|
? Math.min(pendingPosRef.current, count - 1)
|
||||||
|
: clamped
|
||||||
|
|
||||||
|
if (pendingEnd) pendingEndRef.current = false
|
||||||
|
if (pendingPos) pendingPosRef.current = 0
|
||||||
|
|
||||||
|
const widthChanged = width !== vwRef.current
|
||||||
|
// 窗口宽度变了要重新分页;锁住后忽略 scrollWidth 抖动(content-visibility 估算高度)。
|
||||||
|
if (widthChanged) pageCountLockedRef.current = false
|
||||||
|
const countChanged = !pageCountLockedRef.current && count !== pageCountRef.current
|
||||||
|
const pageChanged = nextPage !== pageRef.current
|
||||||
|
|
||||||
|
// 热路径上尺寸没变就别 setState——否则每次翻页都会重渲染 2000+ 段正文。
|
||||||
|
if (!widthChanged && !countChanged && !pageChanged && !pendingEnd && !pendingPos) {
|
||||||
|
if (count > 1) pageCountLockedRef.current = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vwRef.current = width
|
||||||
|
if (!pageCountLockedRef.current) pageCountRef.current = count
|
||||||
|
pageRef.current = nextPage
|
||||||
|
if (widthChanged) setVw(width)
|
||||||
|
if (countChanged) {
|
||||||
setPageCount(count)
|
setPageCount(count)
|
||||||
// 恢复进度 / 夹紧页码属于排版结果,不是用户翻页——关掉过渡,免得整章闪滑。
|
if (count > 1) pageCountLockedRef.current = true
|
||||||
|
}
|
||||||
|
if (pageChanged || pendingEnd || pendingPos) {
|
||||||
snapTextPageRef.current = true
|
snapTextPageRef.current = true
|
||||||
setPage((p) => {
|
setPage(nextPage)
|
||||||
if (pendingEndRef.current && count > 0) {
|
updateTextPageLabel(nextPage)
|
||||||
pendingEndRef.current = false
|
applyTextPageTransform(nextPage, false)
|
||||||
return count - 1
|
|
||||||
}
|
}
|
||||||
if (pendingPosRef.current > 0) {
|
}, [applyTextPageTransform, updateTextPageLabel])
|
||||||
const pos = Math.min(pendingPosRef.current, count - 1)
|
|
||||||
pendingPosRef.current = 0
|
|
||||||
return pos
|
|
||||||
}
|
|
||||||
return Math.min(p, count - 1)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
relayout()
|
relayout()
|
||||||
@@ -514,17 +624,35 @@ export default function ReaderViewPage() {
|
|||||||
if (appliedTextPageRef.current === page && appliedTextVwRef.current === vw) return
|
if (appliedTextPageRef.current === page && appliedTextVwRef.current === vw) return
|
||||||
const animate = !snapTextPageRef.current && appliedTextPageRef.current !== page
|
const animate = !snapTextPageRef.current && appliedTextPageRef.current !== page
|
||||||
snapTextPageRef.current = false
|
snapTextPageRef.current = false
|
||||||
|
pageRef.current = page
|
||||||
applyTextPageTransform(page, animate)
|
applyTextPageTransform(page, animate)
|
||||||
}, [page, vw, contentType, settings.pageMode, applyTextPageTransform])
|
updateTextPageLabel(page)
|
||||||
|
}, [page, vw, contentType, settings.pageMode, applyTextPageTransform, updateTextPageLabel])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const vp = viewportRef.current
|
const vp = viewportRef.current
|
||||||
if (!vp) return
|
if (!vp) return
|
||||||
const ro = new ResizeObserver(() => relayout())
|
// 合并同一帧里多次 resize,避免字体/滚动条抖动连带 relayout→setState。
|
||||||
|
let raf = 0
|
||||||
|
const ro = new ResizeObserver(() => {
|
||||||
|
if (raf) cancelAnimationFrame(raf)
|
||||||
|
raf = requestAnimationFrame(() => {
|
||||||
|
raf = 0
|
||||||
|
relayout()
|
||||||
|
})
|
||||||
|
})
|
||||||
ro.observe(vp)
|
ro.observe(vp)
|
||||||
return () => ro.disconnect()
|
return () => {
|
||||||
|
if (raf) cancelAnimationFrame(raf)
|
||||||
|
ro.disconnect()
|
||||||
|
}
|
||||||
}, [relayout, content, settings.pageMode])
|
}, [relayout, content, settings.pageMode])
|
||||||
|
|
||||||
|
// 打开菜单时把 pageRef 写回 state,进度条才不会停在旧页。
|
||||||
|
useEffect(() => {
|
||||||
|
if (menuOpen) flushTextPageReactSync()
|
||||||
|
}, [menuOpen, flushTextPageReactSync])
|
||||||
|
|
||||||
// 滚动模式恢复进度
|
// 滚动模式恢复进度
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (settings.pageMode !== 'scroll' || content === null) return
|
if (settings.pageMode !== 'scroll' || content === null) return
|
||||||
@@ -558,13 +686,22 @@ export default function ReaderViewPage() {
|
|||||||
[book, chapterIndex, chapters],
|
[book, chapterIndex, chapters],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 文本翻页进度:不依赖 React page(翻页热路径不 setPage),定时读 pageRef。
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 只处理文本:听书/漫画的正文为空串(不是 null),不能落到页码保存逻辑里,
|
|
||||||
// 否则打开音频章 1.5 秒后就会把页码 0 写成进度,覆盖掉刚才恢复的秒数。
|
|
||||||
if (contentType !== 'text' || settings.pageMode !== 'page' || content === null || chapterIndex === null) return
|
if (contentType !== 'text' || settings.pageMode !== 'page' || content === null || chapterIndex === null) return
|
||||||
const t = setTimeout(() => savePos(page), 1500)
|
let last = -1
|
||||||
return () => clearTimeout(t)
|
const tick = () => {
|
||||||
}, [page, content, chapterIndex, settings.pageMode, contentType, savePos])
|
const pos = pageRef.current
|
||||||
|
if (pos === last) return
|
||||||
|
last = pos
|
||||||
|
savePos(pos)
|
||||||
|
}
|
||||||
|
const t = setInterval(tick, 2000)
|
||||||
|
return () => {
|
||||||
|
clearInterval(t)
|
||||||
|
tick()
|
||||||
|
}
|
||||||
|
}, [content, chapterIndex, settings.pageMode, contentType, savePos])
|
||||||
|
|
||||||
// 漫画单页进度保存
|
// 漫画单页进度保存
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -742,14 +879,10 @@ export default function ReaderViewPage() {
|
|||||||
scrollRef.current?.scrollBy({ top: -window.innerHeight * 0.9, behavior: 'auto' })
|
scrollRef.current?.scrollBy({ top: -window.innerHeight * 0.9, behavior: 'auto' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 文本翻页:先命令式挪正文列,再 setPage 更新页脚。顺序不能反——
|
// 文本翻页读 pageRef:动画期间 React page 可能还没跟上,连点不能被旧 state 卡住。
|
||||||
// 否则长章重渲染会把过渡堵到卡顿结束才开始。
|
if (pageRef.current > 0) commitTextPage(pageRef.current - 1)
|
||||||
if (page > 0) {
|
else goChapter(-1, true)
|
||||||
const next = page - 1
|
}, [contentType, settings.pageMode, comicDoublePage, comicSpreads, comicSpreadIndex, comicPage, goChapter, commitTextPage])
|
||||||
applyTextPageTransform(next, true)
|
|
||||||
setPage(next)
|
|
||||||
} else goChapter(-1, true)
|
|
||||||
}, [contentType, settings.pageMode, comicDoublePage, comicSpreads, comicSpreadIndex, comicPage, page, goChapter, applyTextPageTransform])
|
|
||||||
|
|
||||||
const goNext = useCallback(() => {
|
const goNext = useCallback(() => {
|
||||||
if (contentType === 'audio') {
|
if (contentType === 'audio') {
|
||||||
@@ -773,12 +906,9 @@ export default function ReaderViewPage() {
|
|||||||
else el?.scrollBy({ top: window.innerHeight * 0.9, behavior: 'auto' })
|
else el?.scrollBy({ top: window.innerHeight * 0.9, behavior: 'auto' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (page < pageCount - 1) {
|
if (pageRef.current < pageCountRef.current - 1) commitTextPage(pageRef.current + 1)
|
||||||
const next = page + 1
|
else goChapter(1)
|
||||||
applyTextPageTransform(next, true)
|
}, [contentType, settings.pageMode, comicDoublePage, comicSpreads, comicSpreadIndex, media, comicPage, goChapter, commitTextPage])
|
||||||
setPage(next)
|
|
||||||
} else goChapter(1)
|
|
||||||
}, [contentType, settings.pageMode, comicDoublePage, comicSpreads, comicSpreadIndex, media, comicPage, page, pageCount, goChapter, applyTextPageTransform])
|
|
||||||
|
|
||||||
// ── 点击分区(左 30% 上一页 / 中间呼出菜单 / 右 30% 下一页) ──
|
// ── 点击分区(左 30% 上一页 / 中间呼出菜单 / 右 30% 下一页) ──
|
||||||
// 翻页模式用覆盖层上的三个按钮驱动;滚动模式不能再用覆盖层——
|
// 翻页模式用覆盖层上的三个按钮驱动;滚动模式不能再用覆盖层——
|
||||||
@@ -791,14 +921,11 @@ export default function ReaderViewPage() {
|
|||||||
// 「滑动后浏览器补发的 click」的去重都在 useHorizontalSwipe 里)。
|
// 「滑动后浏览器补发的 click」的去重都在 useHorizontalSwipe 里)。
|
||||||
//
|
//
|
||||||
// 跟手拖拽平移的是两个不同的元素:
|
// 跟手拖拽平移的是两个不同的元素:
|
||||||
// - 文本:正文列外面单独挂一层「跟手层」(pageDragRef)。分页平移由 applyTextPageTransform
|
// - 文本:正文列外面单独挂一层「跟手层」(pageDragRef)。正文多栏只瞬移,
|
||||||
// 写在里层(contentRef),跟手层只管手指位移;松手时两层同时动——跟手位移归零 +
|
// 滑页动画全在跟手层(settleTextDrag);长章不能给多栏做 transition。
|
||||||
// 页码变化——合起来就是一次连续滑动。分两层是为了避免跟手位移和 React/命令式页码
|
|
||||||
// 位移互相覆盖;页码不再经 React style 下发,长章翻页才不会先卡主线程再开动画。
|
|
||||||
// - 漫画:平移 ReaderComic 的舞台(comicStageRef)。舞台两侧摆着上一页/下一页
|
// - 漫画:平移 ReaderComic 的舞台(comicStageRef)。舞台两侧摆着上一页/下一页
|
||||||
// (见那边的 carousel 分支),翻页时单元格的 left 偏移与舞台位移在同一次提交里
|
// (见那边的 carousel 分支),翻页时单元格的 left 偏移与舞台位移在同一次提交里
|
||||||
// 相抵,画面看不出切换。
|
// 相抵,画面看不出切换。
|
||||||
const pageDragRef = useRef<HTMLDivElement>(null)
|
|
||||||
const comicStageRef = useRef<HTMLDivElement>(null)
|
const comicStageRef = useRef<HTMLDivElement>(null)
|
||||||
const pageDragEnabled = settings.pageMode === 'page' && (contentType === 'text' || comicDragSinglePage)
|
const pageDragEnabled = settings.pageMode === 'page' && (contentType === 'text' || comicDragSinglePage)
|
||||||
|
|
||||||
@@ -807,7 +934,7 @@ export default function ReaderViewPage() {
|
|||||||
const el = contentType === 'image' ? comicStageRef.current : pageDragRef.current
|
const el = contentType === 'image' ? comicStageRef.current : pageDragRef.current
|
||||||
if (!el) return
|
if (!el) return
|
||||||
el.style.transition = 'none'
|
el.style.transition = 'none'
|
||||||
el.style.transform = `translateX(${dx}px)`
|
el.style.transform = `translate3d(${dx}px,0,0)`
|
||||||
},
|
},
|
||||||
[contentType],
|
[contentType],
|
||||||
)
|
)
|
||||||
@@ -831,13 +958,31 @@ export default function ReaderViewPage() {
|
|||||||
}
|
}
|
||||||
const el = pageDragRef.current
|
const el = pageDragRef.current
|
||||||
if (!el) return
|
if (!el) return
|
||||||
// 跟手层归零与页码平移同时进行,合成一次连续滑动(位移不够时就是回弹)。
|
if (dx <= -DRAG_TURN_DISTANCE) {
|
||||||
|
if (pageRef.current < pageCountRef.current - 1) {
|
||||||
|
commitTextPage(pageRef.current + 1, { dragDx: dx })
|
||||||
|
} else {
|
||||||
el.style.transition = PAGE_TRANSITION
|
el.style.transition = PAGE_TRANSITION
|
||||||
el.style.transform = 'translateX(0px)'
|
el.style.transform = 'translate3d(0,0,0)'
|
||||||
if (dx <= -DRAG_TURN_DISTANCE) goNext()
|
goChapter(1)
|
||||||
else if (dx >= DRAG_TURN_DISTANCE) goPrev()
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (dx >= DRAG_TURN_DISTANCE) {
|
||||||
|
if (pageRef.current > 0) {
|
||||||
|
commitTextPage(pageRef.current - 1, { dragDx: dx })
|
||||||
|
} else {
|
||||||
|
el.style.transition = PAGE_TRANSITION
|
||||||
|
el.style.transform = 'translate3d(0,0,0)'
|
||||||
|
goChapter(-1, true)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 位移不够:回弹,不翻页。
|
||||||
|
el.style.transition = PAGE_TRANSITION
|
||||||
|
el.style.transform = 'translate3d(0,0,0)'
|
||||||
},
|
},
|
||||||
[contentType, goNext, goPrev],
|
[contentType, goNext, goPrev, commitTextPage, goChapter],
|
||||||
)
|
)
|
||||||
|
|
||||||
const { onTouchStart, onTouchMove, onTouchEnd, onTouchCancel, consumeSwipe } = useHorizontalSwipe({
|
const { onTouchStart, onTouchMove, onTouchEnd, onTouchCancel, consumeSwipe } = useHorizontalSwipe({
|
||||||
@@ -925,6 +1070,7 @@ export default function ReaderViewPage() {
|
|||||||
}, [settings.pageMode, menuOpen, contentType, goPrev, goNext])
|
}, [settings.pageMode, menuOpen, contentType, goPrev, goNext])
|
||||||
|
|
||||||
const currentChapter = chapterIndex !== null ? chapters[chapterIndex] : null
|
const currentChapter = chapterIndex !== null ? chapters[chapterIndex] : null
|
||||||
|
chapterTitleRef.current = currentChapter?.title ?? ''
|
||||||
|
|
||||||
// 顶栏高度量一次:菜单打开时正文下移,开头几行不被顶栏压住。
|
// 顶栏高度量一次:菜单打开时正文下移,开头几行不被顶栏压住。
|
||||||
// 用 transform 而不是 padding,避免改变视口高度触发重新分页。
|
// 用 transform 而不是 padding,避免改变视口高度触发重新分页。
|
||||||
@@ -972,10 +1118,10 @@ export default function ReaderViewPage() {
|
|||||||
),
|
),
|
||||||
onChange: (v: number) => {
|
onChange: (v: number) => {
|
||||||
if (settings.pageMode === 'page') {
|
if (settings.pageMode === 'page') {
|
||||||
const next = v - 1
|
commitTextPage(v - 1)
|
||||||
applyTextPageTransform(next, true)
|
flushTextPageReactSync()
|
||||||
setPage(next)
|
}
|
||||||
} else {
|
else {
|
||||||
const el = scrollRef.current
|
const el = scrollRef.current
|
||||||
if (el) el.scrollTop = (v / 1000) * (el.scrollHeight - el.clientHeight)
|
if (el) el.scrollTop = (v / 1000) * (el.scrollHeight - el.clientHeight)
|
||||||
}
|
}
|
||||||
@@ -1227,7 +1373,11 @@ export default function ReaderViewPage() {
|
|||||||
|
|
||||||
{/* 页脚页码(翻页模式) */}
|
{/* 页脚页码(翻页模式) */}
|
||||||
{settings.pageMode === 'page' && contentType === 'text' && content !== null && (
|
{settings.pageMode === 'page' && contentType === 'text' && content !== null && (
|
||||||
<div className="pointer-events-none pb-2 text-center text-2xs opacity-50" style={{ color: theme.text }}>
|
<div
|
||||||
|
ref={pageLabelRef}
|
||||||
|
className="pointer-events-none pb-2 text-center text-2xs opacity-50"
|
||||||
|
style={{ color: theme.text }}
|
||||||
|
>
|
||||||
{page + 1} / {pageCount} · {currentChapter?.title ?? ''}
|
{page + 1} / {pageCount} · {currentChapter?.title ?? ''}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user