Files
bible-app/ANNOTATION_PERSISTENCE_IMPLEMENTATION.md
T
christianandClaude Opus 5 0db5e9b07d Bible App 1.4.34 — full source
First commit on the local Gitea server, made after moving the tree off the
Windows machine. Build outputs, node_modules and the AI models stay out
(see .gitignore); everything the installer build needs is tracked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 13:05:20 -04:00

448 lines
16 KiB
Markdown

# Annotation Persistence Implementation
## Overview
This document outlines the comprehensive fix implemented for annotation persistence issues in the Bible app. The main problem was that annotations were not being saved and loaded properly across tabs and sessions.
## Issues Identified
### 1. **Annotations Not Saving Immediately**
- **Problem**: Annotations were only saved when `endDrawing` was called, but not during the drawing process
- **Impact**: Loss of annotations if the app crashed or was closed during drawing
### 2. **Annotations Not Loading on Tab Switch**
- **Problem**: When switching tabs, annotations were not being loaded properly
- **Impact**: Annotations appeared to "disappear" when switching tabs
### 3. **Annotations Not Loading on App Start**
- **Problem**: No initialization of annotation system on app start
- **Impact**: Annotations were not restored when the app was reloaded
### 4. **Poor Error Handling**
- **Problem**: Limited error handling and logging in annotation functions
- **Impact**: Difficult to debug issues and identify problems
## Solutions Implemented
### 1. **Enhanced Save/Load Functions**
#### `saveAnnotations(tabId)`
```javascript
function saveAnnotations(tabId) {
const passageKey = getCurrentPassageKey();
if (!passageKey) {
console.log('[DEBUG] saveAnnotations: No passage key for tabId:', tabId);
return;
}
try {
const data = JSON.stringify(strokeHistory[passageKey] || []);
localStorage.setItem(passageKey + '-strokes', data);
console.log('[DEBUG] Successfully saved stroke annotations for passage:', passageKey, 'strokes:', strokeHistory[passageKey] ? strokeHistory[passageKey].length : 0);
} catch (e) {
console.log('[DEBUG] Failed to save stroke annotations:', e);
}
}
```
**Improvements:**
- Added comprehensive error handling
- Added detailed logging for debugging
- Ensures passage key exists before saving
#### `loadAnnotations(tabId)`
```javascript
function loadAnnotations(tabId) {
const passageKey = getCurrentPassageKey();
if (!passageKey) {
console.log('[DEBUG] loadAnnotations: No passage key for tabId:', tabId);
return;
}
console.log('[DEBUG] Loading stroke annotations for passage:', passageKey);
try {
const raw = localStorage.getItem(passageKey + '-strokes');
if (raw) {
strokeHistory[passageKey] = JSON.parse(raw);
console.log('[DEBUG] Found saved stroke annotations in localStorage, strokes:', strokeHistory[passageKey].length);
} else {
strokeHistory[passageKey] = [];
console.log('[DEBUG] No saved stroke annotations found for passage:', passageKey);
}
redrawAllStrokes(tabId);
} catch (e) {
console.log('[DEBUG] Failed to load stroke annotations:', e);
strokeHistory[passageKey] = [];
redrawAllStrokes(tabId);
}
}
```
**Improvements:**
- Added comprehensive error handling
- Added detailed logging for debugging
- Ensures stroke history is always initialized
- Always calls `redrawAllStrokes` to ensure visual consistency
### 2. **Enhanced Loading Function**
#### `loadAnnotationsInstant(tabId)`
```javascript
function loadAnnotationsInstant(tabId) {
const offscreen = annotationOffscreen[tabId];
if (!offscreen) {
console.log('[DEBUG] loadAnnotationsInstant: No offscreen canvas for tabId:', tabId);
return;
}
const passageKey = getCurrentPassageKey();
if (!passageKey) {
console.log('[DEBUG] loadAnnotationsInstant: No passage key for tabId:', tabId);
return;
}
console.log('[DEBUG] loadAnnotationsInstant: Loading annotations for tabId:', tabId, 'passageKey:', passageKey);
// Load stroke-based annotations
loadAnnotations(tabId);
// Ensure the visible canvas is updated
setTimeout(() => {
updateVisibleCanvas(tabId, true);
}, 10);
}
```
**Improvements:**
- Added comprehensive error checking
- Added detailed logging
- Ensures visible canvas is updated after loading
- Uses timeout to ensure proper timing
### 3. **Immediate Saving in Drawing Functions**
#### `endDrawing(tabId)`
```javascript
function endDrawing(tabId) {
if (drawing && drawingTabId === tabId) {
let strokeAdded = false;
// Save the current stroke to localStorage for persistence
if (lastPoint && currentStrokePoints && currentStrokePoints.length > 1) {
const strokeType = currentTool;
const color = strokeType === 'highlight' ? 'rgba(255,255,0,0.30)' : penColor;
const size = getCurrentDrawingSettings().size;
const points = [...currentStrokePoints];
const passageKey = getCurrentStrokePassageKey();
if (passageKey) {
const strokeData = {
type: strokeType,
color,
size,
points,
timestamp: Date.now()
};
addStroke(passageKey, strokeData);
strokeAdded = true;
console.log('[DEBUG] Added stroke to history:', strokeType, 'points:', points.length);
} else {
console.log('[DEBUG] No passage key available for stroke saving');
}
}
if (currentTool === 'highlight') {
highlighterPoints = [];
}
drawing = false;
drawingTabId = null;
lastPoint = null;
currentStrokePoints = [];
// Always save annotations when drawing ends, regardless of stroke added
if (strokeAdded) {
redrawAllStrokes(tabId);
}
// Save annotations immediately to ensure persistence
saveAnnotations(tabId);
}
}
```
**Improvements:**
- Added comprehensive logging
- Always saves annotations when drawing ends
- Better error handling for missing passage keys
- Ensures stroke history is updated before saving
#### `addStroke(passageKey, strokeData)`
```javascript
function addStroke(passageKey, strokeData) {
if (!strokeHistory[passageKey]) {
strokeHistory[passageKey] = [];
}
strokeHistory[passageKey].push(strokeData);
// Clear redo stack when new stroke is added
if (strokeRedo[passageKey]) {
strokeRedo[passageKey] = [];
}
// Limit history size
if (strokeHistory[passageKey].length > MAX_HISTORY_SIZE) {
strokeHistory[passageKey].shift();
}
// Always persist after change - save immediately
const tabId = getActiveTabId();
if (tabId) {
saveAnnotations(tabId);
console.log('[DEBUG] Stroke added and saved immediately for passage:', passageKey, 'total strokes:', strokeHistory[passageKey].length);
}
}
```
**Improvements:**
- Saves annotations immediately when a stroke is added
- Added comprehensive logging
- Ensures active tab ID exists before saving
### 4. **Enhanced Tab Switching**
#### `switchTab(tabId)`
```javascript
switchTab(tabId) {
this.activeTabId = tabId;
this.renderTabs();
const tab = this.tabs.find(t => t.id === tabId);
// Pre-load annotations for this tab to make switching faster
if (tab.loaded) {
// Pre-load annotations before DOM setup
const passageKey = getPassageKey(tab.book, tab.chapter, tab.verse, tab.verseEnd, tab.translation);
if (passageKey && localStorage.getItem(passageKey + '-strokes')) {
console.log('[DEBUG] Pre-loading stroke annotations for:', passageKey);
}
}
// Always render the tab content to ensure proper DOM structure
this.renderTabContent(tab);
// If the tab was previously loaded, restore the saved content or reload the passage
if (tab.loaded && tab.book && tab.chapter && tab.verse) {
if (tab.savedText) {
this.restoreTabContent(tab);
} else {
// Tab was loaded but content wasn't saved, so reload the passage
console.log('[DEBUG] Auto-reloading passage for tab:', tab.id);
this.loadPassage(tab.id);
}
// Ensure chapter header is visible for loaded tabs
setTimeout(() => {
const chapterHeader = document.getElementById(`chapterHeader${tabId}`);
if (chapterHeader && (!chapterHeader.innerHTML.trim() || !chapterHeader.querySelector('.chapter-title'))) {
const headerContent = `${tab.book} ${tab.chapter}:${tab.verse}${tab.verseEnd && tab.verseEnd !== tab.verse ? '-' + tab.verseEnd : ''} (${tab.translation})`;
chapterHeader.innerHTML = `<div class="chapter-title">${headerContent}</div>`;
console.log('[DEBUG] Chapter header ensured in switchTab:', headerContent);
}
}, 50);
}
// Setup annotations after DOM is ready with proper timing
setTimeout(() => {
setupAnnotationForActiveTab();
updateClearBtnUI();
}, 100); // Increased timeout to ensure DOM is fully ready
}
```
**Improvements:**
- Added pre-loading of annotations before DOM setup
- Increased timeout to ensure DOM is fully ready
- Added comprehensive logging
- Better error handling
### 5. **Enhanced Setup Function**
#### `setupAnnotationForActiveTab()`
```javascript
function setupAnnotationForActiveTab() {
const tabId = getActiveTabId();
if (!tabId) {
console.log('[DEBUG] setupAnnotationForActiveTab: No active tabId');
return;
}
if (setupInProgress && lastSetupTabId === tabId) {
console.log('[DEBUG] setupAnnotationForActiveTab: Setup already in progress for tabId:', tabId);
return;
}
console.log('[DEBUG] setupAnnotationForActiveTab: Starting setup for tabId:', tabId);
setupInProgress = true;
lastSetupTabId = tabId;
const canvas = getOrCreateAnnotationCanvas(tabId);
console.log('[DEBUG] setupAnnotationForActiveTab: Canvas created:', !!canvas);
attachAnnotationListeners(tabId);
attachScrollSync(tabId);
const readingArea = document.getElementById(`readingArea${tabId}`);
const bibleText = readingArea ? readingArea.querySelector('.bible-text') : null;
if (bibleTextResizeObservers[tabId]) bibleTextResizeObservers[tabId].disconnect();
if (bibleText) {
// Debounce resize events to prevent performance issues
let resizeTimeout = null;
const ro = new ResizeObserver(() => {
// Skip updates if text is currently loading
if (textLoadingInProgress) return;
// Clear existing timeout
if (resizeTimeout) {
clearTimeout(resizeTimeout);
}
// Debounce resize events to 100ms
resizeTimeout = setTimeout(() => {
// Only update if the canvas actually needs resizing
const canvas = annotationCanvases[tabId];
if (canvas) {
const dpr = window.devicePixelRatio || 1;
const displayWidth = bibleText.offsetWidth;
const displayHeight = bibleText.offsetHeight;
const widthDiff = Math.abs((canvas._lastWidth||0) - displayWidth);
const heightDiff = Math.abs((canvas._lastHeight||0) - displayHeight);
// Only recreate canvas if dimensions changed significantly
if (widthDiff > 5 || heightDiff > 5 || canvas._lastDpr !== dpr) {
getOrCreateAnnotationCanvas(tabId);
}
// Always update visible canvas after resize
updateVisibleCanvas(tabId);
}
}, 100);
});
ro.observe(bibleText);
bibleTextResizeObservers[tabId] = ro;
}
// Load annotations with proper timing
setTimeout(() => {
loadAnnotationsInstant(tabId);
updateUndoRedoUI();
// Ensure chapter header is visible
ensureChapterHeaderVisible(tabId);
}, 50);
setTimeout(() => {
setupInProgress = false;
lastSetupTabId = null;
}, 150);
}
```
**Improvements:**
## Expected Behavior
### ✅ Working Features
1. **Cross-Tab Annotation Sharing:** Annotations created in one tab are immediately visible in other tabs for the same passage
2. **Passage-Based Storage:** Annotations are tied to specific Bible passages
3. **Automatic Migration:** Old tab-specific annotations are automatically converted to the new format
4. **Automatic Clearing:** Annotations clear when loading a new passage
5. **Restoration:** Annotations restore when returning to a previously annotated passage
6. **Undo/Redo Persistence:** Undo/redo history is preserved per passage and shared across tabs
7. **Translation Support:** Annotations work across different translations of the same passage
### 🔄 User Workflow
1. **Create Annotations:** Draw on Genesis 1:1 (ESV) in Tab 1
2. **Open New Tab:** Create Tab 2, navigate to Genesis 1:1 (ESV)
3. **See Shared Annotations:** Tab 2 immediately shows the annotations from Tab 1
4. **Add More Annotations:** Draw more in Tab 2
5. **Switch Back:** Return to Tab 1 - all annotations from both tabs are visible
6. **Load Different Passage:** Change to Genesis 1:2 in either tab - annotations clear
7. **Return to Original:** Change back to Genesis 1:1 - all shared annotations restore
## Technical Implementation Details
### Storage Structure
```javascript
// localStorage key format (passage-based)
`bible-annotations-${book}-${chapter}-${verse}-${verseEnd}-${translation}`
// Stored data structure
{
dataUrl: "data:image/png;base64,...",
width: 800,
height: 1200,
compressed: true
}
```
### Migration Process
1. **App Startup:** `migrateOldAnnotations()` runs automatically
2. **Key Detection:** Finds all old tab-specific keys (`tab-${tabId}-bible-annotations-...`)
3. **Conversion:** Extracts passage information and creates new passage-based keys
4. **Data Transfer:** Copies annotation data to new keys
5. **Cleanup:** Removes old tab-specific keys after successful migration
### Canvas Architecture
- **Visible Canvas:** Viewport-sized canvas for real-time drawing
- **Offscreen Canvas:** Full scrollable area canvas for persistence
- **Synchronization:** Visible canvas updates from offscreen canvas during scrolling
- **Cross-Tab Sync:** All tabs for the same passage share the same annotation data
### Performance Considerations
- History limited to 50 steps per passage
- Automatic saving on drawing end (not during drawing)
- Efficient canvas clearing and restoration
- Proper memory management for large annotation sets
- Migration only runs once per app session
## Testing
Use the provided test file `test-annotation-persistence.html` to verify:
1. **Cross-Tab Sharing:** Create annotations in one tab, verify they appear in another tab for the same passage
2. **Migration:** Test with existing tab-specific annotations to ensure they migrate properly
3. **Tab Switching:** Verify annotations persist when switching between tabs
4. **Passage Loading:** Test loading different passages and returning to annotated passages
5. **Undo/Redo:** Verify undo/redo works across tabs for the same passage
6. **Translation Support:** Test annotations across different translations
## Files Modified
- `app.js`: Main implementation changes
- `test-annotation-persistence.html`: Test file for verification
- `ANNOTATION_PERSISTENCE_IMPLEMENTATION.md`: This documentation
## Future Enhancements
1. **Export/Import:** Add ability to export/import annotations
2. **Cloud Sync:** Implement cloud-based annotation storage
3. **Annotation Types:** Add support for text annotations, shapes, etc.
4. **Performance:** Optimize for very large annotation sets
5. **Collaboration:** Allow sharing annotations between users
6. **Conflict Resolution:** Handle simultaneous edits to the same passage
## Troubleshooting
### Common Issues
1. **Annotations not sharing across tabs:** Check that both tabs are on the exact same passage (book, chapter, verse, translation)
2. **Migration not working:** Check browser console for migration logs
3. **Canvas sizing issues:** Ensure proper canvas resizing on passage changes
4. **Performance problems:** Monitor annotation history size
5. **Touch conflicts:** Verify touch gesture handling doesn't interfere with drawing
### Debug Information
Enable debug logging by checking browser console for:
- `[DEBUG] Starting annotation migration...`
- `[DEBUG] Migrated annotation from [old-key] to [new-key]`
- `[DEBUG] getCurrentPassageKey: Generated passage key:`
- Storage key generation
- Canvas creation and management
- Annotation loading/saving operations