refactor(client,tts,html): migrate EPUB TTS utilities to client and unify HTML progress logic
Move `epub-word-highlight`, `tts-epub-handoff`, and `tts-epub-preload` from shared to client/epub to clarify their usage and improve bundling. Update all imports to use new client paths. Extend TTS and sidebar logic to support HTML document progress tracking and restoration, introducing a new location format for HTML and consistent handling for non-EPUB readers. BREAKING CHANGE: EPUB TTS utilities relocated to `lib/client/epub`; update import paths. HTML progress persistence now uses a new format incompatible with previous versions.
This commit is contained in:
parent
8488ad37e1
commit
08d9218ce4
6 changed files with 44 additions and 12 deletions
|
|
@ -422,7 +422,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId }: SegmentsSideb
|
||||||
// canonical resolution hasn't completed yet and for PDF/HTML, which don't
|
// canonical resolution hasn't completed yet and for PDF/HTML, which don't
|
||||||
// have a spine concept.
|
// have a spine concept.
|
||||||
const inferredCurrentLocator: TTSSegmentLocator | null = (() => {
|
const inferredCurrentLocator: TTSSegmentLocator | null = (() => {
|
||||||
if (typeof currDocPage === 'string' && currDocPage.length > 0) {
|
if (activeReaderType === 'epub' && typeof currDocPage === 'string' && currDocPage.length > 0) {
|
||||||
const book = bookRef.current;
|
const book = bookRef.current;
|
||||||
const spine = book && book.isOpen ? resolveSpineFromCfi(book, currDocPage) : null;
|
const spine = book && book.isOpen ? resolveSpineFromCfi(book, currDocPage) : null;
|
||||||
if (spine) {
|
if (spine) {
|
||||||
|
|
@ -436,6 +436,15 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId }: SegmentsSideb
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (activeReaderType === 'html') {
|
||||||
|
if (typeof currDocPage === 'string' && currDocPage.length > 0) {
|
||||||
|
return { readerType: 'html', location: currDocPage };
|
||||||
|
}
|
||||||
|
if (typeof currDocPageNumber === 'number' && Number.isFinite(currDocPageNumber)) {
|
||||||
|
return { readerType: 'html', location: String(Math.floor(currDocPageNumber)) };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (typeof currDocPageNumber === 'number' && Number.isFinite(currDocPageNumber)) {
|
if (typeof currDocPageNumber === 'number' && Number.isFinite(currDocPageNumber)) {
|
||||||
return { readerType: 'pdf', page: Math.floor(currDocPageNumber) };
|
return { readerType: 'pdf', page: Math.floor(currDocPageNumber) };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ import {
|
||||||
buildWordHighlightCacheKey,
|
buildWordHighlightCacheKey,
|
||||||
tokenizeCanonicalSegment,
|
tokenizeCanonicalSegment,
|
||||||
type EpubCanonicalWordToken,
|
type EpubCanonicalWordToken,
|
||||||
} from '@/lib/shared/epub-word-highlight';
|
} from '@/lib/client/epub/epub-word-highlight';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { useConfig } from './ConfigContext';
|
import { useConfig } from './ConfigContext';
|
||||||
import type {
|
import type {
|
||||||
|
|
|
||||||
|
|
@ -46,13 +46,13 @@ import {
|
||||||
import {
|
import {
|
||||||
buildWalkerPlanningSourceUnits,
|
buildWalkerPlanningSourceUnits,
|
||||||
selectUpcomingWalkerItems,
|
selectUpcomingWalkerItems,
|
||||||
} from '@/lib/shared/tts-epub-preload';
|
} from '@/lib/client/epub/tts-epub-preload';
|
||||||
import {
|
import {
|
||||||
completedEpubBoundarySegment,
|
completedEpubBoundarySegment,
|
||||||
resolveEpubBoundaryHandoffStartIndex,
|
resolveEpubBoundaryHandoffStartIndex,
|
||||||
resolveEpubReplaySuppressionAction,
|
resolveEpubReplaySuppressionAction,
|
||||||
type CompletedEpubBoundarySegment,
|
type CompletedEpubBoundarySegment,
|
||||||
} from '@/lib/shared/tts-epub-handoff';
|
} from '@/lib/client/epub/tts-epub-handoff';
|
||||||
import { normalizeTtsLocationKey } from '@/lib/shared/tts-locator';
|
import { normalizeTtsLocationKey } from '@/lib/shared/tts-locator';
|
||||||
import { isKokoroModel } from '@/lib/shared/kokoro';
|
import { isKokoroModel } from '@/lib/shared/kokoro';
|
||||||
import { supportsNativeModelSpeed, supportsTtsInstructions } from '@/lib/shared/tts-provider-catalog';
|
import { supportsNativeModelSpeed, supportsTtsInstructions } from '@/lib/shared/tts-provider-catalog';
|
||||||
|
|
@ -2921,7 +2921,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
skipBackward,
|
skipBackward,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load last location on mount for both EPUB and PDF.
|
// Load last location on mount for EPUB/PDF/HTML.
|
||||||
// Prefer server-backed progress when available, then fall back to local Dexie.
|
// Prefer server-backed progress when available, then fall back to local Dexie.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
@ -2937,14 +2937,35 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEPUB) {
|
if (!isEPUB) {
|
||||||
// For PDF documents, parse the location as "page:sentence"
|
// HTML stores "html:<location>:<sentenceIndex>".
|
||||||
|
// PDF stores "<page>:<sentenceIndex>".
|
||||||
try {
|
try {
|
||||||
|
if (currentReaderType === 'html') {
|
||||||
|
const htmlMatch = /^html:([^:]+):(\d+)$/.exec(lastLocation);
|
||||||
|
if (htmlMatch) {
|
||||||
|
const [, rawLocation, sentenceIndexStr] = htmlMatch;
|
||||||
|
const decodedLocation = decodeURIComponent(rawLocation);
|
||||||
|
const parsedNumber = Number(decodedLocation);
|
||||||
|
const location: TTSLocation = Number.isFinite(parsedNumber) && decodedLocation.trim() !== ''
|
||||||
|
? parsedNumber
|
||||||
|
: decodedLocation || 1;
|
||||||
|
const sentenceIndex = parseInt(sentenceIndexStr, 10);
|
||||||
|
if (!isNaN(sentenceIndex)) {
|
||||||
|
setCurrDocPage(location);
|
||||||
|
pendingJumpTargetRef.current = {
|
||||||
|
locationKey: normalizeLocationKey(location),
|
||||||
|
index: Math.max(0, sentenceIndex),
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward-compatible parser for legacy non-EPUB progress format.
|
||||||
const [pageStr, sentenceIndexStr] = lastLocation.split(':');
|
const [pageStr, sentenceIndexStr] = lastLocation.split(':');
|
||||||
const page = parseInt(pageStr, 10);
|
const page = parseInt(pageStr, 10);
|
||||||
const sentenceIndex = parseInt(sentenceIndexStr, 10);
|
const sentenceIndex = parseInt(sentenceIndexStr, 10);
|
||||||
|
|
||||||
if (!isNaN(page) && !isNaN(sentenceIndex)) {
|
if (!isNaN(page) && !isNaN(sentenceIndex)) {
|
||||||
// Skip to the page first, then the sentence index will be restored when setText is called
|
|
||||||
setCurrDocPage(page);
|
setCurrDocPage(page);
|
||||||
pendingJumpTargetRef.current = {
|
pendingJumpTargetRef.current = {
|
||||||
locationKey: normalizeLocationKey(page),
|
locationKey: normalizeLocationKey(page),
|
||||||
|
|
@ -2952,7 +2973,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error parsing PDF location:', error);
|
console.warn('Error parsing non-EPUB location:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -2990,13 +3011,15 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
||||||
};
|
};
|
||||||
}, [id, isEPUB, currentReaderType, authEnabled]);
|
}, [id, isEPUB, currentReaderType, authEnabled]);
|
||||||
|
|
||||||
// Save current position periodically for PDFs
|
// Save current position periodically for non-EPUB readers.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id && !isEPUB && sentences.length > 0) {
|
if (id && !isEPUB && sentences.length > 0) {
|
||||||
const location = `${currDocPageNumber}:${currentIndex}`;
|
const location = currentReaderType === 'html'
|
||||||
|
? `html:${encodeURIComponent(String(currDocPage || 1))}:${currentIndex}`
|
||||||
|
: `${currDocPageNumber}:${currentIndex}`;
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
setLastDocumentLocation(id as string, location).catch(error => {
|
setLastDocumentLocation(id as string, location).catch(error => {
|
||||||
console.warn('Error saving PDF location:', error);
|
console.warn('Error saving non-EPUB location:', error);
|
||||||
});
|
});
|
||||||
if (authEnabled) {
|
if (authEnabled) {
|
||||||
scheduleDocumentProgressSync({
|
scheduleDocumentProgressSync({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue