Compare commits

...

1 commit

Author SHA1 Message Date
jrosh
deeaa1146d
phone select depraction, cleanup 2025-06-10 00:22:55 +02:00
9 changed files with 451 additions and 320 deletions

View file

@ -1,5 +1,7 @@
# E-inn
[Pre-release](https://jroshthen1.github.io/e-inn-reader/)
E-inn is a basic EPUB reader supporting remote API imports and local file uploads. Books are stored in IndexedDB; settings and reading progress are retained via localStorage. Built as a single-page application with Vue.js, Vue Router, and epub.js for rendering. No accounts, sync, or advanced features, serves static assets. Minimal UI, lightweight, customizable interface.
## Running

View file

@ -48,7 +48,7 @@
</template>
<script setup lang="ts">
import { ref, watch, nextTick, onMounted } from 'vue';
import { ref, watch, nextTick } from 'vue';
import { useI18n } from '../i18n/usei18n';
const { t } = useI18n();

View file

@ -1,12 +1,45 @@
<!-- src/components/AnnotationsButton.vue -->
<!-- AnnotationsButton.vue -->
<template>
<button v-if="count > 0" class="annotations-toggle-btn" @click="$emit('toggle')" :title="isOpen ? 'Close annotations' : 'Open annotations'">
<span class="annotation-count">{{ count }}</span>
<div class="annotations-button-container">
<!-- Count button when no selection -->
<button
v-if="count > 0 && !hasSelection"
class="annotations-count-btn"
:class="{ 'is-open': isOpen }"
@click="$emit('toggle')"
:title="countButtonTitle"
>
<span class="count-indicator">{{ count }}</span>
</button>
<button
v-if="hasSelection"
class="create-annotation-btn"
@click="$emit('createFromSelection')"
:title="'Double-tap text to select, then click here to annotate'"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="pencil-icon"
>
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/>
<path d="m15 5 4 4"/>
</svg>
</button>
</div>
</template>
<script setup lang="ts">
defineProps({
import { computed, watch } from 'vue';
const props = defineProps({
isOpen: {
type: Boolean,
default: false
@ -14,19 +47,35 @@ defineProps({
count: {
type: Number,
default: 0
}
},
hasSelection: {
type: Boolean,
default: false
},
});
defineEmits(['toggle']);
defineEmits(['toggle', 'createFromSelection']);
watch(() => props.hasSelection, (newValue, oldValue) => {
console.log('AnnotationsButton hasSelection changed:', { newValue, oldValue });
}, { immediate: true });
const countButtonTitle = computed(() => {
return props.isOpen ? `Close annotations (${props.count})` : `Open annotations (${props.count})`;
});
</script>
<style>
.annotations-toggle-btn {
<style scoped>
.annotations-button-container {
position: fixed;
right: 60px;
top: 6px;
width: 24px;
height: 24px;
z-index: 40;
}
.annotations-count-btn {
width: 32px;
height: 32px;
color: var(--accent-color);
border: none;
border-bottom: 1px solid var(--accent-color);
@ -34,11 +83,42 @@ defineEmits(['toggle']);
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 40;
transition: transform 0.2s;
font-size: 1rem;
transition: all 0.2s ease;
font-size: 0.9rem;
background: transparent;
font-weight: 600;
}
.annotations-toggle-btn:active {
transform: scale(0.95);
.annotations-count-btn:hover {
transform: translateY(-2px);
}
.create-annotation-btn {
width: 24px;
height: 24px;
background: var(--accent-color);
color: white;
border: none;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
transition: all 0.2s ease-in-out;
}
.pencil-icon {
transition: transform 0.2s ease;
}
.create-annotation-btn:hover .pencil-icon {
transform: rotate(-20deg);
}
.count-indicator {
font-weight: 600;
font-size: 14px;
}
</style>

View file

@ -166,7 +166,7 @@ const truncateText = (text: string, maxLength: number): string => {
.annotation-actions {
display: flex;
gap: 0.5rem;
justify-content: space-between;
margin-top: 0.5rem;
}

View file

@ -29,7 +29,7 @@ import ePub from 'epubjs'
import type { Book, Rendition, Contents } from 'epubjs'
import {
clickListener,
swipListener,
// swipListener,
wheelListener,
keyListener,
selectListener
@ -40,7 +40,6 @@ interface Props {
location?: any // Current Page number | string | Rendition['location']['start']
tocChanged?: (toc: Book['navigation']['toc']) => void
getRendition?: (rendition: Rendition) => void
handleTextSelected?: (cfiRange: string, contents: Contents) => void
handleKeyPress?: () => void
toggleBubble?: (type: string, rect?: any, text?: string, cfiRange?: string) => void // For custom selection
epubInitOptions?: Book['settings']
@ -55,7 +54,6 @@ const props = withDefaults(defineProps<Props>(), {
const {
tocChanged,
getRendition,
handleTextSelected,
handleKeyPress,
toggleBubble,
epubInitOptions,
@ -220,16 +218,9 @@ const registerEvents = () => {
if (!epubOptions?.flow?.includes('scrolled')) {
wheelListener(iframe.document, flipPage)
}
swipListener(iframe.document, flipPage)
//swipListener(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) {
@ -370,6 +361,7 @@ defineExpose({
user-select: none;
appearance: none;
font-weight: bold;
touch-action: manipulation;
}
.arrow:hover {

View file

@ -40,6 +40,7 @@ export default {
position: absolute;
top: 6px;
right: 6px;
touch-action: manipulation;
}
.styles-button > div {

View file

@ -0,0 +1,284 @@
import { ref, type Ref, nextTick } from 'vue';
import type { Annotation, PendingAnnotation, AnnotationFormData } from '../types/annotations';
import type Rendition from 'epubjs/types/rendition';
export function useAnnotations(
rendition: Ref<Rendition | null>,
currentHref: Ref<string | number | null>,
accentColor: Ref<string>
) {
const savedAnnotations = ref<Annotation[]>([]);
const pendingAnnotation = ref<PendingAnnotation | null>(null);
const showAnnotationModal = ref<boolean>(false);
const showAnnotationsPanel = ref<boolean>(false);
const annotationName = ref<string>('');
const annotationNote = ref<string>('');
const editingAnnotation = ref<Annotation | null>(null);
const currentBookId = ref<string>('');
const hasTextSelection = ref<boolean>(false);
// Device detection
const isMobileDevice = /iPad|iPhone|iPod|Android/i.test(navigator.userAgent);
// Storage key helper
const getAnnotationStorageKey = (bookId: string): string => {
return `epub-annotations-${bookId}`;
};
// Load annotations from storage
const loadAnnotations = (bookId: string): void => {
try {
currentBookId.value = bookId;
const storageKey = getAnnotationStorageKey(bookId);
const stored = localStorage.getItem(storageKey);
if (stored) {
const parsedAnnotations: Annotation[] = JSON.parse(stored);
savedAnnotations.value = parsedAnnotations.sort((a, b) => b.createdAt - a.createdAt);
} else {
savedAnnotations.value = [];
}
} catch (error) {
console.error('Error loading annotations:', error);
savedAnnotations.value = [];
}
};
const saveAnnotationsToStorage = (bookId: string): void => {
try {
const storageKey = getAnnotationStorageKey(bookId);
localStorage.setItem(storageKey, JSON.stringify(savedAnnotations.value));
} catch (error) {
console.error('Error saving annotations:', error);
}
};
const generateAnnotationId = (): string => {
return `annotation-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
};
// Apply annotations to view
const applyAnnotationsToView = async (): Promise<void> => {
if (!rendition.value || savedAnnotations.value.length === 0) return;
try {
await nextTick();
setTimeout(() => {
savedAnnotations.value.forEach(annotation => {
try {
rendition.value?.annotations.highlight(
annotation.cfiRange,
{
id: annotation.id,
name: annotation.name,
note: annotation.note
},
undefined,
'saved-annotation',
{
fill: accentColor.value,
'fill-opacity': '0.4',
'mix-blend-mode': 'multiply',
stroke: accentColor.value,
'stroke-width': '1px'
}
);
} catch (error) {
console.warn('Failed to apply annotation:', annotation.id, error);
}
});
}, 1000);
} catch (error) {
console.error('Error applying annotations:', error);
}
};
// Simple toggle selection bubble
const toggleSelectionBubble = (type: string, rect: any, text: string, cfiRange: string): void => {
if (type === 'selected' && text && text.length > 0) {
hasTextSelection.value = true;
pendingAnnotation.value = {
cfiRange,
text,
contents: rendition.value?.getContents() || null
};
// Reset form fields
annotationName.value = '';
annotationNote.value = '';
editingAnnotation.value = null;
} else if (type === 'cleared') {
hasTextSelection.value = false;
pendingAnnotation.value = null;
}
};
const createAnnotationFromSelection = (): void => {
console.log('Creating annotation from selection...');
if (pendingAnnotation.value) {
showAnnotationModal.value = true;
} else {
console.warn('No pending annotation to create');
}
};
// Handle annotation save from modal
const handleAnnotationSave = (formData?: AnnotationFormData): void => {
const bookId = currentBookId.value;
if (!pendingAnnotation.value || !bookId || !formData) {
console.error('Missing data for annotation save:', {
pendingAnnotation: !!pendingAnnotation.value,
bookId: !!bookId,
formData: !!formData
});
return;
}
try {
const now = Date.now();
if (editingAnnotation.value) {
// Update existing annotation
const index = savedAnnotations.value.findIndex(a => a.id === editingAnnotation.value!.id);
if (index !== -1) {
savedAnnotations.value[index] = {
...editingAnnotation.value,
name: formData.name,
note: formData.note || undefined,
updatedAt: now
};
console.log('Updated existing annotation');
}
} else {
// Create new annotation
const annotation: Annotation = {
id: generateAnnotationId(),
bookId: bookId,
cfiRange: pendingAnnotation.value.cfiRange,
text: pendingAnnotation.value.text,
name: formData.name,
note: formData.note || undefined,
createdAt: now,
updatedAt: now,
chapter: currentHref.value?.toString()
};
savedAnnotations.value.unshift(annotation);
console.log('Created new annotation:', annotation.name);
// Apply visual highlight only if it's a real CFI
try {
rendition.value?.annotations.highlight(
annotation.cfiRange,
{
id: annotation.id,
name: annotation.name,
note: annotation.note
},
undefined,
'saved-annotation',
{
fill: accentColor.value,
'fill-opacity': '0.4',
'mix-blend-mode': 'multiply',
stroke: accentColor.value,
'stroke-width': '1px'
}
);
} catch (error) {
console.warn('Could not apply highlight immediately:', error);
}
}
saveAnnotationsToStorage(bookId);
closeAnnotationModal();
// Show annotations panel after creating new annotation
if (!editingAnnotation.value) {
showAnnotationsPanel.value = true;
}
} catch (error) {
console.error('Error saving annotation:', error);
}
};
const closeAnnotationModal = (): void => {
showAnnotationModal.value = false;
pendingAnnotation.value = null;
annotationName.value = '';
annotationNote.value = '';
editingAnnotation.value = null;
hasTextSelection.value = false;
};
const goToAnnotation = (cfiRange: string): void => {
if (rendition.value) {
rendition.value.display(cfiRange);
}
};
const editAnnotation = (annotation: Annotation): void => {
editingAnnotation.value = annotation;
annotationName.value = annotation.name;
annotationNote.value = annotation.note || '';
pendingAnnotation.value = {
cfiRange: annotation.cfiRange,
text: annotation.text,
contents: null as any
};
showAnnotationModal.value = true;
};
const deleteAnnotation = (annotationId: string, bookId?: string): void => {
const actualBookId = bookId || currentBookId.value;
if (confirm('Are you sure you want to delete this annotation?')) {
const index = savedAnnotations.value.findIndex(a => a.id === annotationId);
if (index !== -1) {
const annotation = savedAnnotations.value[index];
try {
rendition.value?.annotations.remove(annotation.cfiRange, 'saved-annotation');
} catch (error) {
console.warn('Could not remove highlight:', error);
}
savedAnnotations.value.splice(index, 1);
saveAnnotationsToStorage(actualBookId);
console.log('Deleted annotation:', annotation.name);
}
}
};
const toggleAnnotationsPanel = (): void => {
showAnnotationsPanel.value = !showAnnotationsPanel.value;
};
return {
savedAnnotations,
pendingAnnotation,
showAnnotationModal,
showAnnotationsPanel,
annotationName,
annotationNote,
editingAnnotation,
hasTextSelection,
loadAnnotations,
applyAnnotationsToView,
toggleSelectionBubble,
createAnnotationFromSelection,
handleAnnotationSave,
closeAnnotationModal,
goToAnnotation,
editAnnotation,
deleteAnnotation,
toggleAnnotationsPanel,
};
}

View file

@ -1,7 +1,7 @@
/**
* @param {Document} document - The document object to add event
* @param {Object} rendition - The EPUBJS rendition
* @param {Function} fb - The listener function
* @param {Function} fn - The listener function
*/
export default function selectListener(document, rendition, fn) {
document.addEventListener('mousedown', () => {

View file

@ -24,7 +24,9 @@
<AnnotationsButton
:is-open="showAnnotationsPanel"
:count="savedAnnotations.length"
:has-selection="hasTextSelection"
@toggle="toggleAnnotationsPanel"
@createFromSelection="createAnnotationFromSelection"
/>
<div v-if="loading" class="loading">{{ t("reader.loading") }}</div>
@ -104,9 +106,10 @@ import AnnotationModal from "../components/AnnotationModal.vue";
import AnnotationsButton from "../components/AnnotationsButton.vue";
import TocComponent from "../components/TocComponent.vue";
import { useStyles } from "../composables/useStyles";
import { useAnnotations } from "../composables/useAnnotations";
import { loadBookFromIndexedDB } from "../utils/utils";
import type { EpubFile } from "../types/epubFile";
import type { Annotation, PendingAnnotation, AnnotationFormData } from "../types/annotations";
import type { AnnotationFormData } from "../types/annotations";
// Import epub.js types
import type Rendition from 'epubjs/types/rendition';
@ -147,22 +150,6 @@ const showToc = ref<boolean>(true);
const epubRef = ref<InstanceType<typeof EpubView> | 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
const savedAnnotations = ref<Annotation[]>([]);
const pendingAnnotation = ref<PendingAnnotation | null>(null);
const showAnnotationModal = ref<boolean>(false);
const showAnnotationsPanel = ref<boolean>(false);
const annotationName = ref<string>('');
const annotationNote = ref<string>('');
const editingAnnotation = ref<Annotation | null>(null);
// TOC related state
const bookState = reactive({
toc: [] as Array<ExtendedNavItem>,
@ -177,6 +164,28 @@ const {
toggleStylesModal, rendition, setRendition,
} = useStyles();
// Annotations composable
const {
savedAnnotations,
pendingAnnotation,
showAnnotationModal,
showAnnotationsPanel,
annotationName,
annotationNote,
editingAnnotation,
hasTextSelection,
loadAnnotations,
applyAnnotationsToView,
toggleSelectionBubble,
handleAnnotationSave,
closeAnnotationModal,
goToAnnotation,
editAnnotation,
deleteAnnotation,
toggleAnnotationsPanel,
createAnnotationFromSelection,
} = useAnnotations(rendition, currentHref, accentColor);
const BookProgressManager = {
saveProgress(bookId: string, cfi: string, extraData = {}) {
try {
@ -203,7 +212,6 @@ const BookProgressManager = {
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);
@ -217,38 +225,6 @@ const BookProgressManager = {
}
};
// 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;
@ -276,10 +252,10 @@ const loadBook = async (): Promise<void> => {
const progress = BookProgressManager.loadProgress(bookId);
if (progress && progress.cfi) {
location.value = progress.cfi;
//console.log("Setting initial location from localStorage:", location.value);
}
loadAnnotations();
// Load annotations
loadAnnotations(bookId);
} catch (err: unknown) {
const errorMsg = err instanceof Error ? err.message : String(err);
console.error("Error loading book:", err);
@ -291,10 +267,8 @@ const loadBook = async (): Promise<void> => {
// Handle location changes
const locationChange = (epubcifi: string): void => {
// Skip saving the location on the first render to prevent
// overriding our saved location
// Skip saving the location on the first render
if (!firstRenderDone.value) {
//console.log("## first render");
firstRenderDone.value = true;
return;
}
@ -311,19 +285,30 @@ const locationChange = (epubcifi: string): void => {
}
};
const getRendition = (renditionObj: Rendition): void => {
setRendition(renditionObj);
const getRendition = (rendition: Rendition): void => {
setRendition(rendition);
renditionObj.on("relocated", (location: RelocatedEvent) => {
rendition.on("relocated", (location: RelocatedEvent) => {
currentHref.value = location.start.href;
});
nextTick(() => {
applyAnnotationsToView();
let annotationsApplied = true;
rendition.on("rendered", async () => {
if (!annotationsApplied) {
try {
annotationsApplied = true;
} catch (error) {
console.error("An error occurred while applying annotations:", error);
}
}
});
// Get book metadata
const book: Book = renditionObj.book;
const book: Book = rendition.book;
book.ready.then(() => {
const meta = book.packaging?.metadata;
if (!bookTitle.value && meta?.title) {
@ -333,194 +318,10 @@ const getRendition = (renditionObj: Rendition): void => {
});
};
// Annotation storage functions
const getAnnotationStorageKey = (bookId: string): string => {
return `epub-annotations-${bookId}`;
};
const loadAnnotations = (): void => {
try {
// Wrapper for delete annotation to include bookId
const deleteAnnotationWrapper = (annotationId: string): void => {
const bookId = route.params.bookId as string;
const storageKey = getAnnotationStorageKey(bookId);
const stored = localStorage.getItem(storageKey);
if (stored) {
const parsedAnnotations: Annotation[] = JSON.parse(stored);
savedAnnotations.value = parsedAnnotations.sort((a, b) => b.createdAt - a.createdAt);
}
} catch (error) {
console.error('Error loading annotations:', error);
savedAnnotations.value = [];
}
};
const saveAnnotationsToStorage = (): void => {
try {
const bookId = route.params.bookId as string;
const storageKey = getAnnotationStorageKey(bookId);
localStorage.setItem(storageKey, JSON.stringify(savedAnnotations.value));
} catch (error) {
console.error('Error saving annotations:', error);
}
};
// Apply annotations to view
const applyAnnotationsToView = async (): Promise<void> => {
if (!rendition.value || savedAnnotations.value.length === 0) return;
try {
await nextTick();
setTimeout(() => {
savedAnnotations.value.forEach(annotation => {
try {
rendition.value?.annotations.highlight(
annotation.cfiRange,
{
id: annotation.id,
name: annotation.name,
note: annotation.note
},
undefined,
'saved-annotation',
{
fill: accentColor.value,
'fill-opacity': '0.4',
'mix-blend-mode': 'multiply',
stroke: accentColor.value,
'stroke-width': '1px'
}
);
} catch (error) {
console.warn('Failed to apply annotation:', annotation.id, error);
}
});
}, 500);
} catch (error) {
console.error('Error applying annotations:', error);
}
};
// Handle annotation save from modal
const handleAnnotationSave = (formData: AnnotationFormData): void => {
if (!pendingAnnotation.value) return;
try {
const bookId = route.params.bookId as string;
const now = Date.now();
if (editingAnnotation.value) {
// Update existing annotation
const index = savedAnnotations.value.findIndex(a => a.id === editingAnnotation.value!.id);
if (index !== -1) {
savedAnnotations.value[index] = {
...editingAnnotation.value,
name: formData.name,
note: formData.note || undefined,
updatedAt: now
};
}
} else {
// Create new annotation
const annotation: Annotation = {
id: generateAnnotationId(),
bookId,
cfiRange: pendingAnnotation.value.cfiRange,
text: pendingAnnotation.value.text,
name: formData.name,
note: formData.note || undefined,
createdAt: now,
updatedAt: now,
chapter: currentHref.value?.toString()
};
savedAnnotations.value.unshift(annotation);
// Add visual highlight
rendition.value?.annotations.highlight(
annotation.cfiRange,
{
id: annotation.id,
name: annotation.name,
note: annotation.note
},
undefined,
'saved-annotation',
{
fill: accentColor.value,
'fill-opacity': '0.4',
'mix-blend-mode': 'multiply',
stroke: accentColor.value,
'stroke-width': '1px'
}
);
}
saveAnnotationsToStorage();
closeAnnotationModal();
// Show the annotations panel after creating a new annotation
if (!editingAnnotation.value) {
showAnnotationsPanel.value = true;
}
} catch (error) {
console.error('Error saving annotation:', error);
}
};
// Close annotation modal
const closeAnnotationModal = (): void => {
showAnnotationModal.value = false;
pendingAnnotation.value = null;
annotationName.value = '';
annotationNote.value = '';
editingAnnotation.value = null;
};
// Generate annotation ID
const generateAnnotationId = (): string => {
return `annotation-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
};
// Go to annotation
const goToAnnotation = (cfiRange: string): void => {
if (rendition.value) {
rendition.value.display(cfiRange);
}
};
// Edit annotation
const editAnnotation = (annotation: Annotation): void => {
editingAnnotation.value = annotation;
annotationName.value = annotation.name;
annotationNote.value = annotation.note || '';
// Need to set a dummy pending annotation to make the modal work
pendingAnnotation.value = {
cfiRange: annotation.cfiRange,
text: annotation.text,
contents: null as any // This is fine as we're just editing
};
showAnnotationModal.value = true;
};
// Delete annotation
const deleteAnnotation = (annotationId: string): void => {
if (confirm('Are you sure you want to delete this annotation?')) {
const index = savedAnnotations.value.findIndex(a => a.id === annotationId);
if (index !== -1) {
const annotation = savedAnnotations.value[index];
try {
rendition.value?.annotations.remove(annotation.cfiRange, 'highlight');
} catch (error) {
console.warn('Could not remove highlight:', error);
}
savedAnnotations.value.splice(index, 1);
saveAnnotationsToStorage();
}
}
deleteAnnotation(annotationId, bookId);
};
// Toggle TOC panel
@ -533,11 +334,6 @@ const toggleToc = (): void => {
}
};
// Toggle annotations panel
const toggleAnnotationsPanel = () => {
showAnnotationsPanel.value = !showAnnotationsPanel.value;
};
// Handle key events
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
@ -583,36 +379,8 @@ const setLocation = (
expandedToc.value = !close;
};
const debugStoredLocation = () => {
const bookId = route.params.bookId as string;
const progressKey = `book-progress-${bookId}`;
const savedLocation = localStorage.getItem(progressKey);
console.log('================ DEBUG INFO ================');
console.log('Book ID:', bookId);
console.log('Progress key:', progressKey);
console.log('Saved location in localStorage:', savedLocation);
// 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'));
}
}
console.log('=========================================');
};
onMounted(() => {
// debugStoredLocation();
loadBook();
// Add keyboard shortcuts
window.addEventListener('keydown', handleKeyDown);
});
@ -653,9 +421,11 @@ defineExpose({
// Annotation related
savedAnnotations,
goToAnnotation,
editAnnotation,
deleteAnnotation,
toggleAnnotationsPanel
editAnnotation: editAnnotation,
deleteAnnotation: deleteAnnotationWrapper,
toggleAnnotationsPanel,
toggleSelectionBubble,
createAnnotationFromSelection
});
</script>
@ -702,6 +472,7 @@ defineExpose({
top: 6px;
left: 6px;
z-index: 10;
touch-action: manipulation;
}
.toc-button-bar {
@ -739,6 +510,7 @@ defineExpose({
top: 10px;
white-space: nowrap;
z-index: 5;
touch-action: manipulation;
}
.reader-view {