=> {
- 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,
- };
-}
\ No newline at end of file
diff --git a/src/utils/listeners/listener.ts b/src/utils/listeners/listener.ts
index a754f50..a8e0f74 100644
--- a/src/utils/listeners/listener.ts
+++ b/src/utils/listeners/listener.ts
@@ -3,10 +3,12 @@ import keyListener from './key'
import wheelListener from './wheel'
import swipListener from './swip'
import selectListener from './select'
+import touchListener from './touch'
export {
clickListener,
keyListener,
wheelListener,
swipListener,
selectListener,
+ touchListener
}
diff --git a/src/utils/listeners/select.ts b/src/utils/listeners/select.ts
index bd5b22a..383a3d3 100644
--- a/src/utils/listeners/select.ts
+++ b/src/utils/listeners/select.ts
@@ -1,38 +1,159 @@
+// src/utils/listeners/selectListener.js
+
/**
+ * Enhanced selection listener with touch support
* @param {Document} document - The document object to add event
* @param {Object} rendition - The EPUBJS rendition
* @param {Function} fn - The listener function
*/
export default function selectListener(document, rendition, fn) {
- document.addEventListener('mousedown', () => {
- document.getSelection().removeAllRanges()
- fn('cleared')
- })
+ // Track touch selection state
+ let touchStarted = false;
+ let touchSelection = false;
+ let touchStartTime = 0;
+ const LONG_PRESS_DURATION = 500; // ms
+ const TOUCH_MOVE_THRESHOLD = 10; // pixels
+ let startX = 0;
+ let startY = 0;
+
+ // Check if we're on a mobile device
+ const isMobileDevice = () => {
+ return (('ontouchstart' in window) ||
+ (navigator.maxTouchPoints > 0) ||
+ (navigator.msMaxTouchPoints > 0));
+ };
+
+ // Check if we're on iOS
+ const isIOS = () => {
+ return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
+ };
+
+ // Clear selection handler
+ const clearSelection = () => {
+ if (document.getSelection) {
+ document.getSelection().removeAllRanges();
+ }
+ fn('cleared');
+ };
+
+ // Process text selection
+ const processSelection = (e) => {
+ if (e.ignore) return;
+ e.ignore = true;
+
+ const selection = document.getSelection();
+ const text = selection.toString();
+
+ if (text === '') return;
+
+ try {
+ const range = selection.getRangeAt(0);
+ const [contents] = rendition.getContents();
+ const cfiRange = contents.cfiFromRange(range);
+
+ const SelectionReact = range.getBoundingClientRect();
+ const viewRect = rendition.manager.container.getBoundingClientRect();
+
+ let react = {
+ left: `${
+ viewRect.x + SelectionReact.x - (rendition.manager.scrollLeft || 0)
+ }px`,
+ top: `${viewRect.y + SelectionReact.y}px`,
+ width: `${SelectionReact.width}px`,
+ height: `${SelectionReact.height}px`,
+ };
+
+ fn('selected', react, text, cfiRange);
+ } catch (error) {
+ console.error('Error processing selection:', error);
+ }
+ };
+
+ // Mouse events for desktop
+ document.addEventListener('mousedown', clearSelection);
document.addEventListener('mouseup', (e) => {
- if (e.ignore) return
- e.ignore = true
+ processSelection(e);
+ });
- const selection = document.getSelection()
- const text = selection.toString()
+ // Only add touch events if we're on a touch device
+ if (isMobileDevice()) {
+ // Touch events for mobile
+ document.addEventListener('touchstart', (e) => {
+ touchStarted = true;
+ touchSelection = false;
+ touchStartTime = Date.now();
+
+ // Store start position for determining if it's a tap or selection attempt
+ if (e.touches && e.touches[0]) {
+ startX = e.touches[0].clientX;
+ startY = e.touches[0].clientY;
+ }
+ }, { passive: true });
- if (text === '') return
- const range = selection.getRangeAt(0)
+ document.addEventListener('touchmove', (e) => {
+ if (!touchStarted) return;
+
+ // Check if this might be a selection attempt (not just a tap)
+ if (e.touches && e.touches[0]) {
+ const moveX = Math.abs(e.touches[0].clientX - startX);
+ const moveY = Math.abs(e.touches[0].clientY - startY);
+
+ // If user has moved finger more than threshold, might be trying to select
+ if (moveX > TOUCH_MOVE_THRESHOLD || moveY > TOUCH_MOVE_THRESHOLD) {
+ touchSelection = true;
+ }
+ }
+ }, { passive: true });
- const [contents] = rendition.getContents()
- const cfiRange = contents.cfiFromRange(range)
+ document.addEventListener('touchend', (e) => {
+ if (!touchStarted) return;
+ touchStarted = false;
+
+ const touchDuration = Date.now() - touchStartTime;
+
+ // Check if it was a long press or a deliberate selection movement
+ if (touchDuration > LONG_PRESS_DURATION || touchSelection) {
+ // Delay processing to allow the browser to complete the selection
+ setTimeout(() => {
+ processSelection(e);
+ }, 50);
+ } else {
+ // It was a quick tap, clear selection
+ clearSelection();
+ }
+ });
- const SelectionReact = range.getBoundingClientRect()
- const viewRect = rendition.manager.container.getBoundingClientRect()
-
- let react = {
- left: `${
- viewRect.x + SelectionReact.x - (rendition.manager.scrollLeft || 0)
- }px`,
- top: `${viewRect.y + SelectionReact.y}px`,
- width: `${SelectionReact.width}px`,
- height: `${SelectionReact.height}px`,
+ // Handle context menu long press on mobile
+ document.addEventListener('contextmenu', (e) => {
+ // This event fires on long-press on many mobile browsers
+ // Prevent the default context menu
+ if (isMobileDevice()) {
+ e.preventDefault();
+ }
+
+ // Delay to let the browser create the selection
+ setTimeout(() => {
+ processSelection(e);
+ }, 50);
+ });
+
+ // Add specific handling for iOS
+ if (isIOS()) {
+ // iOS sometimes needs additional help with text selection
+ document.addEventListener('selectionchange', () => {
+ // Only process if we're in a touch selection operation
+ if (touchSelection || (Date.now() - touchStartTime) > LONG_PRESS_DURATION) {
+ // Delay to let selection complete
+ setTimeout(() => {
+ const selection = document.getSelection();
+ if (selection && selection.toString().length > 0) {
+ const e = { ignore: false };
+ processSelection(e);
+ }
+ }, 100);
+ }
+ });
}
- fn('selected', react, text, cfiRange)
- })
+ }
}
\ No newline at end of file
diff --git a/src/utils/listeners/touch.ts b/src/utils/listeners/touch.ts
new file mode 100644
index 0000000..05cc44e
--- /dev/null
+++ b/src/utils/listeners/touch.ts
@@ -0,0 +1,248 @@
+/**
+ * Unified touch handler that supports both swipe navigation and text selection
+ * @param {Document} document - The document to add event listeners to
+ * @param {Object} rendition - The EPUBJS rendition
+ * @param {function} navigationFn - Function to call for navigation (swipe)
+ * @param {function} selectionFn - Function to call for text selection
+ */
+
+type epubEvent = TouchEvent & { ignore?: boolean };
+type Direction = 'next' | 'prev' | 'up' | 'down';
+
+export default function touchHandler(
+ document: Document,
+ rendition: any,
+ navigationFn: (direction: Direction) => void,
+ selectionFn: (type: string, rect?: any, text?: string, cfiRange?: string) => void
+) {
+ // State tracking
+ let touchState = {
+ startX: 0,
+ startY: 0,
+ startTime: 0,
+ isLongPress: false,
+ isSelectionAttempt: false,
+ lastTapTime: 0,
+ moveCount: 0,
+ lastMoveX: 0,
+ lastMoveY: 0
+ };
+
+ // Constants for gesture detection
+ const SWIPE = {
+ threshold: 50, // Min distance for swipe
+ restraint: 200, // Max perpendicular movement
+ allowedTime: 500 // Max time for swipe
+ };
+
+ const SELECTION = {
+ longPressDuration: 500, // Time for long press
+ moveThreshold: 10, // Movement to trigger selection mode
+ selectionDelay: 50 // Delay to process selection after touch
+ };
+
+ // Clear selection
+ const clearSelection = () => {
+ if (document.getSelection) {
+ document.getSelection()?.removeAllRanges();
+ }
+ selectionFn('cleared');
+ };
+
+ // Process text selection
+ const processSelection = () => {
+ const selection = document.getSelection();
+ if (!selection) return;
+
+ const text = selection.toString();
+ if (text === '') return;
+
+ try {
+ const range = selection.getRangeAt(0);
+ const [contents] = rendition.getContents();
+ const cfiRange = contents.cfiFromRange(range);
+
+ const selectionRect = range.getBoundingClientRect();
+ const viewRect = rendition.manager.container.getBoundingClientRect();
+
+ const rect = {
+ left: `${
+ viewRect.x + selectionRect.x - (rendition.manager.scrollLeft || 0)
+ }px`,
+ top: `${viewRect.y + selectionRect.y}px`,
+ width: `${selectionRect.width}px`,
+ height: `${selectionRect.height}px`,
+ };
+
+ selectionFn('selected', rect, text, cfiRange);
+ } catch (error) {
+ console.error('Error processing selection:', error);
+ }
+ };
+
+ // Check if device is iOS
+ const isIOS = () => {
+ return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
+ };
+
+ // Device-specific adjustments
+ if (isIOS()) {
+ SELECTION.longPressDuration = 400;
+ SELECTION.selectionDelay = 100;
+ }
+
+ // Touch start event
+ document.addEventListener('touchstart', (e: epubEvent) => {
+ if (e.ignore) return;
+ e.ignore = true;
+
+ // Get initial touch position
+ if (e.touches && e.touches[0]) {
+ touchState.startX = e.touches[0].pageX;
+ touchState.startY = e.touches[0].pageY;
+ touchState.lastMoveX = touchState.startX;
+ touchState.lastMoveY = touchState.startY;
+ touchState.startTime = Date.now();
+ touchState.isLongPress = false;
+ touchState.isSelectionAttempt = false;
+ touchState.moveCount = 0;
+
+ // Handle long press with timeout
+ const longPressTimeout = setTimeout(() => {
+ // If finger hasn't moved much, trigger long press
+ const currentX = touchState.lastMoveX;
+ const currentY = touchState.lastMoveY;
+ const moveX = Math.abs(currentX - touchState.startX);
+ const moveY = Math.abs(currentY - touchState.startY);
+
+ if (moveX < SELECTION.moveThreshold && moveY < SELECTION.moveThreshold) {
+ touchState.isLongPress = true;
+ }
+ }, SELECTION.longPressDuration);
+
+ // Store the timeout in a property to clear it if needed
+ (document as any)._longPressTimeout = longPressTimeout;
+ }
+ }, { passive: true });
+
+ // Touch move event
+ document.addEventListener('touchmove', (e: epubEvent) => {
+ if (e.ignore) return;
+ e.ignore = true;
+
+ if (e.touches && e.touches[0]) {
+ touchState.lastMoveX = e.touches[0].pageX;
+ touchState.lastMoveY = e.touches[0].pageY;
+ touchState.moveCount++;
+
+ // Calculate movement
+ const moveX = Math.abs(touchState.lastMoveX - touchState.startX);
+ const moveY = Math.abs(touchState.lastMoveY - touchState.startY);
+
+ // Determine if this might be a selection attempt
+ if (moveX > SELECTION.moveThreshold || moveY > SELECTION.moveThreshold) {
+ // Clear long press timeout if significant movement
+ if ((document as any)._longPressTimeout) {
+ clearTimeout((document as any)._longPressTimeout);
+ (document as any)._longPressTimeout = null;
+ }
+
+ // If moved a lot horizontally relative to vertically, might be a swipe
+ // If moved a lot vertically or in a pattern, might be selection
+ if ((moveY > moveX * 1.5) || touchState.moveCount > 5) {
+ touchState.isSelectionAttempt = true;
+ }
+ }
+ }
+ }, { passive: true });
+
+ // Touch end event
+ document.addEventListener('touchend', (e: epubEvent) => {
+ if (e.ignore) return;
+ e.ignore = true;
+
+ // Clear long press timeout if it's still active
+ if ((document as any)._longPressTimeout) {
+ clearTimeout((document as any)._longPressTimeout);
+ (document as any)._longPressTimeout = null;
+ }
+
+ // Calculate touch stats
+ const touchEndTime = Date.now();
+ const elapsedTime = touchEndTime - touchState.startTime;
+
+ // Get distance traveled
+ const distX = e.changedTouches[0].pageX - touchState.startX;
+ const distY = e.changedTouches[0].pageY - touchState.startY;
+
+ // Handle selection if it was a long press or selection attempt
+ if (touchState.isLongPress || touchState.isSelectionAttempt) {
+ // Delay slightly to let the browser finish selection
+ setTimeout(() => {
+ processSelection();
+ }, SELECTION.selectionDelay);
+ return;
+ }
+
+ // Handle swipe if it wasn't a selection attempt
+ if (elapsedTime <= SWIPE.allowedTime) {
+ // Horizontal swipe
+ if (Math.abs(distX) >= SWIPE.threshold && Math.abs(distY) <= SWIPE.restraint) {
+ // If dist traveled is negative, it indicates right swipe
+ navigationFn(distX < 0 ? 'next' : 'prev');
+ }
+ // Vertical swipe
+ else if (Math.abs(distY) >= SWIPE.threshold && Math.abs(distX) <= SWIPE.restraint) {
+ // If dist traveled is negative, it indicates up swipe
+ navigationFn(distY < 0 ? 'up' : 'down');
+ }
+ // Tap - convert to click for regular interaction
+ else {
+ clearSelection();
+
+ // Convert tap to click
+ document.dispatchEvent(
+ new MouseEvent('click', {
+ clientX: touchState.startX,
+ clientY: touchState.startY,
+ })
+ );
+ }
+ }
+ }, { passive: false });
+
+ // Handle context menu (long press on many mobile browsers)
+ document.addEventListener('contextmenu', (e) => {
+ // Only handle on mobile devices
+ if ('ontouchstart' in window) {
+ e.preventDefault();
+ // Delay to let the browser create the selection
+ setTimeout(() => {
+ processSelection();
+ }, SELECTION.selectionDelay);
+ }
+ });
+
+ // Add specific handling for iOS
+ if (isIOS()) {
+ // iOS sometimes needs additional help with text selection
+ document.addEventListener('selectionchange', () => {
+ // Only process if it might be from a touch operation
+ if (touchState.isLongPress || touchState.isSelectionAttempt) {
+ setTimeout(() => {
+ const selection = document.getSelection();
+ if (selection && selection.toString().length > 0) {
+ processSelection();
+ }
+ }, SELECTION.selectionDelay);
+ }
+ });
+ }
+
+ // Regular mouse events for desktop
+ document.addEventListener('mousedown', clearSelection);
+
+ document.addEventListener('mouseup', () => {
+ processSelection();
+ });
+}
\ No newline at end of file
diff --git a/src/views/ReaderView.vue b/src/views/ReaderView.vue
index 6b3f4a9..df3b496 100644
--- a/src/views/ReaderView.vue
+++ b/src/views/ReaderView.vue
@@ -24,9 +24,7 @@
{{ t("reader.loading") }}
@@ -106,10 +104,9 @@ 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 { AnnotationFormData } from "../types/annotations";
+import type { Annotation, PendingAnnotation, AnnotationFormData } from "../types/annotations";
// Import epub.js types
import type Rendition from 'epubjs/types/rendition';
@@ -150,6 +147,22 @@ const showToc = ref(true);
const epubRef = ref | null>(null);
const currentHref = ref(null);
+const selectionBubble = reactive({
+ visible: false,
+ position: { left: '0px', top: '0px', width: '0px', height: '0px' },
+ selectedText: '',
+ cfiRange: ''
+});
+
+// Annotation state
+const savedAnnotations = ref([]);
+const pendingAnnotation = ref(null);
+const showAnnotationModal = ref(false);
+const showAnnotationsPanel = ref(false);
+const annotationName = ref('');
+const annotationNote = ref('');
+const editingAnnotation = ref(null);
+
// TOC related state
const bookState = reactive({
toc: [] as Array,
@@ -164,28 +177,6 @@ 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 {
@@ -212,6 +203,7 @@ 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);
@@ -225,6 +217,38 @@ 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 => {
loading.value = true;
error.value = null;
@@ -252,10 +276,10 @@ const loadBook = async (): Promise => {
const progress = BookProgressManager.loadProgress(bookId);
if (progress && progress.cfi) {
location.value = progress.cfi;
+ //console.log("Setting initial location from localStorage:", location.value);
}
- // Load annotations
- loadAnnotations(bookId);
+ loadAnnotations();
} catch (err: unknown) {
const errorMsg = err instanceof Error ? err.message : String(err);
console.error("Error loading book:", err);
@@ -267,8 +291,10 @@ const loadBook = async (): Promise => {
// Handle location changes
const locationChange = (epubcifi: string): void => {
- // Skip saving the location on the first render
+ // 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;
}
@@ -285,30 +311,19 @@ const locationChange = (epubcifi: string): void => {
}
};
-const getRendition = (rendition: Rendition): void => {
- setRendition(rendition);
+const getRendition = (renditionObj: Rendition): void => {
+ setRendition(renditionObj);
- rendition.on("relocated", (location: RelocatedEvent) => {
+ renditionObj.on("relocated", (location: RelocatedEvent) => {
currentHref.value = location.start.href;
});
- applyAnnotationsToView();
-
- let annotationsApplied = true;
-
- rendition.on("rendered", async () => {
-
- if (!annotationsApplied) {
- try {
- annotationsApplied = true;
- } catch (error) {
- console.error("An error occurred while applying annotations:", error);
- }
- }
+ nextTick(() => {
+ applyAnnotationsToView();
});
// Get book metadata
- const book: Book = rendition.book;
+ const book: Book = renditionObj.book;
book.ready.then(() => {
const meta = book.packaging?.metadata;
if (!bookTitle.value && meta?.title) {
@@ -318,10 +333,194 @@ const getRendition = (rendition: Rendition): void => {
});
};
-// Wrapper for delete annotation to include bookId
-const deleteAnnotationWrapper = (annotationId: string): void => {
- const bookId = route.params.bookId as string;
- deleteAnnotation(annotationId, bookId);
+// Annotation storage functions
+const getAnnotationStorageKey = (bookId: string): string => {
+ return `epub-annotations-${bookId}`;
+};
+
+const loadAnnotations = (): void => {
+ try {
+ 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 => {
+ 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();
+ }
+ }
};
// Toggle TOC panel
@@ -334,6 +533,11 @@ const toggleToc = (): void => {
}
};
+// Toggle annotations panel
+const toggleAnnotationsPanel = () => {
+ showAnnotationsPanel.value = !showAnnotationsPanel.value;
+};
+
// Handle key events
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
@@ -379,8 +583,36 @@ 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);
});
@@ -421,11 +653,9 @@ defineExpose({
// Annotation related
savedAnnotations,
goToAnnotation,
- editAnnotation: editAnnotation,
- deleteAnnotation: deleteAnnotationWrapper,
- toggleAnnotationsPanel,
- toggleSelectionBubble,
- createAnnotationFromSelection
+ editAnnotation,
+ deleteAnnotation,
+ toggleAnnotationsPanel
});
@@ -472,7 +702,6 @@ defineExpose({
top: 6px;
left: 6px;
z-index: 10;
- touch-action: manipulation;
}
.toc-button-bar {
@@ -510,7 +739,6 @@ defineExpose({
top: 10px;
white-space: nowrap;
z-index: 5;
- touch-action: manipulation;
}
.reader-view {