Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,061 changes: 172 additions & 889 deletions packages/app-expo/assets/reader/reader.html

Large diffs are not rendered by default.

931 changes: 107 additions & 824 deletions packages/app-expo/assets/reader/reader.template.html

Large diffs are not rendered by default.

27 changes: 0 additions & 27 deletions packages/app-expo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ import { Audio } from "expo-av";
import { I18nextProvider } from "react-i18next";
import TrackPlayer, {
AppKilledPlaybackBehavior,
Event as TrackEvent,
Capability,
} from "react-native-track-player";

Expand Down Expand Up @@ -165,32 +164,6 @@ export default function App() {
],
});

// Remote event → TTS store bridge
const { useTTSStore: ttsStore } = await import("@/stores/tts-store");
TrackPlayer.addEventListener(TrackEvent.RemotePlay, () => {
ttsStore.getState().resume();
});
TrackPlayer.addEventListener(TrackEvent.RemotePause, () => {
ttsStore.getState().pause();
});
TrackPlayer.addEventListener(TrackEvent.RemoteStop, () => {
ttsStore.getState().stop();
});
TrackPlayer.addEventListener(TrackEvent.RemoteNext, () => {
const { jumpToChunk, currentChunkIndex, totalChunks } = ttsStore.getState();
const nextIndex = currentChunkIndex + 1;
if (nextIndex < totalChunks) {
jumpToChunk(nextIndex);
}
});
TrackPlayer.addEventListener(TrackEvent.RemotePrevious, () => {
const { jumpToChunk, currentChunkIndex } = ttsStore.getState();
const prevIndex = currentChunkIndex - 1;
if (prevIndex >= 0) {
jumpToChunk(prevIndex);
}
});

console.log("[App] bootstrap: done");
setReady(true);
// Hide native splash now — our animated splash takes over
Expand Down
16 changes: 4 additions & 12 deletions packages/app-expo/src/components/reader/SelectionPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ interface Props {
onDismiss: () => void;
onCopy: () => void;
onAIChat: () => void;
onSpeak?: (text: string, cfi: string) => void;
onSpeak?: (cfi: string) => void;
onNote?: (text: string, cfi: string) => void;
onTranslate?: (text: string) => void;
onRemoveHighlight?: () => void;
Expand Down Expand Up @@ -98,11 +98,7 @@ export function SelectionPopover({
}
}, [selection.cfi, hasExistingHighlight]);

const buttonCount =
4 +
(onNote ? 1 : 0) +
(onTranslate ? 1 : 0) +
(onSpeak ? 1 : 0);
const buttonCount = 4 + (onNote ? 1 : 0) + (onTranslate ? 1 : 0) + (onSpeak ? 1 : 0);
const colorRowItemCount = HIGHLIGHT_COLORS.length + (canRemoveHighlight ? 2 : 0);
const colorRowWidth = showColors
? HIGHLIGHT_COLORS.length * COLOR_DOT_SIZE +
Expand Down Expand Up @@ -157,12 +153,9 @@ export function SelectionPopover({
}, [selection.text, onCopy]);

const handleSpeak = useCallback(() => {
const text = selection.text.trim();
if (text && onSpeak) {
onSpeak(text, selection.cfi);
}
onSpeak?.(selection.cfi);
onDismiss();
}, [selection.text, selection.cfi, onSpeak, onDismiss]);
}, [selection.cfi, onSpeak, onDismiss]);

const handleNote = useCallback(() => {
setShowNoteModal(true);
Expand Down Expand Up @@ -270,7 +263,6 @@ export function SelectionPopover({
<Volume2Icon size={18} color={colors.foreground} />
</TouchableOpacity>
)}

</View>
</View>

Expand Down
28 changes: 25 additions & 3 deletions packages/app-expo/src/components/reader/TTSPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { useColors, radius } from "@/styles/theme";
import {
buildNarrationPreview,
findTTSLyricIndex,
type TTSConfig,
type TTSPlayState,
getTTSVoiceLabel,
Expand Down Expand Up @@ -59,6 +60,8 @@ interface TTSPageProps {
narrationSegments?: Array<{ text: string; cfi?: string | null }>;
/** Sentences from the previously-read page, shown above current page sentences */
prevNarrationSegments?: Array<{ text: string; cfi?: string | null }>;
/** Persisted lyric list, independent from the audio synthesis cache. */
lyricSegments?: Array<{ text: string; cfi?: string | null }>;
currentSegmentCfi?: string | null;
currentSegmentText?: string | null;
currentChunkIndex?: number;
Expand Down Expand Up @@ -88,6 +91,12 @@ interface TTSPageProps {
onNextChapter?: () => void | Promise<void>;
}

interface TTSLyricItem {
id: string;
text: string;
cfi?: string | null;
}

function clampPct(p: number) {
return Math.max(0, Math.min(100, Math.round(p * 100)));
}
Expand All @@ -108,6 +117,7 @@ export function TTSPage({
continuousEnabled,
narrationSegments = [],
prevNarrationSegments = [],
lyricSegments: persistedLyricSegments,
currentSegmentCfi,
currentSegmentText,
currentChunkIndex = 0,
Expand Down Expand Up @@ -162,7 +172,7 @@ export function TTSPage({
// Number of prev-page sentences prepended to the list
const prevCount =
prevNarrationSegments?.filter((segment) => segment.text.trim().length > 0).length ?? 0;
const lyricSegments = useMemo(() => {
const fallbackLyricSegments = useMemo(() => {
const keyCounts = new Map<string, number>();
const toLyricItem = (
prefix: "prev" | "curr",
Expand Down Expand Up @@ -193,6 +203,12 @@ export function TTSPage({
}
return currentText ? [{ id: "fallback:current-text", text: currentText, cfi: null }] : [];
}, [currentText, narrationSegments, prevNarrationSegments]);
const lyricSegments: TTSLyricItem[] = persistedLyricSegments?.length
? persistedLyricSegments.map((segment, index) => ({
...segment,
id: `persistent:${segment.cfi || `${segment.text}:${index}`}`,
}))
: fallbackLyricSegments;
const lyricSegmentIdsKey = useMemo(
() => lyricSegments.map((segment) => segment.id).join("|"),
[lyricSegments],
Expand All @@ -213,6 +229,11 @@ export function TTSPage({
return currentIndex;
}
}
const persistedIndex = findTTSLyricIndex(lyricSegments, {
cfi: currentSegmentCfi,
text: normalizedCurrentText,
});
if (persistedLyricSegments?.length && persistedIndex >= 0) return persistedIndex;
const actualPrevCount = lyricSegments.filter((s) => s.id.startsWith("prev:")).length;
const prevCountStale = prevCount !== actualPrevCount;
if (prevCountStale) {
Expand Down Expand Up @@ -249,6 +270,7 @@ export function TTSPage({
currentSegmentText,
lyricSegments,
narrationSegments,
persistedLyricSegments,
prevCount,
]);
const lyricCenterPadding = useMemo(
Expand Down Expand Up @@ -487,14 +509,14 @@ export function TTSPage({

const handleLyricPress = useCallback(
(segment: { text: string; cfi?: string | null }, index: number) => {
const offsetFromCurrent = index - prevCount;
const offsetFromCurrent = persistedLyricSegments?.length ? index : index - prevCount;
if (onJumpToLyricSegment) {
onJumpToLyricSegment(segment, offsetFromCurrent);
return;
}
onJumpToSegment?.(offsetFromCurrent);
},
[onJumpToLyricSegment, onJumpToSegment, prevCount],
[onJumpToLyricSegment, onJumpToSegment, persistedLyricSegments, prevCount],
);

const triggerLoadMoreAbove = useCallback(() => {
Expand Down
Loading