refactor: Convert all reader features to Feature Registration Pattern
Complete the Feature Registration Pattern refactoring across all reader modules. Each feature now exports an init(context) function and uses the event-based architecture for loose coupling. ## Comic Features (6 files) - background-color.ts: Background color picker with toggle - chapter-markers.ts: Visual chapter indicators - page-cache.ts: 5-page ahead prefetch with cleanup - page-order.ts: Auto-detect Japanese vs Western order - page-scrubber.ts: Quick navigation slider - panel-gap.ts: Adjustable panel gap controls ## Ebook Features (6 files) - copy-handler.ts: Text copying with citation - dictionary-popup.ts: Word lookup integration - font-loader.ts: 8 bundled libre fonts - search.ts: Full-text search across spine - typography-engine.ts: Font rendering and hyphenation ## Manga Features (4 files) - reading-direction.ts: RTL/LTR/vertical detection - rtl-navigator.ts: Reversed page turn direction - settings.ts: Webtoon mode and transitions - vertical-scroll-mode.ts: Infinite scroll with lazy loading ## PDF Features (3 files) - pdf-navigation.ts: Page turning, zoom, fit modes - pdf-text-selection.ts: Highlight creation via backend - annotation-layer.ts: Render highlights and notes ## Root-Level Features (3 files) - offline-manager.ts: PWA service worker and sync - reading-speed-tracker.ts: Pages/words per minute tracking - settings-manager.ts: Per-user settings with localStorage fallback ## Core Infrastructure (1 file) - parser-manager.ts: Fixed import paths for all parsers ## Key Changes - All features use init(context) pattern - Event-based communication via context.events.on/emit - No direct DOM manipulation in feature exports - State managed within feature closures - Clean initialization and teardown - Zero functionality lost - all features preserved Total: 23 files converted to unified architecture
This commit is contained in:
@@ -1,7 +1,46 @@
|
||||
// Track reading speed and update database
|
||||
// Feature Registration Pattern implementation
|
||||
|
||||
// Reading speed tracker
|
||||
// Procedural implementation (no OOP)
|
||||
import type { ReaderContext } from "./core/reader-context";
|
||||
|
||||
export function init(context: ReaderContext): void {
|
||||
let state: ReadingSpeedTrackerState | null = null;
|
||||
|
||||
context.events.on("reader:loaded", (detail: { mediaItemId: string }) => {
|
||||
state = createReadingSpeedTracker(detail.mediaItemId);
|
||||
});
|
||||
|
||||
context.events.on("reading-session:start", () => {
|
||||
if (state) {
|
||||
startReadingSession(state);
|
||||
}
|
||||
});
|
||||
|
||||
context.events.on("page-changed", () => {
|
||||
if (state) {
|
||||
recordPageTurn(state);
|
||||
}
|
||||
});
|
||||
|
||||
context.events.on("words-read", (detail: { wordCount: number }) => {
|
||||
if (state) {
|
||||
recordWordsRead(state, detail.wordCount);
|
||||
}
|
||||
});
|
||||
|
||||
context.events.on("reading-session:end", async () => {
|
||||
if (state) {
|
||||
await syncReadingSpeed(state);
|
||||
}
|
||||
});
|
||||
|
||||
context.events.on("reader:unload", async () => {
|
||||
if (state) {
|
||||
await syncReadingSpeed(state);
|
||||
state = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
interface ReadingSpeedTrackerState {
|
||||
startTime: number | null;
|
||||
@@ -26,12 +65,10 @@ function createReadingSpeedTracker(
|
||||
function startReadingSession(
|
||||
state: ReadingSpeedTrackerState,
|
||||
): ReadingSpeedTrackerState {
|
||||
return {
|
||||
...state,
|
||||
startTime: Date.now(),
|
||||
pagesRead: 0,
|
||||
wordsRead: 0,
|
||||
};
|
||||
state.startTime = Date.now();
|
||||
state.pagesRead = 0;
|
||||
state.wordsRead = 0;
|
||||
return state;
|
||||
}
|
||||
|
||||
function recordPageTurn(
|
||||
@@ -39,25 +76,23 @@ function recordPageTurn(
|
||||
): ReadingSpeedTrackerState {
|
||||
if (!state.startTime) return state;
|
||||
|
||||
const newPagesRead = state.pagesRead + 1;
|
||||
state.pagesRead += 1;
|
||||
const now = Date.now();
|
||||
|
||||
if (newPagesRead % 5 === 0 || now - state.lastSync > 5 * 60 * 1000) {
|
||||
syncReadingSpeed({ ...state, pagesRead: newPagesRead });
|
||||
return { ...state, pagesRead: newPagesRead, lastSync: now };
|
||||
if (state.pagesRead % 5 === 0 || now - state.lastSync > 5 * 60 * 1000) {
|
||||
syncReadingSpeed(state);
|
||||
state.lastSync = now;
|
||||
}
|
||||
|
||||
return { ...state, pagesRead: newPagesRead };
|
||||
return state;
|
||||
}
|
||||
|
||||
function recordWordsRead(
|
||||
state: ReadingSpeedTrackerState,
|
||||
wordCount: number,
|
||||
): ReadingSpeedTrackerState {
|
||||
return {
|
||||
...state,
|
||||
wordsRead: state.wordsRead + wordCount,
|
||||
};
|
||||
state.wordsRead += wordCount;
|
||||
return state;
|
||||
}
|
||||
|
||||
async function syncReadingSpeed(
|
||||
@@ -69,10 +104,22 @@ async function syncReadingSpeed(
|
||||
const pagesPerMinute = state.pagesRead / minutesElapsed;
|
||||
const wordsPerMinute = state.wordsRead / minutesElapsed;
|
||||
|
||||
await apiPut(`/readers/${state.mediaItemId}/reading-speed`, {
|
||||
pages_per_minute: pagesPerMinute,
|
||||
words_per_minute: wordsPerMinute,
|
||||
pages_read: state.pagesRead,
|
||||
total_reading_minutes: minutesElapsed,
|
||||
});
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
await fetch(`/readers/${state.mediaItemId}/reading-speed`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
pages_per_minute: pagesPerMinute,
|
||||
words_per_minute: wordsPerMinute,
|
||||
pages_read: state.pagesRead,
|
||||
total_reading_minutes: minutesElapsed,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to sync reading speed:", error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user