mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-05 04:56:37 +08:00
优化
This commit is contained in:
@@ -5,6 +5,7 @@ import { ArrowLeft, ArrowLeftRight, BookOpen, ChevronDown, ChevronUp, Loader2, R
|
||||
|
||||
import { readerAPI, type ReaderBook, type ReaderBookInfo, type ReaderSearchOrigin, type ReaderTocChapter } from '../../api/reader'
|
||||
import ReaderBookCover from '../../components/ReaderBookCover'
|
||||
import { pickChapterIndex } from './bookDetailModel'
|
||||
import { SourcePickerDialog } from './SourcePickerDialog'
|
||||
|
||||
// 书籍详情页(仿 legado BookInfoActivity:封面 + 信息 + 简介 + 目录入口 + 加书架/开始阅读)。
|
||||
@@ -86,7 +87,16 @@ export default function ReaderBookPage() {
|
||||
|
||||
const firstReadableChapter = useMemo(() => chapters?.find((c) => !c.is_volume && c.url) ?? null, [chapters])
|
||||
|
||||
const startReading = async () => {
|
||||
/**
|
||||
* 目录点章:把点中的章节换算成阅读器要的序号(换算规则与理由见 bookDetailModel)。
|
||||
*/
|
||||
const resolveChapterIndex = async (bookID: string, chapter: ReaderTocChapter): Promise<number> => {
|
||||
const cached = await readerAPI.listChapters(bookID).catch(() => [])
|
||||
return pickChapterIndex(cached, chapter)
|
||||
}
|
||||
|
||||
// chapter 为空=开始/继续阅读(沿用服务端进度);给了章节=目录点章跳读。
|
||||
const startReading = async (chapter?: ReaderTocChapter) => {
|
||||
setBusy(true)
|
||||
try {
|
||||
let book = shelfBook
|
||||
@@ -105,7 +115,9 @@ export default function ReaderBookPage() {
|
||||
cover_url: cover,
|
||||
})
|
||||
}
|
||||
navigate(`/reader/view/${book.id}`)
|
||||
// 目录点章:带上阅读器认识的 chapter 序号跳进去(没有它阅读器就用服务端进度)。
|
||||
const index = chapter ? await resolveChapterIndex(book.id, chapter) : null
|
||||
navigate(index === null ? `/reader/view/${book.id}` : `/reader/view/${book.id}?chapter=${index}`)
|
||||
} catch (e) {
|
||||
toast.error((e as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '操作失败')
|
||||
} finally {
|
||||
@@ -273,11 +285,11 @@ export default function ReaderBookPage() {
|
||||
<Trash2 size={13} className="mr-1 inline" /> 移出书架
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={startReading} disabled={busy} className="btn-outline flex-1 text-xs disabled:opacity-50">
|
||||
<button type="button" onClick={() => startReading()} disabled={busy} className="btn-outline flex-1 text-xs disabled:opacity-50">
|
||||
<BookOpen size={13} className="mr-1 inline" /> 加入书架
|
||||
</button>
|
||||
)}
|
||||
<button type="button" onClick={startReading} disabled={busy || !firstReadableChapter} className="btn-primary flex-1 text-xs disabled:opacity-50">
|
||||
<button type="button" onClick={() => startReading()} disabled={busy || !firstReadableChapter} className="btn-primary flex-1 text-xs disabled:opacity-50">
|
||||
{busy ? <Loader2 size={13} className="mr-1 inline animate-spin" /> : null}
|
||||
{shelfBook?.dur_chapter_title ? '继续阅读' : '开始阅读'}
|
||||
</button>
|
||||
@@ -318,6 +330,16 @@ export default function ReaderBookPage() {
|
||||
<div key={c.index} className="px-1 py-1.5 text-xs text-[var(--app-muted)]">
|
||||
{c.is_volume ? (
|
||||
<p className="font-bold text-[var(--app-text)]">{c.title}</p>
|
||||
) : c.url ? (
|
||||
// 点章节=跳到阅读器并从这一章开始(听书源会自动起播这一章)。
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => startReading(c)}
|
||||
className="block w-full truncate text-left transition-colors hover:text-brand-600 disabled:opacity-50"
|
||||
>
|
||||
{c.title}
|
||||
</button>
|
||||
) : (
|
||||
c.title
|
||||
)}
|
||||
|
||||
@@ -220,7 +220,10 @@ export default function ReaderViewPage() {
|
||||
else if (b.dur_chapter_index > 0 && chs[b.dur_chapter_index] && !chs[b.dur_chapter_index].is_volume) {
|
||||
idx = b.dur_chapter_index
|
||||
}
|
||||
pendingPosRef.current = b.dur_chapter_pos ?? 0
|
||||
// 位置只在「接着上次那一章读」时恢复:URL 显式跳到别的章时,b.dur_chapter_pos
|
||||
// 属于另一章,套到新章上会从中间开始(音频表现为一进去就从错的时间点续播)。
|
||||
const resumingSavedChapter = !explicitChapter || idx === b.dur_chapter_index
|
||||
pendingPosRef.current = resumingSavedChapter ? (b.dur_chapter_pos ?? 0) : 0
|
||||
// 听书:引擎是常驻的,可能已经自动播到更靠后的章,而服务端的进度还停在
|
||||
// 上一次落库的位置。这种「续听」场景要以引擎的实时章节为准,否则回到本书
|
||||
// 会把正在播的章节往回拽。URL 显式指定 chapter 时尊重 URL(那是明确的跳转)。
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ReaderChapter, ReaderTocChapter } from '../../api/reader.ts'
|
||||
import { pickChapterIndex } from './bookDetailModel.ts'
|
||||
|
||||
function check(name: string, condition: boolean) {
|
||||
if (!condition) throw new Error(`bookDetailModel: ${name}`)
|
||||
}
|
||||
|
||||
function cached(index: number, url: string): ReaderChapter {
|
||||
return { index, title: `第${index}章`, url, is_volume: false }
|
||||
}
|
||||
|
||||
function toc(index: number, url: string, over: Partial<ReaderTocChapter> = {}): ReaderTocChapter {
|
||||
return { index, title: `第${index}章`, url, is_volume: false, update_time: '', ...over }
|
||||
}
|
||||
|
||||
// 目录一致(正常情况):按 url 定位到的序号与现抓序号相同
|
||||
{
|
||||
const list = [cached(0, 'https://s/1'), cached(1, 'https://s/2'), cached(2, 'https://s/3')]
|
||||
check('same url keeps the same index', pickChapterIndex(list, toc(2, 'https://s/3')) === 2)
|
||||
}
|
||||
|
||||
// 站点在开头插了新章:同一章在现抓目录里是 2,在缓存目录里是 1——必须用缓存里的 1,
|
||||
// 照搬现抓的 2 会跳到下一章。
|
||||
{
|
||||
const list = [cached(0, 'https://s/1'), cached(1, 'https://s/2'), cached(2, 'https://s/3')]
|
||||
check('inserted chapter maps back to the cached index', pickChapterIndex(list, toc(2, 'https://s/2')) === 1)
|
||||
}
|
||||
|
||||
// 缓存目录里没有这一章(站点改版、或两边取的不是同一份目录):退回现抓的序号
|
||||
{
|
||||
const list = [cached(0, 'https://s/1')]
|
||||
check('unknown url falls back to the fetched index', pickChapterIndex(list, toc(7, 'https://s/7')) === 7)
|
||||
}
|
||||
|
||||
// 缓存为空(第一次打开这本书):退回现抓序号,阅读器随后会把这份目录存下来
|
||||
{
|
||||
check('empty cache falls back to the fetched index', pickChapterIndex([], toc(5, 'https://s/5')) === 5)
|
||||
}
|
||||
|
||||
// 章节没有 url(拿不到地址):退回现抓序号,不做 url 匹配
|
||||
{
|
||||
const list = [cached(0, ''), cached(1, '')]
|
||||
check('chapter without url falls back to the fetched index', pickChapterIndex(list, toc(1, '')) === 1)
|
||||
}
|
||||
|
||||
console.log('bookDetailModel.test.ts ok')
|
||||
@@ -0,0 +1,20 @@
|
||||
// 书籍详情页点目录跳读的纯逻辑(不依赖 React,便于单测)。
|
||||
|
||||
import type { ReaderChapter, ReaderTocChapter } from '../../api/reader'
|
||||
|
||||
/**
|
||||
* 把详情页点中的章节换算成阅读器要的序号。
|
||||
*
|
||||
* 阅读器打开一本书时优先用库里缓存的目录(`/books/:id/chapters`),而详情页显示的
|
||||
* 目录是现抓的:站点更新过目录(插入或调整过章节)时两者会错位,直接照搬现抓目录
|
||||
* 里的序号会跳到别的章。url 是章节的稳定标识,所以先按 url 在缓存目录里定位;
|
||||
* 找不到(缓存为空,或两边取的根本不是同一份目录)才退回现抓的序号——缓存为空时
|
||||
* 阅读器本来就会把这次现抓的目录存下来,序号自然对得上。
|
||||
*/
|
||||
export function pickChapterIndex(cached: ReaderChapter[], chapter: ReaderTocChapter): number {
|
||||
if (chapter.url) {
|
||||
const hit = cached.findIndex((c) => c.url === chapter.url)
|
||||
if (hit >= 0) return hit
|
||||
}
|
||||
return chapter.index
|
||||
}
|
||||
Reference in New Issue
Block a user