fix: resolve various tracked exceptions in ph (#3584)

* fix: handle synced bookmarks without cfi

* chore: clean up some exceptions in ph
This commit is contained in:
Huang Xin
2026-03-22 15:16:38 +08:00
committed by GitHub
parent e0cf7e8d9f
commit 1936136596
14 changed files with 43 additions and 24 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ PDFJS_FONTS_PATH=../../packages/foliate-js/node_modules/pdfjs-dist
PDFJS_STYLE_PATH=../../packages/foliate-js/vendor/pdfjs
NEXT_PUBLIC_DEFAULT_POSTHOG_URL_BASE64="aHR0cHM6Ly91cy5pLnBvc3Rob2cuY29t"
NEXT_PUBLIC_DEFAULT_POSTHOG_KEY_BASE64="cGhjX2ViNXowbVRxWm8yZm5YYnZGNmE3bFh5TThpTmRSNTNsR1A3VFM3VGh4S08="
NEXT_PUBLIC_DEFAULT_POSTHOG_KEY_BASE64="cGhjX0x3ekhZRWtsZUVub3ZSc05ZQlRpTVRTV2MyS1NUOFdZMzBIWWFhN0ZPa1IK"
NEXT_PUBLIC_DEFAULT_SUPABASE_URL_BASE64="aHR0cHM6Ly9yZWFkZXN0LnN1cGFiYXNlLmNv"
NEXT_PUBLIC_DEFAULT_SUPABASE_KEY_BASE64="ZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnBjM01pT2lKemRYQmhZbUZ6WlNJc0luSmxaaUk2SW5aaWMzbDRablZ6YW1weFpIaHJhbkZzZVhOaklpd2ljbTlzWlNJNkltRnViMjRpTENKcFlYUWlPakUzTXpReE1qTTJOekVzSW1WNGNDSTZNakEwT1RZNU9UWTNNWDAuM1U1VXFhb3VfMVNnclZlMWVvOXJBcGMwdUtqcWhwUWRVWGh2d1VIbVVmZw=="
@@ -18,6 +18,12 @@ describe('isCfiInLocation', () => {
expect(isCfiInLocation('epubcfi(/6/6)', null)).toBe(false);
expect(isCfiInLocation('epubcfi(/6/6)', undefined)).toBe(false);
});
it('should return false for null/undefined/empty cfi', () => {
expect(isCfiInLocation(null as unknown as string, 'epubcfi(/6/6)')).toBe(false);
expect(isCfiInLocation(undefined as unknown as string, 'epubcfi(/6/6)')).toBe(false);
expect(isCfiInLocation('', 'epubcfi(/6/6)')).toBe(false);
});
});
describe('findNearestCfi', () => {
@@ -264,7 +264,13 @@ const FoliateViewer: React.FC<{
item.style &&
getIndexFromCfi(item.cfi) === sectionIndex,
)
.forEach((annotation) => viewRef.current?.addAnnotation(annotation));
.map((annotation) => {
try {
viewRef.current?.addAnnotation(annotation);
} catch (err) {
console.warn('Failed to add annotation', { annotation, error: err });
}
});
}, 100);
if (!detail.doc.isEventListenersAdded) {
@@ -54,10 +54,9 @@ const ProgressBar: React.FC<ProgressBarProps> = ({
const progressInfo = formatProgress(pageInfo?.current, pageInfo?.total, template, localize, lang);
const { page: current = 0, pages: total = 0 } = view?.renderer || {};
const pagesLeft = Math.min(
Math.max(total - current, 1),
pageInfo ? pageInfo.total - pageInfo.current : total,
);
const pagesLeft = bookData?.isFixedLayout
? 1
: Math.min(Math.max(total - current, 1), pageInfo ? pageInfo.total - pageInfo.current : total);
const showPagesLeft = total > 0 || bookData?.isFixedLayout;
const timeLeftStr = showPagesLeft
? _('{{time}} min left in chapter', {
@@ -333,7 +333,11 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
getIndexFromCfi(booknote.cfi) === detail.index,
)
.map((annotation) => {
view?.addAnnotation(annotation);
try {
view?.addAnnotation(annotation);
} catch (err) {
console.warn('Failed to add annotation', { annotation, error: err });
}
});
};
@@ -15,7 +15,8 @@ export const useProgressAutoSave = (bookKey: string) => {
const saveBookConfig = useCallback(
debounce(() => {
setTimeout(async () => {
const config = getConfig(bookKey)!;
const config = getConfig(bookKey);
if (!config) return;
const settings = useSettingsStore.getState().settings;
await saveConfig(envConfig, bookKey, config, settings);
}, 500);
@@ -23,6 +23,7 @@ if (typeof window !== 'undefined' && process.env['NODE_ENV'] === 'production' &&
posthog.init(posthogKey, {
api_host: posthogUrl,
person_profiles: 'always',
autocapture: false,
});
}
}
+1 -1
View File
@@ -87,7 +87,7 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
openLastBooks: false,
lastOpenBooks: [],
autoImportBooksOnOpen: false,
telemetryEnabled: false,
telemetryEnabled: true,
discordRichPresenceEnabled: false,
libraryViewMode: 'grid',
librarySortBy: LibrarySortByType.Updated,
@@ -181,7 +181,7 @@ export class NativeTTSClient implements TTSClient {
const { marks } = parseSSMLMarks(ssml, this.#primaryLang);
for (const mark of marks) {
this.controller?.dispatchSpeakMark(mark);
if (!preload) this.controller?.dispatchSpeakMark(mark);
for await (const ev of this.speakMark(mark, preload, signal)) {
if (signal.aborted) {
yield { code: 'error', message: 'Aborted' } as TTSMessageEvent;
@@ -32,7 +32,7 @@ export class TTSController extends EventTarget {
#nossmlCnt: number = 0;
#currentSpeakAbortController: AbortController | null = null;
#currentSpeakPromise: Promise<void> | null = null;
#isPreloading: boolean = false;
#ttsSectionIndex: number = -1;
state: TTSState = 'stopped';
@@ -261,7 +261,6 @@ export class TTSController extends EventTarget {
const tts = this.view.tts;
if (!tts) return;
this.#isPreloading = true;
const ssmls: string[] = [];
for (let i = 0; i < count; i++) {
const ssml = await this.#preprocessSSML(tts.next());
@@ -271,7 +270,6 @@ export class TTSController extends EventTarget {
for (let i = 0; i < ssmls.length; i++) {
tts.prev();
}
this.#isPreloading = false;
await Promise.all(ssmls.map((ssml) => this.preloadSSML(ssml, new AbortController().signal)));
}
@@ -533,15 +531,11 @@ export class TTSController extends EventTarget {
dispatchSpeakMark(mark?: TTSMark) {
this.dispatchEvent(new CustomEvent('tts-speak-mark', { detail: mark || { text: '' } }));
if (mark && mark.name !== '-1') {
if (this.#isPreloading) {
setTimeout(() => this.dispatchSpeakMark(mark), 500);
} else {
try {
const range = this.view.tts?.setMark(mark.name);
try {
const cfi = this.view.getCFI(this.#ttsSectionIndex, range);
this.dispatchEvent(new CustomEvent('tts-highlight-mark', { detail: { cfi } }));
} catch {}
}
const cfi = this.view.getCFI(this.#ttsSectionIndex, range);
this.dispatchEvent(new CustomEvent('tts-highlight-mark', { detail: { cfi } }));
} catch {}
}
}
@@ -179,6 +179,8 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
}
}
}
// Filter out invalid booknotes
config.booknotes = config.booknotes?.filter((booknote) => booknote.cfi) ?? [];
await updateToc(
bookDoc,
config.viewSettings?.sortedTOC ?? false,
+7 -2
View File
@@ -6,14 +6,19 @@ const unwrapCfi = (cfi: string): string => {
};
export function isCfiInLocation(cfi: string, location: string | null | undefined): boolean {
if (!location) return false;
if (!cfi || !location) return false;
if (cfi === location) return true;
if (cfi && unwrapCfi(cfi).startsWith(unwrapCfi(location))) return true;
const start = CFI.collapse(location);
const end = CFI.collapse(location, true);
return CFI.compare(cfi, start) >= 0 && CFI.compare(cfi, end) <= 0;
try {
return CFI.compare(cfi, start) >= 0 && CFI.compare(cfi, end) <= 0;
} catch (err) {
console.warn('Failed to compare CFIs', { cfi, location, error: err });
return false;
}
}
/**
+1
View File
@@ -19,6 +19,7 @@ export const findParentPath = (toc: TOCItem[], href: string): TOCItem[] => {
};
export const findTocItemBS = (toc: TOCItem[], cfi: string): TOCItem | null => {
if (!cfi) return null;
let left = 0;
let right = toc.length - 1;
let result: TOCItem | null = null;