mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
feat: improve PDF reader loading state and UI
This commit is contained in:
@@ -33,6 +33,8 @@ const bookId = Number(route.params.bookId)
|
||||
const fileId = Number(route.params.fileId)
|
||||
const fileFormat = (route.query.format as string) || 'epub'
|
||||
const isAudioFormat = getFormatGroup(fileFormat) === 'audio'
|
||||
const isPdfFormat = fileFormat === 'pdf'
|
||||
const isComicFormat = fileFormat === 'cbz' || fileFormat === 'cbr' || fileFormat === 'cb7'
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const showSidebar = ref(false)
|
||||
@@ -137,26 +139,22 @@ onMounted(async () => {
|
||||
document.addEventListener('fullscreenchange', onFullscreenChange)
|
||||
onUnmounted(() => document.removeEventListener('fullscreenchange', onFullscreenChange))
|
||||
|
||||
// Specialized readers own their own progress/settings/loading lifecycle.
|
||||
if (isAudioFormat || isPdfFormat || isComicFormat) return
|
||||
|
||||
await progress.load()
|
||||
|
||||
// Audio formats are handled entirely by AudiobookReaderView
|
||||
if (isAudioFormat) return
|
||||
|
||||
// Only epub-group formats use the epub reader; PDF/CBZ are rendered by child components
|
||||
const isEpubFormat = !['pdf', 'cbz', 'cbr', 'cb7'].includes(fileFormat)
|
||||
if (isEpubFormat) {
|
||||
await bookSettings.load()
|
||||
const effective = bookSettings.effective.value as EpubReaderSettings
|
||||
if (effective.overrideBookFormatting) {
|
||||
shouldApplyStyles.value = true
|
||||
seedState(effective)
|
||||
} else if (bookSettings.isCustomized.value) {
|
||||
// Only apply the per-book delta - don't bleed global defaults like dark theme into the book
|
||||
shouldApplyStyles.value = true
|
||||
seedState(bookSettings.bookDelta.value as Partial<ReaderState>)
|
||||
} else {
|
||||
shouldApplyStyles.value = false
|
||||
}
|
||||
await bookSettings.load()
|
||||
const effective = bookSettings.effective.value as EpubReaderSettings
|
||||
if (effective.overrideBookFormatting) {
|
||||
shouldApplyStyles.value = true
|
||||
seedState(effective)
|
||||
} else if (bookSettings.isCustomized.value) {
|
||||
// Only apply the per-book delta - don't bleed global defaults like dark theme into the book
|
||||
shouldApplyStyles.value = true
|
||||
seedState(bookSettings.bookDelta.value as Partial<ReaderState>)
|
||||
} else {
|
||||
shouldApplyStyles.value = false
|
||||
}
|
||||
|
||||
await open(bookId, fileId, fileFormat, progress.cfi.value)
|
||||
@@ -287,8 +285,8 @@ function closeSearch() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PdfReaderView v-if="fileFormat === 'pdf'" :bookId="bookId" :fileId="fileId" />
|
||||
<CbzReaderView v-else-if="fileFormat === 'cbz' || fileFormat === 'cbr' || fileFormat === 'cb7'" :bookId="bookId" :fileId="fileId" />
|
||||
<PdfReaderView v-if="isPdfFormat" :bookId="bookId" :fileId="fileId" />
|
||||
<CbzReaderView v-else-if="isComicFormat" :bookId="bookId" :fileId="fileId" />
|
||||
<AudiobookReaderView v-else-if="isAudioFormat" :bookId="bookId" :fileId="fileId" />
|
||||
<div
|
||||
v-else
|
||||
|
||||
@@ -61,7 +61,7 @@ function onDimUpdate(pageNum: number, dim: PageDim) {
|
||||
}
|
||||
|
||||
const renderer = usePdfRenderer(startRenderPage, getTextContent, scale, totalPages, onDimUpdate)
|
||||
const { canvasMap, textLayerMap, invalidate, setupIO, reset, destroy } = renderer
|
||||
const { canvasMap, textLayerMap, invalidate, renderPage, setupIO, reset, destroy } = renderer
|
||||
|
||||
// ── Find ──────────────────────────────────────────────────────────────────────
|
||||
const find = usePdfFind(textLayerMap)
|
||||
@@ -161,6 +161,8 @@ watch(currentPage, (page) => {
|
||||
})
|
||||
|
||||
const progressPct = computed(() => (totalPages.value ? Math.round((currentPage.value / totalPages.value) * 100) : 0))
|
||||
const readerReady = ref(false)
|
||||
const isReaderLoading = computed(() => !error.value && (!readerReady.value || loading.value))
|
||||
|
||||
// ── Scroll container style ────────────────────────────────────────────────────
|
||||
const scrollStyle = computed((): CSSProperties => {
|
||||
@@ -226,49 +228,53 @@ onMounted(async () => {
|
||||
isFullscreen.value = !!document.fullscreenElement
|
||||
})
|
||||
|
||||
await progress.load()
|
||||
await bookSettings.load()
|
||||
try {
|
||||
await progress.load()
|
||||
await bookSettings.load()
|
||||
|
||||
const ps = bookSettings.effective.value as PdfReaderSettings
|
||||
// Coerce saved 'continuous' (old value) to 'vertical'
|
||||
const savedMode = ps.scrollMode as string
|
||||
scrollMode.value = savedMode === 'continuous' ? 'vertical' : (ps.scrollMode as ScrollMode)
|
||||
spread.value = ps.spread
|
||||
zoomMode.value = ps.zoomMode
|
||||
customScale.value = ps.customScale
|
||||
const ps = bookSettings.effective.value as PdfReaderSettings
|
||||
// Coerce saved 'continuous' (old value) to 'vertical'
|
||||
const savedMode = ps.scrollMode as string
|
||||
scrollMode.value = savedMode === 'continuous' ? 'vertical' : (ps.scrollMode as ScrollMode)
|
||||
spread.value = ps.spread
|
||||
zoomMode.value = ps.zoomMode
|
||||
customScale.value = ps.customScale
|
||||
|
||||
watch(spread, (v) => bookSettings.updateBookSettings({ spread: v }))
|
||||
watch(zoomMode, (v) => bookSettings.updateBookSettings({ zoomMode: v }))
|
||||
watch(customScale, (v) => bookSettings.updateBookSettings({ customScale: v }))
|
||||
watch(scrollMode, (v) => bookSettings.updateBookSettings({ scrollMode: v } as Parameters<typeof bookSettings.updateBookSettings>[0]))
|
||||
watch(spread, (v) => bookSettings.updateBookSettings({ spread: v }))
|
||||
watch(zoomMode, (v) => bookSettings.updateBookSettings({ zoomMode: v }))
|
||||
watch(customScale, (v) => bookSettings.updateBookSettings({ customScale: v }))
|
||||
watch(scrollMode, (v) => bookSettings.updateBookSettings({ scrollMode: v } as Parameters<typeof bookSettings.updateBookSettings>[0]))
|
||||
|
||||
await load(props.fileId)
|
||||
if (!pdfDoc.value) return
|
||||
await load(props.fileId)
|
||||
if (!pdfDoc.value) return
|
||||
|
||||
const firstDim = await getPageDim(1)
|
||||
pageDims.value = Array.from({ length: totalPages.value }, () => ({ ...firstDim }))
|
||||
reset()
|
||||
const firstDim = await getPageDim(1)
|
||||
pageDims.value = Array.from({ length: totalPages.value }, () => ({ ...firstDim }))
|
||||
reset()
|
||||
|
||||
await nextTick()
|
||||
measure()
|
||||
|
||||
await outline.load()
|
||||
|
||||
resizeObserver = new ResizeObserver(async () => {
|
||||
await nextTick()
|
||||
measure()
|
||||
invalidate()
|
||||
await nextTick()
|
||||
if (scrollRef.value) setupIO(scrollRef.value)
|
||||
})
|
||||
|
||||
if (scrollRef.value) {
|
||||
resizeObserver.observe(scrollRef.value)
|
||||
setupIO(scrollRef.value)
|
||||
}
|
||||
const initialPage = progress.pageNumber.value && progress.pageNumber.value > 1 ? progress.pageNumber.value : 1
|
||||
goToPage(initialPage, 'instant')
|
||||
await renderPage(initialPage)
|
||||
|
||||
if (progress.pageNumber.value && progress.pageNumber.value > 1) {
|
||||
await nextTick()
|
||||
goToPage(progress.pageNumber.value, 'instant')
|
||||
resizeObserver = new ResizeObserver(async () => {
|
||||
measure()
|
||||
invalidate()
|
||||
await nextTick()
|
||||
if (scrollRef.value) setupIO(scrollRef.value)
|
||||
})
|
||||
|
||||
if (scrollRef.value) {
|
||||
resizeObserver.observe(scrollRef.value)
|
||||
setupIO(scrollRef.value)
|
||||
}
|
||||
|
||||
readerReady.value = true
|
||||
outline.load().catch(() => {})
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to initialize PDF reader'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -286,6 +292,13 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="fixed inset-0 flex flex-col overflow-hidden select-none bg-muted">
|
||||
<div v-if="isReaderLoading" class="absolute inset-0 z-[60] flex items-center justify-center bg-background">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-full border-2 border-primary border-t-transparent animate-spin" />
|
||||
<p class="text-sm text-muted-foreground">Loading PDF...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar (always visible) -->
|
||||
<PdfToolbar
|
||||
:current-page="currentPage"
|
||||
@@ -348,14 +361,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- PDF Viewport -->
|
||||
<div class="flex-1 min-w-0 relative">
|
||||
<div v-if="loading" class="absolute inset-0 flex items-center justify-center bg-background">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-full border-2 border-primary border-t-transparent animate-spin" />
|
||||
<p class="text-sm text-muted-foreground">Loading PDF...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="absolute inset-0 flex items-center justify-center p-8 text-center bg-background">
|
||||
<div v-if="error" class="absolute inset-0 flex items-center justify-center p-8 text-center bg-background">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground mb-1">Failed to load PDF</p>
|
||||
<p class="text-xs text-muted-foreground">{{ error }}</p>
|
||||
@@ -363,7 +369,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
v-show="!error"
|
||||
ref="scrollRef"
|
||||
class="absolute inset-0"
|
||||
:class="cursorTool === 'hand' ? 'cursor-grab active:cursor-grabbing' : 'cursor-auto'"
|
||||
@@ -454,7 +460,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div v-if="!loading && !error && totalPages > 0" class="absolute bottom-0 left-0 right-0 h-0.5 bg-border z-10">
|
||||
<div v-if="readerReady && !error && totalPages > 0" class="absolute bottom-0 left-0 right-0 h-0.5 bg-border z-10">
|
||||
<div class="h-full bg-primary/60 transition-all duration-500" :style="{ width: `${progressPct}%` }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ const emit = defineEmits<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col shrink-0 overflow-hidden border-r border-border bg-card" style="width: 240px">
|
||||
<div class="flex flex-col shrink-0 overflow-hidden border-r border-border bg-card px-1.5 w-64">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-1.5 px-3 py-2.5 shrink-0 text-xs text-muted-foreground border-b border-border">
|
||||
<List :size="12" />
|
||||
|
||||
@@ -46,6 +46,12 @@ export function usePdfRenderer(
|
||||
flushQueue()
|
||||
}
|
||||
|
||||
async function renderPage(pageNum: number) {
|
||||
if (pageNum < 1 || pageNum > totalPages.value || rendered.has(pageNum)) return
|
||||
renderQueue = [pageNum, ...renderQueue.filter((n) => n !== pageNum)]
|
||||
await flushQueue()
|
||||
}
|
||||
|
||||
async function flushQueue() {
|
||||
if (isRendering) return
|
||||
isRendering = true
|
||||
@@ -147,5 +153,5 @@ export function usePdfRenderer(
|
||||
for (const task of activeRenders.values()) task.cancel()
|
||||
}
|
||||
|
||||
return { canvasMap, textLayerMap, invalidate, setupIO, reset, destroy }
|
||||
return { canvasMap, textLayerMap, invalidate, renderPage, setupIO, reset, destroy }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user