fix(kosync): normalize xpointer to improve KOReader sync tolerance, closes #1857 (#1914)

This commit is contained in:
Huang Xin
2025-08-27 15:18:32 +08:00
committed by GitHub
parent 584f3511f8
commit 515bfee2a1
3 changed files with 30 additions and 12 deletions
@@ -9,7 +9,7 @@ import { Book, BookProgress, FIXED_LAYOUT_FORMATS } from '@/types/book';
import { BookDoc } from '@/libs/document';
import { debounce } from '@/utils/debounce';
import { eventDispatcher } from '@/utils/event';
import { getCFIFromXPointer, XCFI } from '@/utils/xcfi';
import { getCFIFromXPointer, normalizeProgressXPointer, XCFI } from '@/utils/xcfi';
type SyncState = 'idle' | 'checking' | 'conflict' | 'synced' | 'error';
@@ -72,7 +72,7 @@ export const useKOSync = (bookKey: string) => {
const { doc, index: spineIndex } = content;
const converter = new XCFI(doc, spineIndex || 0);
const xpointerResult = converter.cfiToXPointer(cfi);
koProgress = xpointerResult.xpointer;
koProgress = normalizeProgressXPointer(xpointerResult.xpointer);
}
} catch (error) {
console.error('Failed to convert CFI to XPointer', error);
@@ -194,7 +194,7 @@ export const useKOSync = (bookKey: string) => {
await kosyncClient.updateProgress(currentBook, progress.koProgress, progress.percentage);
}, 5000),
// eslint-disable-next-line react-hooks/exhaustive-deps
[bookKey, appService, kosyncClient, generateKOProgress],
[bookKey, appService, kosyncClient],
);
const pullProgress = useCallback(
@@ -239,10 +239,6 @@ export const useKOSync = (bookKey: string) => {
);
useEffect(() => {
const handlePullProgress = (event: CustomEvent) => {
if (event.detail.bookKey !== bookKey) return;
pullProgress();
};
const handlePushProgress = (event: CustomEvent) => {
if (event.detail.bookKey !== bookKey) return;
pushProgress();
@@ -251,16 +247,26 @@ export const useKOSync = (bookKey: string) => {
if (event.detail.bookKey !== bookKey) return;
pushProgress.flush();
};
eventDispatcher.on('pull-kosync', handlePullProgress);
eventDispatcher.on('push-kosync', handlePushProgress);
eventDispatcher.on('flush-kosync', handleFlush);
return () => {
eventDispatcher.off('pull-kosync', handlePullProgress);
eventDispatcher.off('push-kosync', handlePushProgress);
eventDispatcher.off('flush-kosync', handleFlush);
pushProgress.flush();
console.log('useKOSync cleanup done for', bookKey);
};
}, [bookKey, pushProgress, pullProgress]);
}, [bookKey, pushProgress]);
useEffect(() => {
const handlePullProgress = (event: CustomEvent) => {
if (event.detail.bookKey !== bookKey) return;
pullProgress();
};
eventDispatcher.on('pull-kosync', handlePullProgress);
return () => {
eventDispatcher.off('pull-kosync', handlePullProgress);
};
}, [bookKey, pullProgress]);
// Pull: pull progress once when the book is opened
useEffect(() => {
@@ -11,7 +11,7 @@ import { CFI } from '@/libs/document';
import { debounce } from '@/utils/debounce';
import { eventDispatcher } from '@/utils/event';
import { DEFAULT_BOOK_SEARCH_CONFIG, SYNC_PROGRESS_INTERVAL_SEC } from '@/services/constants';
import { getCFIFromXPointer, getXPointerFromCFI } from '@/utils/xcfi';
import { getCFIFromXPointer, getXPointerFromCFI, normalizeProgressXPointer } from '@/utils/xcfi';
export const useProgressSync = (bookKey: string) => {
const _ = useTranslation();
@@ -56,7 +56,7 @@ export const useProgressSync = (bookKey: string) => {
if (content && !FIXED_LAYOUT_FORMATS.has(book.format)) {
const { doc, index } = content;
const xpointerResult = await getXPointerFromCFI(config.location!, doc, index || 0);
config.xpointer = xpointerResult.xpointer;
config.xpointer = normalizeProgressXPointer(xpointerResult.xpointer);
}
} catch (error) {
console.warn('Failed to convert CFI to XPointer', error);
+12
View File
@@ -539,3 +539,15 @@ export const getXPointerFromCFI = async (
const xpointer = converter.cfiToXPointer(cfi);
return xpointer;
};
// Koreader sometimes cannot recognize totally valid XPointer.
// Workaround this by cleaning up any trailing /text().N segments.
// This has neglectable effect on position accuracy as the XPointer still point to the correct element
// while offset within the text node is usually ignored by pagination.
export const normalizeProgressXPointer = (xpointer: string): string => {
const tailingTextRange = /\/text\(\)\.\d+$/;
if (xpointer.match(tailingTextRange)) {
xpointer = xpointer.replace(tailingTextRange, '');
}
return xpointer;
};