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
156 changes: 99 additions & 57 deletions packages/app-expo/assets/reader/reader.html

Large diffs are not rendered by default.

48 changes: 43 additions & 5 deletions packages/app-expo/assets/reader/reader.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,46 @@
function postToRN(type, data) {
if (RN) RN.postMessage(JSON.stringify({ type, ...data }));
}
const SCROLL_RELOCATE_POST_INTERVAL_MS = 500;
let pendingScrollRelocatePayload = null;
let scrollRelocatePostTimer = null;
let lastScrollRelocatePostAt = 0;

function flushPendingScrollRelocate() {
if (scrollRelocatePostTimer) {
clearTimeout(scrollRelocatePostTimer);
scrollRelocatePostTimer = null;
}
if (!pendingScrollRelocatePayload) return;
lastScrollRelocatePostAt = Date.now();
postToRN('relocate', pendingScrollRelocatePayload);
pendingScrollRelocatePayload = null;
}

function postRelocateToRN(payload) {
const isContinuousScroll =
currentViewMode === 'scroll' &&
view &&
view.renderer &&
view.renderer.scrolled &&
!view.isFixedLayout;
if (!isContinuousScroll) {
flushPendingScrollRelocate();
postToRN('relocate', payload);
return;
}

pendingScrollRelocatePayload = payload;
const now = Date.now();
const remaining = SCROLL_RELOCATE_POST_INTERVAL_MS - (now - lastScrollRelocatePostAt);
if (remaining <= 0) {
flushPendingScrollRelocate();
return;
}
if (!scrollRelocatePostTimer) {
scrollRelocatePostTimer = setTimeout(flushPendingScrollRelocate, remaining);
}
}
window.__READANY_READER_BUILD_ID = 'android-local-server-cors';
postToRN('debug', { message: '[ReaderBuild] ' + window.__READANY_READER_BUILD_ID });
Promise.withResolvers ??= function () {
Expand Down Expand Up @@ -313,6 +353,7 @@
if (currentViewMode === 'scroll') {
renderer.removeAttribute('no-continuous-scroll');
renderer.setAttribute('flow', 'scrolled');
renderer.setAttribute('max-block-size', '100%');
renderer.setAttribute('max-inline-size', '9999px');
return;
}
Expand Down Expand Up @@ -1189,9 +1230,6 @@
if (!hit) return;
armNoteTapGuard(doc, 900);

// Prevent subsequent click / show-annotation
e.preventDefault && e.preventDefault();

const rect = hit.range.getBoundingClientRect();
const iframe = doc.defaultView && doc.defaultView.frameElement;
const iframeRect = iframe ? iframe.getBoundingClientRect() : { left: 0, top: 0 };
Expand All @@ -1206,7 +1244,7 @@
},
});
}, LONG_PRESS_MS);
}, { passive: false });
}, { passive: true });

doc.addEventListener('touchmove', (e) => {
if (!timer) return;
Expand Down Expand Up @@ -1551,7 +1589,7 @@
total: Math.max(1, rendererPages - 2),
}
: null;
postToRN('relocate', {
postRelocateToRN({
fraction: detail.fraction,
section: { current: currentSectionIndex, total: relocatedSectionTotal },
location: detail.location,
Expand Down
167 changes: 123 additions & 44 deletions packages/app-expo/src/screens/ReaderScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ const MAX_TRACKED_PAGE_DELTA = 20;
const MAX_TRACKED_FRACTION_DELTA = 0.08;
const INITIAL_PROGRESS_RESTORE_GUARD_MS = 1800;
const PROGRAMMATIC_NAV_GUARD_MS = 1200;
const RELOCATE_UI_UPDATE_MS = 250;
const RELOCATE_CONTEXT_UPDATE_MS = 1000;
const BOOK_MIME_TYPES = [
"application/epub+zip",
"application/pdf",
Expand Down Expand Up @@ -149,11 +151,7 @@ const NOTE_TOOLTIP_TOP_THRESHOLD = 180;
import { useRubyStore } from "@readany/core/stores/ruby-store";
import { ReaderSettingsPanel } from "./reader/ReaderSettingsPanel";
import { ReaderTOCPanel } from "./reader/ReaderTOCPanel";
import {
CONTROLS_TIMEOUT,
SCREEN_HEIGHT,
SCREEN_WIDTH,
} from "./reader/reader-constants";
import { CONTROLS_TIMEOUT, SCREEN_HEIGHT, SCREEN_WIDTH } from "./reader/reader-constants";
import { BatteryIcon, ListIcon, SettingsIcon } from "./reader/reader-icons";
import { makeStyles, noteTooltipMdStyles } from "./reader/reader-styles";
import { useReaderBookmark } from "./reader/useReaderBookmark";
Expand All @@ -167,6 +165,22 @@ const LOCAL_FONT_SERVER_DIR = "readany-fonts";

type Props = NativeStackScreenProps<RootStackParamList, "Reader">;
type TTSSegment = VisibleTTSSegment;
type PendingRelocateState = {
bookId: string;
fraction?: number;
cfi?: string;
pageCurrent: number;
pageTotal: number;
chapter?: string;
};
type PendingReadingContextState = {
bookId: string;
cfi?: string;
chapterIndex: number;
chapterTitle: string;
chapterHref: string;
percentage: number;
};

// ──────────────────────────── helpers ────────────────────────────

Expand Down Expand Up @@ -203,6 +217,24 @@ function buildCustomFontFaceCSS(
.join("\n");
}

function syncReadingContextFromRelocate(pending: PendingReadingContextState | null): void {
if (!pending) return;
const currentBook = useLibraryStore.getState().books.find((item) => item.id === pending.bookId);
readingContextService.updateContext({
bookId: pending.bookId,
bookTitle: currentBook?.meta?.title || "",
currentChapter: {
index: pending.chapterIndex,
title: pending.chapterTitle,
href: pending.chapterHref,
},
currentPosition: {
cfi: pending.cfi || "",
percentage: pending.percentage,
},
});
}

// ──────────────────────────── ReaderScreen ────────────────────────────
export function ReaderScreen({ route, navigation }: Props) {
const colors = useColors();
Expand Down Expand Up @@ -349,6 +381,9 @@ export function ReaderScreen({ route, navigation }: Props) {
const locationHistoryRef = useRef<string[]>([]);
const lastNavigatedCfiRef = useRef<string | undefined>(undefined);
const fileServerRef = useRef<string | null>(null);
const pendingRelocateStateRef = useRef<PendingRelocateState | null>(null);
const pendingReadingContextRef = useRef<PendingReadingContextState | null>(null);
const isMountedRef = useRef(true);
const sessionProgressRef = useRef<{
mode: "location" | "page" | "characters";
current: number;
Expand All @@ -374,6 +409,31 @@ export function ReaderScreen({ route, navigation }: Props) {
});
}, 5000),
).current;
const flushPendingRelocateState = useRef(
throttle(() => {
if (!isMountedRef.current) return;
const pending = pendingRelocateStateRef.current;
if (!pending) return;
pendingRelocateStateRef.current = null;

if (pending.fraction != null) {
progressRef.current = pending.fraction;
setProgress(pending.fraction);
}
setCurrentPage(pending.pageCurrent);
setTotalPages(pending.pageTotal);
if (pending.chapter) setCurrentChapter(pending.chapter);
if (pending.cfi) setCurrentCfi(pending.cfi);
}, RELOCATE_UI_UPDATE_MS),
).current;
const flushPendingReadingContext = useRef(
throttle(() => {
if (!isMountedRef.current) return;
const pending = pendingReadingContextRef.current;
pendingReadingContextRef.current = null;
syncReadingContextFromRelocate(pending);
}, RELOCATE_CONTEXT_UPDATE_MS),
).current;
const {
addHighlight,
updateHighlight,
Expand Down Expand Up @@ -624,13 +684,6 @@ export function ReaderScreen({ route, navigation }: Props) {
totalBookCharactersRef.current = totalCharacters > 0 ? totalCharacters : null;
},
onRelocate: (detail: RelocateEvent) => {
console.log("[ReaderScreen] onRelocate", {
section: detail.section,
fraction: detail.fraction,
cfi: detail.cfi,
routeCfi: cfi,
lastNavigated: lastNavigatedCfiRef.current,
});
if (loading) {
setLoading(false);
}
Expand All @@ -641,20 +694,17 @@ export function ReaderScreen({ route, navigation }: Props) {
setTranslationReady(false);
chapterTranslation.reset();
}
const previousProgress = progressRef.current;

if (detail.fraction != null) setProgress(detail.fraction);

let nextPageCurrent = 0;
let nextPageTotal = 0;
if (detail.page) {
setCurrentPage(Math.max(1, detail.page.current));
setTotalPages(Math.max(1, detail.page.total));
nextPageCurrent = Math.max(1, detail.page.current);
nextPageTotal = Math.max(1, detail.page.total);
} else if (detail.section?.total && !detail.location?.total) {
// Fixed-layout documents can still expose stable section pages.
setCurrentPage(Math.max(1, detail.section.current + 1));
setTotalPages(Math.max(1, detail.section.total));
} else {
// Reflowable books without renderer-backed pagination should fall back to percent.
setCurrentPage(0);
setTotalPages(0);
nextPageCurrent = Math.max(1, detail.section.current + 1);
nextPageTotal = Math.max(1, detail.section.total);
}

const trackingSuppressed = Date.now() < progressTrackingGuardUntilRef.current;
Expand Down Expand Up @@ -731,10 +781,9 @@ export function ReaderScreen({ route, navigation }: Props) {
}
sessionProgressRef.current = { mode: "page", current: detail.section.current };
}
if (detail.tocItem?.label) setCurrentChapter(detail.tocItem.label);
if (detail.cfi) {
if (lastCfiRef.current && detail.cfi !== lastCfiRef.current) {
const fractionDiff = Math.abs((detail.fraction ?? 0) - progress);
const fractionDiff = Math.abs((detail.fraction ?? 0) - previousProgress);
if (fractionDiff > 0.02 || locationHistoryRef.current.length === 0) {
locationHistoryRef.current.push(lastCfiRef.current);
if (locationHistoryRef.current.length > 50) {
Expand All @@ -743,18 +792,35 @@ export function ReaderScreen({ route, navigation }: Props) {
}
}
lastCfiRef.current = detail.cfi;
setCurrentCfi(detail.cfi);
// Use throttled save instead of immediate update
throttledSaveProgress(bookId, detail.fraction ?? 0, detail.cfi);
}

pendingRelocateStateRef.current = {
bookId,
fraction: detail.fraction,
cfi: detail.cfi,
pageCurrent: nextPageCurrent,
pageTotal: nextPageTotal,
chapter: detail.tocItem?.label,
};
flushPendingRelocateState();
pendingReadingContextRef.current = {
bookId,
cfi: detail.cfi,
chapterIndex: detail.section?.current ?? 0,
chapterTitle: detail.tocItem?.label || "",
chapterHref: detail.tocItem?.href || "",
percentage: (detail.fraction ?? 0) * 100,
};
flushPendingReadingContext();

// Mark translation ready after first successful relocate (CFI navigation done)
if (!translationReady) setTranslationReady(true);

// If TTS is waiting for a page turn to complete, fire the continuation callback now
// that the renderer has fully updated its position (renderer.start reflects new page).
if (ttsPendingContinueRef.current?.pendingTTSContinueCallbackRef.current) {
console.log("[ReaderScreen][TTS] onRelocate triggered pending TTS continuation");
const cb = ttsPendingContinueRef.current.pendingTTSContinueCallbackRef.current;
ttsPendingContinueRef.current.pendingTTSContinueCallbackRef.current = null;
// Cancel the safety timer since onRelocate fired successfully
Expand All @@ -766,20 +832,7 @@ export function ReaderScreen({ route, navigation }: Props) {
void cb();
}

// Sync reading context for AI tools
readingContextService.updateContext({
bookId,
bookTitle: book?.meta?.title || "",
currentChapter: {
index: detail.section?.current ?? 0,
title: detail.tocItem?.label || "",
href: detail.tocItem?.href || "",
},
currentPosition: {
cfi: detail.cfi || "",
percentage: (detail.fraction ?? 0) * 100,
},
});
// Reading context is synchronized by the throttled relocate flush above.
},
onTocReady: (items: TOCItem[]) => {
setToc(items);
Expand Down Expand Up @@ -906,10 +959,25 @@ export function ReaderScreen({ route, navigation }: Props) {
appActive,
// 维护约定:任何新增遮盖正文/输入态/导航跳转,必须在此追加判定。
[
readSettings.volumeButtonsPageTurn, webViewReady, loading, error, isReimporting,
showSearch, showTOC, showSettings, showNotebook, showTTS,
showTranslation, showChapterTranslation, chapterTranslation.state.status,
selection, noteViewHighlight, noteTooltip, ttsPlayState, isFocused, appActive,
readSettings.volumeButtonsPageTurn,
webViewReady,
loading,
error,
isReimporting,
showSearch,
showTOC,
showSettings,
showNotebook,
showTTS,
showTranslation,
showChapterTranslation,
chapterTranslation.state.status,
selection,
noteViewHighlight,
noteTooltip,
ttsPlayState,
isFocused,
appActive,
],
);

Expand Down Expand Up @@ -1079,6 +1147,17 @@ export function ReaderScreen({ route, navigation }: Props) {
// Save progress immediately on unmount
useEffect(() => {
return () => {
const pendingRelocate = pendingRelocateStateRef.current;
if (pendingRelocate?.fraction != null) {
progressRef.current = pendingRelocate.fraction;
}
if (pendingRelocate?.cfi) {
lastCfiRef.current = pendingRelocate.cfi;
}
syncReadingContextFromRelocate(pendingReadingContextRef.current);
pendingRelocateStateRef.current = null;
pendingReadingContextRef.current = null;
isMountedRef.current = false;
if (fileServerRef.current) {
stopFileServer();
fileServerRef.current = null;
Expand Down
Loading