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
|
||||
// have a spine concept.
|
||||
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 spine = book && book.isOpen ? resolveSpineFromCfi(book, currDocPage) : null;
|
||||
if (spine) {
|
||||
|
|
@ -436,6 +436,15 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId }: SegmentsSideb
|
|||
}
|
||||
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)) {
|
||||
return { readerType: 'pdf', page: Math.floor(currDocPageNumber) };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import {
|
|||
buildWordHighlightCacheKey,
|
||||
tokenizeCanonicalSegment,
|
||||
type EpubCanonicalWordToken,
|
||||
} from '@/lib/shared/epub-word-highlight';
|
||||
} from '@/lib/client/epub/epub-word-highlight';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useConfig } from './ConfigContext';
|
||||
import type {
|
||||
|
|
|
|||
|
|
@ -46,13 +46,13 @@ import {
|
|||
import {
|
||||
buildWalkerPlanningSourceUnits,
|
||||
selectUpcomingWalkerItems,
|
||||
} from '@/lib/shared/tts-epub-preload';
|
||||
} from '@/lib/client/epub/tts-epub-preload';
|
||||
import {
|
||||
completedEpubBoundarySegment,
|
||||
resolveEpubBoundaryHandoffStartIndex,
|
||||
resolveEpubReplaySuppressionAction,
|
||||
type CompletedEpubBoundarySegment,
|
||||
} from '@/lib/shared/tts-epub-handoff';
|
||||
} from '@/lib/client/epub/tts-epub-handoff';
|
||||
import { normalizeTtsLocationKey } from '@/lib/shared/tts-locator';
|
||||
import { isKokoroModel } from '@/lib/shared/kokoro';
|
||||
import { supportsNativeModelSpeed, supportsTtsInstructions } from '@/lib/shared/tts-provider-catalog';
|
||||
|
|
@ -2921,7 +2921,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
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.
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
|
|
@ -2937,14 +2937,35 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
}
|
||||
|
||||
if (!isEPUB) {
|
||||
// For PDF documents, parse the location as "page:sentence"
|
||||
// HTML stores "html:<location>:<sentenceIndex>".
|
||||
// PDF stores "<page>:<sentenceIndex>".
|
||||
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 page = parseInt(pageStr, 10);
|
||||
const sentenceIndex = parseInt(sentenceIndexStr, 10);
|
||||
|
||||
if (!isNaN(page) && !isNaN(sentenceIndex)) {
|
||||
// Skip to the page first, then the sentence index will be restored when setText is called
|
||||
setCurrDocPage(page);
|
||||
pendingJumpTargetRef.current = {
|
||||
locationKey: normalizeLocationKey(page),
|
||||
|
|
@ -2952,7 +2973,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
};
|
||||
}
|
||||
} 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]);
|
||||
|
||||
// Save current position periodically for PDFs
|
||||
// Save current position periodically for non-EPUB readers.
|
||||
useEffect(() => {
|
||||
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(() => {
|
||||
setLastDocumentLocation(id as string, location).catch(error => {
|
||||
console.warn('Error saving PDF location:', error);
|
||||
console.warn('Error saving non-EPUB location:', error);
|
||||
});
|
||||
if (authEnabled) {
|
||||
scheduleDocumentProgressSync({
|
||||
|
|
|
|||
Loading…
Reference in a new issue