fix location and use select listener
This commit is contained in:
parent
ea373332bb
commit
85c3099b5f
3 changed files with 540 additions and 380 deletions
|
|
@ -24,7 +24,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, toRefs, watch, unref } from 'vue'
|
import { ref, onMounted, onUnmounted, toRefs, watch, unref, reactive, computed, nextTick } from 'vue'
|
||||||
import ePub from 'epubjs'
|
import ePub from 'epubjs'
|
||||||
import type { Book, Rendition, Contents } from 'epubjs'
|
import type { Book, Rendition, Contents } from 'epubjs'
|
||||||
import {
|
import {
|
||||||
|
|
@ -32,6 +32,7 @@ import {
|
||||||
swipListener,
|
swipListener,
|
||||||
wheelListener,
|
wheelListener,
|
||||||
keyListener,
|
keyListener,
|
||||||
|
selectListener
|
||||||
} from '../utils/listeners/listener'
|
} from '../utils/listeners/listener'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -41,9 +42,11 @@ interface Props {
|
||||||
getRendition?: (rendition: Rendition) => void
|
getRendition?: (rendition: Rendition) => void
|
||||||
handleTextSelected?: (cfiRange: string, contents: Contents) => void
|
handleTextSelected?: (cfiRange: string, contents: Contents) => void
|
||||||
handleKeyPress?: () => void
|
handleKeyPress?: () => void
|
||||||
|
toggleBubble?: (type: string, rect?: any, text?: string, cfiRange?: string) => void // For custom selection
|
||||||
epubInitOptions?: Book['settings']
|
epubInitOptions?: Book['settings']
|
||||||
epubOptions?: Rendition['settings']
|
epubOptions?: Rendition['settings']
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
epubInitOptions: () => ({}),
|
epubInitOptions: () => ({}),
|
||||||
epubOptions: () => ({}),
|
epubOptions: () => ({}),
|
||||||
|
|
@ -54,6 +57,7 @@ const {
|
||||||
getRendition,
|
getRendition,
|
||||||
handleTextSelected,
|
handleTextSelected,
|
||||||
handleKeyPress,
|
handleKeyPress,
|
||||||
|
toggleBubble,
|
||||||
epubInitOptions,
|
epubInitOptions,
|
||||||
epubOptions,
|
epubOptions,
|
||||||
} = props
|
} = props
|
||||||
|
|
@ -69,13 +73,50 @@ const isLoaded = ref(false)
|
||||||
let book: null | Book = null,
|
let book: null | Book = null,
|
||||||
rendition: null | Rendition = null
|
rendition: null | Rendition = null
|
||||||
|
|
||||||
const initBook = async () => {
|
// Create reactive state for tracking loading states
|
||||||
|
const loadingState = reactive({
|
||||||
|
bookInitialized: false,
|
||||||
|
bookReady: false,
|
||||||
|
renditionCreated: false,
|
||||||
|
firstContentDisplayed: false,
|
||||||
|
locationApplied: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed property to determine if we're ready to apply location
|
||||||
|
const readyToApplyLocation = computed(() => {
|
||||||
|
return loadingState.bookReady &&
|
||||||
|
loadingState.renditionCreated &&
|
||||||
|
loadingState.firstContentDisplayed;
|
||||||
|
})
|
||||||
|
|
||||||
|
// Store pending location for restoration
|
||||||
|
const pendingLocation = ref<string | null>(null)
|
||||||
|
const isFirstRender = ref(true)
|
||||||
|
|
||||||
|
const initBook = async () => {
|
||||||
if (book) book.destroy()
|
if (book) book.destroy()
|
||||||
|
|
||||||
|
// Reset loading states
|
||||||
|
loadingState.bookInitialized = false
|
||||||
|
loadingState.bookReady = false
|
||||||
|
loadingState.renditionCreated = false
|
||||||
|
loadingState.firstContentDisplayed = false
|
||||||
|
loadingState.locationApplied = false
|
||||||
|
isFirstRender.value = true
|
||||||
|
|
||||||
if (url.value) {
|
if (url.value) {
|
||||||
try {
|
try {
|
||||||
book = ePub(unref(url.value), epubInitOptions)
|
book = ePub(unref(url.value), epubInitOptions)
|
||||||
|
loadingState.bookInitialized = true
|
||||||
|
|
||||||
book!.ready.then(() => {
|
// Set pending location if provided in props
|
||||||
|
if (location.value && typeof location.value === 'string' && location.value.includes('epubcfi')) {
|
||||||
|
//console.log("Storing pending location:", location.value)
|
||||||
|
pendingLocation.value = location.value
|
||||||
|
}
|
||||||
|
|
||||||
|
book.ready.then(() => {
|
||||||
|
loadingState.bookReady = true
|
||||||
return book!.loaded.navigation
|
return book!.loaded.navigation
|
||||||
}).then(({ toc: _toc }) => {
|
}).then(({ toc: _toc }) => {
|
||||||
isLoaded.value = true
|
isLoaded.value = true
|
||||||
|
|
@ -92,7 +133,7 @@ let book: null | Book = null,
|
||||||
console.error('Error initializing book:', error)
|
console.error('Error initializing book:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initReader = () => {
|
const initReader = () => {
|
||||||
if (!book) return
|
if (!book) return
|
||||||
|
|
@ -103,7 +144,11 @@ const initReader = () => {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
...epubOptions,
|
...epubOptions,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
loadingState.renditionCreated = true
|
||||||
|
|
||||||
if (rendition && book) {
|
if (rendition && book) {
|
||||||
|
// Fix spine handling for better navigation
|
||||||
const spine_get = book.spine.get.bind(book.spine)
|
const spine_get = book.spine.get.bind(book.spine)
|
||||||
book.spine.get = function(target: any) {
|
book.spine.get = function(target: any) {
|
||||||
let t = spine_get(target)
|
let t = spine_get(target)
|
||||||
|
|
@ -114,16 +159,16 @@ const initReader = () => {
|
||||||
}
|
}
|
||||||
// Try to find by href match
|
// Try to find by href match
|
||||||
if (!t && typeof target === 'string') {
|
if (!t && typeof target === 'string') {
|
||||||
let i = 0;
|
let i = 0
|
||||||
let spineItem = book!.spine.get(i);
|
let spineItem = book!.spine.get(i)
|
||||||
// Iterate through spine items until we find a match or reach the end
|
// Iterate through spine items until we find a match or reach the end
|
||||||
while (spineItem) {
|
while (spineItem) {
|
||||||
if (spineItem.href === target ||
|
if (spineItem.href === target ||
|
||||||
spineItem.href.endsWith(target)) {
|
spineItem.href.endsWith(target)) {
|
||||||
return spineItem;
|
return spineItem
|
||||||
}
|
}
|
||||||
i++;
|
i++
|
||||||
spineItem = book!.spine.get(i);
|
spineItem = book!.spine.get(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return t
|
return t
|
||||||
|
|
@ -133,24 +178,8 @@ const initReader = () => {
|
||||||
registerEvents()
|
registerEvents()
|
||||||
getRendition && getRendition(rendition)
|
getRendition && getRendition(rendition)
|
||||||
|
|
||||||
if (typeof location?.value === 'string') {
|
// Always start with default content
|
||||||
rendition.display(location.value).catch(err => {
|
|
||||||
console.error('Error displaying location:', err)
|
|
||||||
displayFallback()
|
displayFallback()
|
||||||
})
|
|
||||||
} else if (typeof location?.value === 'number') {
|
|
||||||
rendition.display(location.value).catch(err => {
|
|
||||||
console.error('Error displaying page number:', err)
|
|
||||||
displayFallback()
|
|
||||||
})
|
|
||||||
} else if (toc.value.length > 0 && toc?.value[0]?.href) {
|
|
||||||
rendition.display(toc.value[0].href).catch(err => {
|
|
||||||
console.error('Error displaying TOC:', err)
|
|
||||||
displayFallback()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
displayFallback()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing reader:', error)
|
console.error('Error initializing reader:', error)
|
||||||
}
|
}
|
||||||
|
|
@ -158,6 +187,7 @@ const initReader = () => {
|
||||||
|
|
||||||
const displayFallback = () => {
|
const displayFallback = () => {
|
||||||
if (!rendition) return
|
if (!rendition) return
|
||||||
|
|
||||||
// Try to display with empty parameter
|
// Try to display with empty parameter
|
||||||
rendition.display().catch(err => {
|
rendition.display().catch(err => {
|
||||||
console.error('Error with default display:', err)
|
console.error('Error with default display:', err)
|
||||||
|
|
@ -182,37 +212,92 @@ const flipPage = (direction: string) => {
|
||||||
|
|
||||||
const registerEvents = () => {
|
const registerEvents = () => {
|
||||||
if (rendition) {
|
if (rendition) {
|
||||||
rendition.on('rendered', (e: Event, iframe: any) => {
|
rendition.on('rendered', (section, iframe) => {
|
||||||
|
// Focus the iframe
|
||||||
iframe?.iframe?.contentWindow.focus()
|
iframe?.iframe?.contentWindow.focus()
|
||||||
// clickListener(iframe?.document, rendition as Rendition, flipPage);
|
|
||||||
// selectListener(iframe.document, rendition, toggleBuble);
|
// Register interaction listeners
|
||||||
if (!epubOptions?.flow?.includes('scrolled'))
|
if (!epubOptions?.flow?.includes('scrolled')) {
|
||||||
wheelListener(iframe.document, flipPage)
|
wheelListener(iframe.document, flipPage)
|
||||||
|
}
|
||||||
swipListener(iframe.document, flipPage)
|
swipListener(iframe.document, flipPage)
|
||||||
keyListener(iframe.document, flipPage)
|
keyListener(iframe.document, flipPage)
|
||||||
|
|
||||||
|
// Register your custom selection listener if toggleBubble is provided
|
||||||
|
if (toggleBubble) {
|
||||||
|
selectListener(iframe.document, rendition, toggleBubble)
|
||||||
|
} else if (handleTextSelected) {
|
||||||
|
// If no toggleBubble but handleTextSelected exists, use the built-in selection event
|
||||||
|
rendition.on('selected', handleTextSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark first content as displayed for location restoration
|
||||||
|
if (!loadingState.firstContentDisplayed) {
|
||||||
|
loadingState.firstContentDisplayed = true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Location change tracking
|
||||||
rendition.on('locationChanged', onLocationChange)
|
rendition.on('locationChanged', onLocationChange)
|
||||||
|
|
||||||
|
rendition.on('relocated', (location: any) => {
|
||||||
|
// console.log('Book relocated to:', location)
|
||||||
|
})
|
||||||
|
|
||||||
rendition.on('displayError', (err: any) => {
|
rendition.on('displayError', (err: any) => {
|
||||||
console.error('Display error:', err)
|
console.error('Display error:', err)
|
||||||
})
|
})
|
||||||
if (handleTextSelected) {
|
|
||||||
rendition.on('selected', handleTextSelected)
|
|
||||||
}
|
|
||||||
if (handleKeyPress) {
|
if (handleKeyPress) {
|
||||||
rendition.on('selected', handleKeyPress)
|
rendition.on('keypress', handleKeyPress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to apply saved location
|
||||||
|
const applyPendingLocation = () => {
|
||||||
|
if (!rendition || !pendingLocation.value) return
|
||||||
|
//console.log("Applying pending location:", pendingLocation.value)
|
||||||
|
try {
|
||||||
|
rendition.display(pendingLocation.value).then(() => {
|
||||||
|
//console.log("Location applied successfully")
|
||||||
|
loadingState.locationApplied = true
|
||||||
|
pendingLocation.value = null
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Error displaying location:', err)
|
||||||
|
pendingLocation.value = null
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error applying location:', error)
|
||||||
|
pendingLocation.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onLocationChange = (loc: Rendition['location']) => {
|
const onLocationChange = (loc: Rendition['location']) => {
|
||||||
const newLocation = loc && loc.start
|
// Skip the event during the initial location restoration
|
||||||
|
if (isFirstRender.value && pendingLocation.value) {
|
||||||
|
isFirstRender.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loc || !loc.start) return
|
||||||
|
|
||||||
|
const newLocation = loc.start
|
||||||
|
//console.log('Location changed to:', newLocation)
|
||||||
|
|
||||||
if (location?.value !== newLocation) {
|
if (location?.value !== newLocation) {
|
||||||
emit('update:location', newLocation)
|
emit('update:location', newLocation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(url, () => {
|
// Watch for changes in readyToApplyLocation
|
||||||
initBook()
|
watch(readyToApplyLocation, (isReady) => {
|
||||||
|
if (isReady && pendingLocation.value && !loadingState.locationApplied) {
|
||||||
|
// Use nextTick to ensure DOM is updated
|
||||||
|
nextTick(() => {
|
||||||
|
applyPendingLocation()
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const nextPage = () => {
|
const nextPage = () => {
|
||||||
|
|
@ -228,6 +313,16 @@ const setLocation = (href: number | string) => {
|
||||||
if (typeof href === 'number') rendition!.display(href)
|
if (typeof href === 'number') rendition!.display(href)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setExactLocation = (cfi: string) => {
|
||||||
|
if (!rendition) return
|
||||||
|
|
||||||
|
console.log('Setting exact location:', cfi)
|
||||||
|
|
||||||
|
rendition.display(cfi).catch(err => {
|
||||||
|
console.error('Error setting exact location:', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initBook()
|
initBook()
|
||||||
})
|
})
|
||||||
|
|
@ -240,6 +335,7 @@ defineExpose({
|
||||||
nextPage,
|
nextPage,
|
||||||
prevPage,
|
prevPage,
|
||||||
setLocation,
|
setLocation,
|
||||||
|
setExactLocation,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
175
src/components/TocComponent.vue
Normal file
175
src/components/TocComponent.vue
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-for="(item, index) in toc" :key="index">
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
'toc-area-button',
|
||||||
|
item.href === current ? 'active' : '',
|
||||||
|
]"
|
||||||
|
@click="handleClick(item)"
|
||||||
|
>
|
||||||
|
<span v-if="isSubmenu" class="submenu-indent">{{ item.label }}</span>
|
||||||
|
<span v-else>{{ item.label }}</span>
|
||||||
|
<div
|
||||||
|
v-if="item.subitems.length > 0"
|
||||||
|
:class="['expansion', { 'open': item.expansion }]"
|
||||||
|
></div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Nested TOC -->
|
||||||
|
<Transition name="collapse-transition">
|
||||||
|
<div
|
||||||
|
v-if="item.subitems.length > 0"
|
||||||
|
v-show="item.expansion"
|
||||||
|
>
|
||||||
|
<TocComponent
|
||||||
|
:toc="item.subitems"
|
||||||
|
:current="current"
|
||||||
|
:setLocation="setLocation"
|
||||||
|
:isSubmenu="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
toc: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
current: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
setLocation: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isSubmenu: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle item click
|
||||||
|
const handleClick = (item) => {
|
||||||
|
if (item.subitems && item.subitems.length > 0) {
|
||||||
|
// Toggle expansion and navigate, but don't close TOC
|
||||||
|
item.expansion = !item.expansion;
|
||||||
|
props.setLocation(item.href, false);
|
||||||
|
} else {
|
||||||
|
// No subitems, just navigate and close TOC
|
||||||
|
props.setLocation(item.href);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
/* TOC area styles */
|
||||||
|
.toc-area {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10;
|
||||||
|
width: 256px;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
padding: 6px 0;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border-right: 1px solid var(--divider-color);
|
||||||
|
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-area::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-area::-webkit-scrollbar-thumb:vertical {
|
||||||
|
height: 5px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-area .toc-area-button {
|
||||||
|
user-select: none;
|
||||||
|
appearance: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
font-family: sans-serif;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 0.9em;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.9em 1em;
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
box-sizing: border-box;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-area .toc-area-button:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-area .toc-area-button:active {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-area .active {
|
||||||
|
border-left: 3px solid var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-area .toc-area-button .expansion {
|
||||||
|
cursor: pointer;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
top: 50%;
|
||||||
|
right: 12px;
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
background-color: #a2a5b4;
|
||||||
|
transition: top 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-area .toc-area-button .expansion::after,
|
||||||
|
.toc-area .toc-area-button .expansion::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 6px;
|
||||||
|
height: 2px;
|
||||||
|
background-color: currentcolor;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: transform 0.3s ease-in-out, top 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-area .toc-area-button .expansion::before {
|
||||||
|
transform: rotate(-45deg) translateX(2.5px);
|
||||||
|
}
|
||||||
|
.toc-area .toc-area-button .open::before {
|
||||||
|
transform: rotate(45deg) translateX(2.5px);
|
||||||
|
}
|
||||||
|
.toc-area .toc-area-button .expansion::after {
|
||||||
|
transform: rotate(45deg) translateX(-2.5px);
|
||||||
|
}
|
||||||
|
.toc-area .toc-area-button .open::after {
|
||||||
|
transform: rotate(-45deg) translateX(-2.5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TOC background overlay */
|
||||||
|
.toc-background {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- ReaderView.vue (Updated with separated components) -->
|
|
||||||
<template>
|
<template>
|
||||||
<div class="reader-container">
|
<div class="reader-container">
|
||||||
<div
|
<div
|
||||||
|
|
@ -39,11 +38,8 @@
|
||||||
@update:location="locationChange"
|
@update:location="locationChange"
|
||||||
:tocChanged="onTocChange"
|
:tocChanged="onTocChange"
|
||||||
:getRendition="getRendition"
|
:getRendition="getRendition"
|
||||||
|
:toggleBubble="toggleSelectionBubble"
|
||||||
/>
|
/>
|
||||||
<!--:epubOptions="{
|
|
||||||
flow: 'scrolled',
|
|
||||||
manager: 'continuous',
|
|
||||||
}" -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -59,7 +55,7 @@
|
||||||
<div v-if="expandedToc" class="toc-background" @click="toggleToc"></div>
|
<div v-if="expandedToc" class="toc-background" @click="toggleToc"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Annotations Panel (imported component) -->
|
<!-- Annotations Panel -->
|
||||||
<AnnotationsPanel
|
<AnnotationsPanel
|
||||||
:annotations="savedAnnotations"
|
:annotations="savedAnnotations"
|
||||||
:is-visible="showAnnotationsPanel"
|
:is-visible="showAnnotationsPanel"
|
||||||
|
|
@ -69,7 +65,7 @@
|
||||||
@delete="deleteAnnotation"
|
@delete="deleteAnnotation"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Annotation Modal (imported component) -->
|
<!-- Annotation Modal -->
|
||||||
<AnnotationModal
|
<AnnotationModal
|
||||||
:is-open="showAnnotationModal"
|
:is-open="showAnnotationModal"
|
||||||
:selected-text="pendingAnnotation?.text || ''"
|
:selected-text="pendingAnnotation?.text || ''"
|
||||||
|
|
@ -80,6 +76,7 @@
|
||||||
@save="handleAnnotationSave"
|
@save="handleAnnotationSave"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Styles Modal -->
|
||||||
<StylesModal
|
<StylesModal
|
||||||
v-model:text-color="textColor"
|
v-model:text-color="textColor"
|
||||||
v-model:background-color="backgroundColor"
|
v-model:background-color="backgroundColor"
|
||||||
|
|
@ -95,21 +92,21 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
ref, reactive, onMounted, onUnmounted, toRefs, h,
|
ref, reactive, onMounted, onUnmounted, toRefs, watch, nextTick
|
||||||
getCurrentInstance, Transition, nextTick
|
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useI18n } from "../i18n/usei18n";
|
import { useI18n } from "../i18n/usei18n";
|
||||||
|
import EpubView from "../components/EpubView.vue";
|
||||||
import StylesModal from "../components/StylesModal.vue";
|
import StylesModal from "../components/StylesModal.vue";
|
||||||
import StylesButton from "../components/StylesButton.vue";
|
import StylesButton from "../components/StylesButton.vue";
|
||||||
import AnnotationsPanel from "../components/AnnotationsPanel.vue";
|
import AnnotationsPanel from "../components/AnnotationsPanel.vue";
|
||||||
import AnnotationModal from "../components/AnnotationModal.vue";
|
import AnnotationModal from "../components/AnnotationModal.vue";
|
||||||
import AnnotationsButton from "../components/AnnotationsButton.vue";
|
import AnnotationsButton from "../components/AnnotationsButton.vue";
|
||||||
|
import TocComponent from "../components/TocComponent.vue";
|
||||||
import { useStyles } from "../composables/useStyles";
|
import { useStyles } from "../composables/useStyles";
|
||||||
import { loadBookFromIndexedDB } from "../utils/utils";
|
import { loadBookFromIndexedDB } from "../utils/utils";
|
||||||
import EpubView from "../components/EpubView.vue";
|
import type { EpubFile } from "../types/epubFile";
|
||||||
import { type EpubFile } from "../types/epubFile";
|
import type { Annotation, PendingAnnotation, AnnotationFormData } from "../types/annotations";
|
||||||
import { type Annotation, type PendingAnnotation, type AnnotationFormData } from "../types/annotations";
|
|
||||||
|
|
||||||
// Import epub.js types
|
// Import epub.js types
|
||||||
import type Rendition from 'epubjs/types/rendition';
|
import type Rendition from 'epubjs/types/rendition';
|
||||||
|
|
@ -135,75 +132,6 @@ interface RelocatedEvent {
|
||||||
atEnd: boolean;
|
atEnd: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TocComponent definition
|
|
||||||
const TocComponent = (props: {
|
|
||||||
toc: Array<ExtendedNavItem>;
|
|
||||||
current: string | number;
|
|
||||||
setLocation: (href: string | number, close?: boolean) => void;
|
|
||||||
isSubmenu?: boolean;
|
|
||||||
}) => {
|
|
||||||
const vm = getCurrentInstance();
|
|
||||||
const renderH = h.bind(vm);
|
|
||||||
|
|
||||||
return renderH(
|
|
||||||
"div",
|
|
||||||
null,
|
|
||||||
props.toc.map((item, index) => {
|
|
||||||
return renderH("div", { key: index }, [
|
|
||||||
renderH(
|
|
||||||
"button",
|
|
||||||
{
|
|
||||||
class: [
|
|
||||||
"toc-area-button",
|
|
||||||
item.href === props.current ? "active" : "",
|
|
||||||
],
|
|
||||||
onClick: () => {
|
|
||||||
if (item.subitems.length > 0) {
|
|
||||||
item.expansion = !item.expansion;
|
|
||||||
props.setLocation(item.href, false);
|
|
||||||
} else {
|
|
||||||
props.setLocation(item.href);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[
|
|
||||||
props.isSubmenu ? " ".repeat(4) + item.label : item.label,
|
|
||||||
item.subitems.length > 0 &&
|
|
||||||
renderH("div", {
|
|
||||||
class: `${item.expansion ? "open" : ""} expansion`,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
// Nested TOC
|
|
||||||
item.subitems.length > 0 &&
|
|
||||||
renderH(
|
|
||||||
Transition,
|
|
||||||
{ name: "collapse-transition" },
|
|
||||||
{
|
|
||||||
default: () =>
|
|
||||||
renderH(
|
|
||||||
"div",
|
|
||||||
{
|
|
||||||
style: {
|
|
||||||
display: item.expansion ? undefined : "none",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[
|
|
||||||
renderH(TocComponent, {
|
|
||||||
toc: item.subitems,
|
|
||||||
current: props.current,
|
|
||||||
setLocation: props.setLocation,
|
|
||||||
isSubmenu: true,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup state
|
// Setup state
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
@ -219,6 +147,13 @@ const showToc = ref<boolean>(true);
|
||||||
const epubRef = ref<InstanceType<typeof EpubView> | null>(null);
|
const epubRef = ref<InstanceType<typeof EpubView> | null>(null);
|
||||||
const currentHref = ref<string | number | null>(null);
|
const currentHref = ref<string | number | null>(null);
|
||||||
|
|
||||||
|
const selectionBubble = reactive({
|
||||||
|
visible: false,
|
||||||
|
position: { left: '0px', top: '0px', width: '0px', height: '0px' },
|
||||||
|
selectedText: '',
|
||||||
|
cfiRange: ''
|
||||||
|
});
|
||||||
|
|
||||||
// Annotation state
|
// Annotation state
|
||||||
const savedAnnotations = ref<Annotation[]>([]);
|
const savedAnnotations = ref<Annotation[]>([]);
|
||||||
const pendingAnnotation = ref<PendingAnnotation | null>(null);
|
const pendingAnnotation = ref<PendingAnnotation | null>(null);
|
||||||
|
|
@ -235,15 +170,167 @@ const bookState = reactive({
|
||||||
});
|
});
|
||||||
const { toc, expandedToc } = toRefs(bookState);
|
const { toc, expandedToc } = toRefs(bookState);
|
||||||
|
|
||||||
|
// Styles and rendition
|
||||||
const {
|
const {
|
||||||
textColor, backgroundColor, accentColor,
|
textColor, backgroundColor, accentColor,
|
||||||
fontFamily, fontSize, stylesModalOpen,
|
fontFamily, fontSize, stylesModalOpen,
|
||||||
toggleStylesModal, rendition, setRendition,
|
toggleStylesModal, rendition, setRendition,
|
||||||
} = useStyles();
|
} = useStyles();
|
||||||
|
|
||||||
// Toggle annotations panel
|
const BookProgressManager = {
|
||||||
const toggleAnnotationsPanel = () => {
|
saveProgress(bookId: string, cfi: string, extraData = {}) {
|
||||||
showAnnotationsPanel.value = !showAnnotationsPanel.value;
|
try {
|
||||||
|
const progressKey = `book-progress-${bookId}`;
|
||||||
|
const data = {
|
||||||
|
cfi,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
...extraData
|
||||||
|
};
|
||||||
|
localStorage.setItem(progressKey, JSON.stringify(data));
|
||||||
|
console.log(`Progress saved for book ${bookId}:`, data);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving book progress:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loadProgress(bookId: string) {
|
||||||
|
try {
|
||||||
|
const progressKey = `book-progress-${bookId}`;
|
||||||
|
const data = localStorage.getItem(progressKey);
|
||||||
|
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
//console.log(`Progress loaded for book ${bookId}:`, parsed);
|
||||||
|
return parsed;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading book progress:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearProgress(bookId: string) {
|
||||||
|
const progressKey = `book-progress-${bookId}`;
|
||||||
|
localStorage.removeItem(progressKey);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The custom selection bubble toggle function to pass to EpubView
|
||||||
|
const toggleSelectionBubble = (type, rect, text, cfiRange) => {
|
||||||
|
if (type === 'selected' && text && text.length > 0) {
|
||||||
|
selectionBubble.visible = true;
|
||||||
|
selectionBubble.position = rect;
|
||||||
|
selectionBubble.selectedText = text;
|
||||||
|
selectionBubble.cfiRange = cfiRange;
|
||||||
|
|
||||||
|
// Create pending annotation to be used when the user wants to save
|
||||||
|
pendingAnnotation.value = {
|
||||||
|
cfiRange,
|
||||||
|
text,
|
||||||
|
contents: rendition.value.getContents()[0]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show annotation modal directly
|
||||||
|
showAnnotationModal.value = true;
|
||||||
|
|
||||||
|
// Clear any selection after capturing it
|
||||||
|
if (rendition.value) {
|
||||||
|
const contents = rendition.value.getContents();
|
||||||
|
contents.forEach(content => {
|
||||||
|
if (content.window && content.window.getSelection) {
|
||||||
|
content.window.getSelection()?.removeAllRanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (type === 'cleared') {
|
||||||
|
selectionBubble.visible = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadBook = async (): Promise<void> => {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bookId = route.params.bookId as string;
|
||||||
|
if (!bookId) throw new Error("No book ID provided.");
|
||||||
|
|
||||||
|
const book: EpubFile = await loadBookFromIndexedDB(bookId);
|
||||||
|
|
||||||
|
// Load book data
|
||||||
|
if (book.data instanceof Blob) {
|
||||||
|
bookData.value = await book.data.arrayBuffer();
|
||||||
|
const blob = new Blob([bookData.value]);
|
||||||
|
bookDataUrl.value = URL.createObjectURL(blob);
|
||||||
|
} else if (book.data instanceof ArrayBuffer) {
|
||||||
|
bookData.value = book.data;
|
||||||
|
const blob = new Blob([bookData.value]);
|
||||||
|
bookDataUrl.value = URL.createObjectURL(blob);
|
||||||
|
} else {
|
||||||
|
throw new Error("Book data is in an unsupported format");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load progress after book data is ready
|
||||||
|
const progress = BookProgressManager.loadProgress(bookId);
|
||||||
|
if (progress && progress.cfi) {
|
||||||
|
location.value = progress.cfi;
|
||||||
|
//console.log("Setting initial location from localStorage:", location.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAnnotations();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error("Error loading book:", err);
|
||||||
|
error.value = `Failed to load the book. ${errorMsg}`;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle location changes
|
||||||
|
const locationChange = (epubcifi: string): void => {
|
||||||
|
// Skip saving the location on the first render to prevent
|
||||||
|
// overriding our saved location
|
||||||
|
if (!firstRenderDone.value) {
|
||||||
|
//console.log("## first render");
|
||||||
|
firstRenderDone.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only save valid locations with epubcfi format
|
||||||
|
if (epubcifi && epubcifi.includes('epubcfi')) {
|
||||||
|
const bookId = route.params.bookId as string;
|
||||||
|
|
||||||
|
BookProgressManager.saveProgress(bookId, epubcifi, {
|
||||||
|
chapter: currentHref.value?.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
location.value = epubcifi;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRendition = (renditionObj: Rendition): void => {
|
||||||
|
setRendition(renditionObj);
|
||||||
|
|
||||||
|
renditionObj.on("relocated", (location: RelocatedEvent) => {
|
||||||
|
currentHref.value = location.start.href;
|
||||||
|
});
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
applyAnnotationsToView();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get book metadata
|
||||||
|
const book: Book = renditionObj.book;
|
||||||
|
book.ready.then(() => {
|
||||||
|
const meta = book.packaging?.metadata;
|
||||||
|
if (!bookTitle.value && meta?.title) {
|
||||||
|
bookTitle.value = meta.title;
|
||||||
|
document.title = meta.title;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Annotation storage functions
|
// Annotation storage functions
|
||||||
|
|
@ -277,56 +364,6 @@ const saveAnnotationsToStorage = (): void => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateAnnotationId = (): string => {
|
|
||||||
return `annotation-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadBook = async (): Promise<void> => {
|
|
||||||
loading.value = true;
|
|
||||||
error.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const bookId = route.params.bookId as string;
|
|
||||||
if (!bookId) throw new Error("No book ID provided.");
|
|
||||||
|
|
||||||
const book: EpubFile = await loadBookFromIndexedDB(bookId);
|
|
||||||
|
|
||||||
const progressKey = `book-progress-${bookId}`;
|
|
||||||
const savedLocation = localStorage.getItem(progressKey);
|
|
||||||
if (savedLocation) location.value = savedLocation;
|
|
||||||
|
|
||||||
if (book.data instanceof Blob) {
|
|
||||||
bookData.value = await book.data.arrayBuffer();
|
|
||||||
const blob = new Blob([bookData.value]);
|
|
||||||
bookDataUrl.value = URL.createObjectURL(blob);
|
|
||||||
} else if (book.data instanceof ArrayBuffer) {
|
|
||||||
bookData.value = book.data;
|
|
||||||
const blob = new Blob([bookData.value]);
|
|
||||||
bookDataUrl.value = URL.createObjectURL(blob);
|
|
||||||
} else {
|
|
||||||
throw new Error("Book data is in an unsupported format");
|
|
||||||
}
|
|
||||||
|
|
||||||
loadAnnotations();
|
|
||||||
} catch (err: unknown) {
|
|
||||||
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
||||||
console.error("Error loading book:", err);
|
|
||||||
error.value = `Failed to load the book. ${errorMsg}`;
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const locationChange = (epubcifi: string): void => {
|
|
||||||
if (!firstRenderDone.value) {
|
|
||||||
firstRenderDone.value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const bookId = route.params.bookId as string;
|
|
||||||
localStorage.setItem(`book-progress-${bookId}`, epubcifi);
|
|
||||||
location.value = epubcifi;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply annotations to view
|
// Apply annotations to view
|
||||||
const applyAnnotationsToView = async (): Promise<void> => {
|
const applyAnnotationsToView = async (): Promise<void> => {
|
||||||
if (!rendition.value || savedAnnotations.value.length === 0) return;
|
if (!rendition.value || savedAnnotations.value.length === 0) return;
|
||||||
|
|
@ -363,35 +400,6 @@ const applyAnnotationsToView = async (): Promise<void> => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle text selection
|
|
||||||
const handleSelection = (cfiRange: string, contents: Contents): void => {
|
|
||||||
try {
|
|
||||||
if (!rendition.value) return;
|
|
||||||
|
|
||||||
const range = rendition.value.getRange(cfiRange);
|
|
||||||
const selectedText = range.toString().trim();
|
|
||||||
|
|
||||||
if (!selectedText || selectedText.length < 3) {
|
|
||||||
console.log('Selection too short, ignoring');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingAnnotation.value = {
|
|
||||||
cfiRange,
|
|
||||||
text: selectedText,
|
|
||||||
contents
|
|
||||||
};
|
|
||||||
|
|
||||||
showAnnotationModal.value = true;
|
|
||||||
|
|
||||||
if (contents.window && contents.window.getSelection) {
|
|
||||||
contents.window.getSelection()?.removeAllRanges();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error handling selection:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle annotation save from modal
|
// Handle annotation save from modal
|
||||||
const handleAnnotationSave = (formData: AnnotationFormData): void => {
|
const handleAnnotationSave = (formData: AnnotationFormData): void => {
|
||||||
if (!pendingAnnotation.value) return;
|
if (!pendingAnnotation.value) return;
|
||||||
|
|
@ -446,7 +454,8 @@ const handleAnnotationSave = (formData: AnnotationFormData): void => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
saveAnnotationsToStorage();
|
|
||||||
|
saveAnnotationsToStorage();
|
||||||
closeAnnotationModal();
|
closeAnnotationModal();
|
||||||
|
|
||||||
// Show the annotations panel after creating a new annotation
|
// Show the annotations panel after creating a new annotation
|
||||||
|
|
@ -467,6 +476,11 @@ const closeAnnotationModal = (): void => {
|
||||||
editingAnnotation.value = null;
|
editingAnnotation.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Generate annotation ID
|
||||||
|
const generateAnnotationId = (): string => {
|
||||||
|
return `annotation-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
};
|
||||||
|
|
||||||
// Go to annotation
|
// Go to annotation
|
||||||
const goToAnnotation = (cfiRange: string): void => {
|
const goToAnnotation = (cfiRange: string): void => {
|
||||||
if (rendition.value) {
|
if (rendition.value) {
|
||||||
|
|
@ -509,39 +523,7 @@ const deleteAnnotation = (annotationId: string): void => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRendition = (renditionObj: Rendition): void => {
|
// Toggle TOC panel
|
||||||
setRendition(renditionObj);
|
|
||||||
|
|
||||||
// Track current location for TOC highlighting
|
|
||||||
renditionObj.on("relocated", (location: RelocatedEvent) => {
|
|
||||||
currentHref.value = location.start.href;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle text selection
|
|
||||||
renditionObj.on('selected', (cfiRange: string, contents: Contents) => {
|
|
||||||
handleSelection(cfiRange, contents);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply saved annotations when view is displayed
|
|
||||||
renditionObj.on('displayed', () => {
|
|
||||||
applyAnnotationsToView();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get book metadata
|
|
||||||
const book: Book = renditionObj.book;
|
|
||||||
book.ready.then(() => {
|
|
||||||
const meta = book.packaging?.metadata;
|
|
||||||
if (!bookTitle.value && meta?.title) {
|
|
||||||
bookTitle.value = meta.title;
|
|
||||||
document.title = meta.title;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const goBack = (): void => {
|
|
||||||
router.push("/");
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleToc = (): void => {
|
const toggleToc = (): void => {
|
||||||
expandedToc.value = !expandedToc.value;
|
expandedToc.value = !expandedToc.value;
|
||||||
if (expandedToc.value) {
|
if (expandedToc.value) {
|
||||||
|
|
@ -551,6 +533,12 @@ const toggleToc = (): void => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Toggle annotations panel
|
||||||
|
const toggleAnnotationsPanel = () => {
|
||||||
|
showAnnotationsPanel.value = !showAnnotationsPanel.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle key events
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
if (showAnnotationModal.value) {
|
if (showAnnotationModal.value) {
|
||||||
|
|
@ -595,23 +583,33 @@ const setLocation = (
|
||||||
expandedToc.value = !close;
|
expandedToc.value = !close;
|
||||||
};
|
};
|
||||||
|
|
||||||
// XHR Progress tracking
|
const debugStoredLocation = () => {
|
||||||
const originalOpen = XMLHttpRequest.prototype.open;
|
const bookId = route.params.bookId as string;
|
||||||
const onProgress = (e: ProgressEvent) => {
|
const progressKey = `book-progress-${bookId}`;
|
||||||
// Progress tracking if needed
|
const savedLocation = localStorage.getItem(progressKey);
|
||||||
};
|
|
||||||
|
|
||||||
XMLHttpRequest.prototype.open = function (
|
console.log('================ DEBUG INFO ================');
|
||||||
method: string,
|
console.log('Book ID:', bookId);
|
||||||
requestUrl: string | URL
|
console.log('Progress key:', progressKey);
|
||||||
) {
|
console.log('Saved location in localStorage:', savedLocation);
|
||||||
if (bookDataUrl.value && requestUrl.toString() === bookDataUrl.value) {
|
|
||||||
this.addEventListener("progress", onProgress);
|
// Check if the location format is valid
|
||||||
|
if (savedLocation) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(savedLocation);
|
||||||
|
console.log('Parsed location:', parsed);
|
||||||
|
console.log('Is valid CFI format:', typeof parsed.cfi === 'string' && parsed.cfi.includes('epubcfi'));
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Raw location string:', savedLocation);
|
||||||
|
console.log('Is valid CFI format:', savedLocation.includes('epubcfi'));
|
||||||
}
|
}
|
||||||
originalOpen.apply(this, arguments as any);
|
}
|
||||||
|
|
||||||
|
console.log('=========================================');
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// debugStoredLocation();
|
||||||
loadBook();
|
loadBook();
|
||||||
|
|
||||||
// Add keyboard shortcuts
|
// Add keyboard shortcuts
|
||||||
|
|
@ -623,7 +621,6 @@ onUnmounted(() => {
|
||||||
URL.revokeObjectURL(bookDataUrl.value);
|
URL.revokeObjectURL(bookDataUrl.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
XMLHttpRequest.prototype.open = originalOpen;
|
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -634,7 +631,6 @@ defineExpose({
|
||||||
bookData,
|
bookData,
|
||||||
bookTitle,
|
bookTitle,
|
||||||
location,
|
location,
|
||||||
goBack,
|
|
||||||
locationChange,
|
locationChange,
|
||||||
getRendition,
|
getRendition,
|
||||||
rendition,
|
rendition,
|
||||||
|
|
@ -654,7 +650,6 @@ defineExpose({
|
||||||
currentHref,
|
currentHref,
|
||||||
setLocation,
|
setLocation,
|
||||||
epubRef,
|
epubRef,
|
||||||
TocComponent,
|
|
||||||
// Annotation related
|
// Annotation related
|
||||||
savedAnnotations,
|
savedAnnotations,
|
||||||
goToAnnotation,
|
goToAnnotation,
|
||||||
|
|
@ -664,112 +659,6 @@ defineExpose({
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* TOC area styles */
|
|
||||||
.toc-area {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 10;
|
|
||||||
width: 256px;
|
|
||||||
overflow-y: auto;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
padding: 6px 0;
|
|
||||||
background-color: var(--background-color);
|
|
||||||
border-right: 1px solid var(--divider-color);
|
|
||||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-area::-webkit-scrollbar {
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-area::-webkit-scrollbar-thumb:vertical {
|
|
||||||
height: 5px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-area .toc-area-button {
|
|
||||||
user-select: none;
|
|
||||||
appearance: none;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
display: block;
|
|
||||||
font-family: sans-serif;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 0.9em;
|
|
||||||
text-align: left;
|
|
||||||
padding: 0.9em 1em;
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
box-sizing: border-box;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-area .toc-area-button:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-area .toc-area-button:active {
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-area .active {
|
|
||||||
border-left: 3px solid var(--accent-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-area .toc-area-button .expansion {
|
|
||||||
cursor: pointer;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
top: 50%;
|
|
||||||
right: 12px;
|
|
||||||
position: absolute;
|
|
||||||
width: 10px;
|
|
||||||
background-color: #a2a5b4;
|
|
||||||
transition: top 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-area .toc-area-button .expansion::after,
|
|
||||||
.toc-area .toc-area-button .expansion::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
width: 6px;
|
|
||||||
height: 2px;
|
|
||||||
background-color: currentcolor;
|
|
||||||
border-radius: 2px;
|
|
||||||
transition: transform 0.3s ease-in-out, top 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toc-area .toc-area-button .expansion::before {
|
|
||||||
transform: rotate(-45deg) translateX(2.5px);
|
|
||||||
}
|
|
||||||
.toc-area .toc-area-button .open::before {
|
|
||||||
transform: rotate(45deg) translateX(2.5px);
|
|
||||||
}
|
|
||||||
.toc-area .toc-area-button .expansion::after {
|
|
||||||
transform: rotate(45deg) translateX(-2.5px);
|
|
||||||
}
|
|
||||||
.toc-area .toc-area-button .open::after {
|
|
||||||
transform: rotate(-45deg) translateX(-2.5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* TOC background overlay */
|
|
||||||
.toc-background {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.reader-container {
|
.reader-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue