=> {
+ 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 a8e0f74..a754f50 100644
--- a/src/utils/listeners/listener.ts
+++ b/src/utils/listeners/listener.ts
@@ -3,12 +3,10 @@ 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 383a3d3..bd5b22a 100644
--- a/src/utils/listeners/select.ts
+++ b/src/utils/listeners/select.ts
@@ -1,159 +1,38 @@
-// 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) {
- // 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('mousedown', () => {
+ document.getSelection().removeAllRanges()
+ fn('cleared')
+ })
document.addEventListener('mouseup', (e) => {
- processSelection(e);
- });
+ if (e.ignore) return
+ e.ignore = true
- // 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 });
+ const selection = document.getSelection()
+ const text = selection.toString()
- 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 });
+ if (text === '') return
+ const range = selection.getRangeAt(0)
- 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 [contents] = rendition.getContents()
+ const cfiRange = contents.cfiFromRange(range)
- // 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);
- }
- });
+ 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)
+ })
}
\ No newline at end of file
diff --git a/src/utils/listeners/touch.ts b/src/utils/listeners/touch.ts
deleted file mode 100644
index 05cc44e..0000000
--- a/src/utils/listeners/touch.ts
+++ /dev/null
@@ -1,248 +0,0 @@
-/**
- * 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 df3b496..6b3f4a9 100644
--- a/src/views/ReaderView.vue
+++ b/src/views/ReaderView.vue
@@ -24,7 +24,9 @@
{{ t("reader.loading") }}
@@ -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(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,
@@ -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 => {
loading.value = true;
error.value = null;
@@ -276,10 +252,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);
}
- 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 => {
// 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();
+ 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 {
- 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();
- }
- }
+// Wrapper for delete annotation to include bookId
+const deleteAnnotationWrapper = (annotationId: string): void => {
+ const bookId = route.params.bookId as string;
+ 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
});
@@ -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 {