Skip to content

Conversation

@boludo00
Copy link
Contributor

@boludo00 boludo00 commented Jan 29, 2026

📝 Description

Adds RSVP (Rapid Serial Visual Presentation) speed reading mode to the ebook reader. This feature displays words one at a time at a configurable speed, with the focal point highlighted and centered for
faster reading. Implements #2209

🛠️ Changes Implemented

  • Add RsvpService for word extraction, playback control, and state management
  • Add RsvpOverlayComponent as the full-screen RSVP reader UI
  • Add RsvpStartDialogComponent for start position selection
  • Integrate RSVP button in ebook reader header
  • Add view manager methods (nextAsync, getCurrentCfi) to support RSVP
  • Export TocItem interface for chapter navigation
  • Features included:
    • Adjustable WPM speed (100-1000, default 300)
    • ORP highlighting with vertical guide lines
    • Chapter dropdown selector
    • Progress bar with word count and time remaining
    • Context panel showing surrounding text when paused
    • 3-2-1 countdown before playback
    • Multiple start options (beginning, current position, selection)
    • Position persistence per book
    • Theme color integration
    • Mobile touch gesture support
    • Keyboard shortcuts (Space, arrows, Escape)

🧪 Testing Strategy

  • Added rsvp.service.spec.ts with 38 unit tests covering:
    • ORP calculation for various word lengths
    • Pause multiplier for punctuation and long words
    • WPM controls with min/max bounds
    • Skip navigation with boundary checks
    • CFI comparison for position persistence
    • Selection matching logic
  • Added rsvp-overlay.component.spec.ts with 18 unit tests covering:
    • Word display splitting at ORP position
    • Time remaining formatting
    • Context text generation
    • Chapter flattening for nested ToC
    • Control action callbacks
  • All 756 tests pass

📸 Visual Changes (if applicable)

This PR introduces a new full-screen RSVP overlay for speed reading. The UI includes:

  • Centered word display with highlighted focal character
  • Vertical guide lines above and below the word
  • Header with close button, chapter selector, and WPM display
  • Context panel (visible when paused) showing surrounding text
  • Footer with progress bar and playback controls
  • Respects the currently selected theme
rsvp_booklore_demo2.mov

⚠️ Required Pre-Submission Checklist

Please Read - This Checklist is Mandatory

Mandatory Requirements (please check ALL boxes):

  • Code adheres to project style guidelines and conventions
  • Branch synchronized with latest develop branch
  • 🚨 CRITICAL: Automated unit tests added/updated to cover changes
  • 🚨 CRITICAL: All tests pass locally (756 tests pass)
  • 🚨 CRITICAL: Manual testing completed in local development environment
  • Flyway migration versioning follows correct sequence (N/A - no database changes)
  • Documentation PR submitted to booklore-docs (pending)

💬 Additional Context (optional)

RSVP is a speed reading technique that presents words sequentially at a fixed point, reducing eye movement and potentially increasing reading speed. The ORP (Optimal Recognition Point) is the character in each word where the eye naturally
focuses - highlighting this character helps with word recognition at high speeds."

Introduces a new speed reading mode that displays words one at a time
with configurable WPM. Features include:
- ORP (Optimal Recognition Point) highlighting for faster reading
- Adjustable speed (100-1000 WPM)
- Play/pause, skip forward/backward controls
- Keyboard and touch gesture support
- Position saving and resume capability
- Start from selection, visible text, or chapter beginning
- nextAsync(): Navigate to next page and wait for relocate event
- getCurrentCfi(): Get current CFI directly from view for accurate position
- Add speed reading button to header with new speed icon
- Wire up RSVP service and overlay component
- Add start choice dialog for selecting start position
- Handle page navigation and position highlighting on stop
Position the ORP character at a fixed center point so eyes don't
need to track horizontal movement between words.
- Change skip forward/back to 15 words with visible labels
- Add chapter dropdown selector for navigation within RSVP mode
- Add 3-2-1 countdown timer on start, resume, and new chapter
- Improve progress bar with "Chapter Progress" label, word count,
  and estimated time remaining
- Center playback controls with balanced layout
- Use theme link color for accent (progress bar, ORP, countdown)
- Replace horizontal guide lines with vertical line through focal point
- Remove underline from ORP character for cleaner look
- Add context panel showing ~100 words around current word
- Context panel appears automatically when paused
- Current word highlighted in accent color
- Remove hint text for cleaner UI
- Improve mobile responsiveness for controls and context
- Add rsvp.service.spec.ts with tests for ORP calculation,
  pause multiplier, WPM controls, skip navigation, CFI
  comparison, and selection matching
- Add rsvp-overlay.component.spec.ts with tests for word
  display, time remaining, context text, chapter flattening,
  and control actions
- Fix edge case in findWordIndexBySelection for whitespace-only
  selections
@boludo00 boludo00 marked this pull request as draft January 29, 2026 23:04
@boludo00
Copy link
Contributor Author

Marking this as a draft for now, reading session is not being properly tracked. Looking into it

- Reduce default punctuation pause from 1000ms to 500ms
- Add dropdown control to adjust pause duration (100ms-1000ms)
- Save pause setting per-book in localStorage
- Update tests for new punctuationPauseMs state property
- Fix bug where RSVP would restart current chapter instead of advancing
  to next chapter when reaching end of section
- Add goToNextSection() to ViewManager for explicit section navigation
- Change punctuation pause options from 100-1000ms to 25-200ms in 25ms
  increments for finer control
- Update tests to match new default punctuation pause value (100ms)
@boludo00 boludo00 marked this pull request as ready for review January 30, 2026 00:36
@boludo00
Copy link
Contributor Author

Fixed reading session tracking.

@acx10
Copy link
Collaborator

acx10 commented Feb 2, 2026

@boludo00 Thanks for the PR, this is seriously impressive work. The feature looks very polished and thoughtfully implemented.

I haven’t had a chance to try it hands-on yet, but I’ll give it a proper go soon and share honest feedback once I do.

@acx10 acx10 added the feature Addition of new functionality label Feb 2, 2026
@acx10 acx10 requested a review from Copilot February 2, 2026 17:37
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an RSVP (Rapid Serial Visual Presentation) “speed reading” mode to the ebook reader, including a full-screen overlay UI, playback/state management, and navigation support.

Changes:

  • Introduces RsvpService for word extraction, playback timing, position persistence, and start/resume flows.
  • Adds RsvpOverlayComponent UI (word display with ORP, controls, chapter dropdown, progress, touch/keyboard interactions).
  • Extends reader integration: header “Speed Read (RSVP)” action, view-manager helpers (getCurrentCfi, nextAsync, goToNextSection), and start-choice dialog UI.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
booklore-ui/src/app/features/readers/ebook-reader/shared/icon.component.ts Adds a new speed icon for RSVP entry point.
booklore-ui/src/app/features/readers/ebook-reader/layout/header/header.service.ts Adds startRsvp$ event stream to trigger RSVP from the header.
booklore-ui/src/app/features/readers/ebook-reader/layout/header/header.component.ts Wires header click handler to start RSVP.
booklore-ui/src/app/features/readers/ebook-reader/layout/header/header.component.html Adds the “Speed Read (RSVP)” button to the header UI.
booklore-ui/src/app/features/readers/ebook-reader/features/rsvp/rsvp.service.ts Implements RSVP extraction/playback/state, persistence, and reader-session updates.
booklore-ui/src/app/features/readers/ebook-reader/features/rsvp/rsvp.service.spec.ts Unit tests for RSVP logic (ORP, pacing, bounds, CFI matching, selection matching).
booklore-ui/src/app/features/readers/ebook-reader/features/rsvp/rsvp-overlay.component.ts Implements the RSVP overlay component logic and interactions.
booklore-ui/src/app/features/readers/ebook-reader/features/rsvp/rsvp-overlay.component.html RSVP overlay template (chapter selector, word display, controls, progress).
booklore-ui/src/app/features/readers/ebook-reader/features/rsvp/rsvp-overlay.component.scss RSVP overlay styling (layout, controls, progress bar, responsive rules).
booklore-ui/src/app/features/readers/ebook-reader/features/rsvp/rsvp-overlay.component.spec.ts Unit tests for overlay behavior and computed outputs.
booklore-ui/src/app/features/readers/ebook-reader/ebook-reader.component.ts Integrates RSVP start flow, overlay, next-page handling, and stop highlighting.
booklore-ui/src/app/features/readers/ebook-reader/ebook-reader.component.html Renders RSVP overlay and start-choice dialog UI.
booklore-ui/src/app/features/readers/ebook-reader/ebook-reader.component.scss Styles the RSVP start-choice dialog overlay.
booklore-ui/src/app/features/readers/ebook-reader/core/view-manager.service.ts Exports TocItem, adds CFI/section helpers and async-next/next-section navigation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +268 to +272
playing: false, // Start paused, countdown will set to playing
words,
currentIndex: startIndex,
progress: (startIndex / words.length) * 100,
resumedFromIndex
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Progress is computed as (startIndex / words.length) * 100, which makes the progress bar show 0% while the UI shows currentIndex + 1 (first word), and it will never reach 100% (last word is (len-1)/len). Consider using (currentIndex + 1) / words.length * 100 (guarding for words.length === 0) and reusing the same helper everywhere progress is updated.

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +107
toggleContext(): void {
this.showContext = !this.showContext;
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

showContext / toggleContext() appear unused (the template always shows the context panel when paused, and no UI element toggles it). Either wire this up (e.g., add a context toggle button and conditionally render the panel) or remove the unused state/method.

Copilot uses AI. Check for mistakes.
Comment on lines +148 to +153
return new Observable(subscriber => {
const handler = () => {
this.view?.removeEventListener('relocate', handler);
subscriber.next();
subscriber.complete();
};
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nextAsync() creates a custom Observable but doesn’t return a teardown to remove the relocate listener if the subscriber unsubscribes early. This can leak listeners and cause unexpected callbacks. Return a cleanup function that removes the listener and clears any pending timeout.

Copilot uses AI. Check for mistakes.
Comment on lines 147 to +152
'auto-fit': [
{d: 'M12 3v3m0 12v3M3 12h3m12 0h3'},
{d: 'M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0-8 0', type: 'path'}
],
'speed': [
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ICONS object literal is missing a comma after the 'auto-fit' entry. As written, 'speed' becomes an invalid token and this file won’t compile. Add a trailing comma after the 'auto-fit' array.

Copilot uses AI. Check for mistakes.
Comment on lines +594 to +596
this.updateState({
currentIndex: newIndex,
progress: (newIndex / state.words.length) * 100
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Progress calculation here uses (newIndex / state.words.length) * 100, which makes the progress bar lag the visible currentIndex + 1 word count and never reach 100% on the last word. Consider (newIndex + 1) / state.words.length * 100 with a length === 0 guard.

Suggested change
this.updateState({
currentIndex: newIndex,
progress: (newIndex / state.words.length) * 100
const totalWords = state.words.length;
const progress = totalWords === 0 ? 0 : ((newIndex + 1) / totalWords) * 100;
this.updateState({
currentIndex: newIndex,
progress

Copilot uses AI. Check for mistakes.
Comment on lines +158 to +162
setTimeout(() => {
this.view?.removeEventListener('relocate', handler);
subscriber.next();
subscriber.complete();
}, 1000);
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setTimeout fallback in nextAsync() isn’t cleared when relocate fires, so it will still run later and do extra work (and can run after unsubscription). Store the timeout id and clear it in the relocate handler / teardown.

Copilot uses AI. Check for mistakes.
Comment on lines +517 to +528
this.updateState({
currentIndex: newIndex,
progress: (newIndex / state.words.length) * 100
});
}

skipBackward(count: number = 10): void {
const state = this.currentState;
const newIndex = Math.max(0, state.currentIndex - count);
this.updateState({
currentIndex: newIndex,
progress: (newIndex / state.words.length) * 100
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Progress calculation here uses (newIndex / state.words.length) * 100, which mismatches the displayed word count (currentIndex + 1) and prevents the bar from ever reaching 100%. Align the formula with the UI (e.g., (newIndex + 1) / state.words.length * 100, with a length === 0 guard) and keep it consistent across all updates.

Suggested change
this.updateState({
currentIndex: newIndex,
progress: (newIndex / state.words.length) * 100
});
}
skipBackward(count: number = 10): void {
const state = this.currentState;
const newIndex = Math.max(0, state.currentIndex - count);
this.updateState({
currentIndex: newIndex,
progress: (newIndex / state.words.length) * 100
const totalWords = state.words.length;
const progress = totalWords === 0 ? 0 : ((newIndex + 1) / totalWords) * 100;
this.updateState({
currentIndex: newIndex,
progress
});
}
skipBackward(count: number = 10): void {
const state = this.currentState;
const newIndex = Math.max(0, state.currentIndex - count);
const totalWords = state.words.length;
const progress = totalWords === 0 ? 0 : ((newIndex + 1) / totalWords) * 100;
this.updateState({
currentIndex: newIndex,
progress

Copilot uses AI. Check for mistakes.
if (words.length === 0) {
// Retry up to 3 times with increasing delay if no words found
if (retryCount < 3) {
setTimeout(() => this.start(retryCount + 1), 150 * (retryCount + 1));
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

start() schedules retries via setTimeout but doesn’t retain/cancel the timer. If the user closes RSVP (or the component is destroyed) while a retry is pending, the callback can still run later and start playback in the background. Store the retry timeout id and clear it in stop()/reset() (and/or check this.currentState.active/a "canceled" flag inside the retry callback).

Suggested change
setTimeout(() => this.start(retryCount + 1), 150 * (retryCount + 1));
setTimeout(() => {
// Only retry if RSVP is still active to avoid restarting after stop/reset
if (this.currentState && this.currentState.active) {
this.start(retryCount + 1);
}
}, 150 * (retryCount + 1));

Copilot uses AI. Check for mistakes.
Comment on lines +702 to +711
private tokenizeText(text: string): RsvpWord[] {
const words = text.split(/\s+/).filter(w => w.length > 0);

return words.map(word => ({
text: word,
orpIndex: this.calculateORP(word),
pauseMultiplier: this.getPauseMultiplier(word)
}));
}

Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tokenizeText() is currently unused in this service. Consider removing it (or using it) to keep the RSVP implementation focused and avoid dead code.

Suggested change
private tokenizeText(text: string): RsvpWord[] {
const words = text.split(/\s+/).filter(w => w.length > 0);
return words.map(word => ({
text: word,
orpIndex: this.calculateORP(word),
pauseMultiplier: this.getPauseMultiplier(word)
}));
}

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,307 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {vi, describe, it, expect, beforeEach} from 'vitest';
import {RsvpOverlayComponent, FlatChapter} from './rsvp-overlay.component';
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import FlatChapter.

Suggested change
import {RsvpOverlayComponent, FlatChapter} from './rsvp-overlay.component';
import {RsvpOverlayComponent} from './rsvp-overlay.component';

Copilot uses AI. Check for mistakes.
@boludo00
Copy link
Contributor Author

boludo00 commented Feb 2, 2026

@boludo00 Thanks for the PR, this is seriously impressive work. The feature looks very polished and thoughtfully implemented.

I haven’t had a chance to try it hands-on yet, but I’ll give it a proper go soon and share honest feedback once I do.

@acx10 Great! That would be amazing. If you want, you can just pull this image from that fork if it's easier:
akiraslingshot/bookkeep:rsvp

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Addition of new functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants