sync: use the location CFI to eliminate occasional one-off page glitches (#268)

This commit is contained in:
Huang Xin
2025-01-29 21:32:12 +01:00
committed by GitHub
parent cb04af1b17
commit b7d06b6312
5 changed files with 53 additions and 30 deletions
@@ -11,9 +11,13 @@ export const useBooksSync = () => {
const { appService } = useEnv();
const { library, setLibrary } = useLibraryStore();
const { syncedBooks, syncBooks, lastSyncedAtBooks } = useSync();
const syncBooksPullingRef = useRef(false);
useEffect(() => {
if (!user) return;
if (syncBooksPullingRef.current) return;
syncBooksPullingRef.current = true;
syncBooks([], 'pull');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -61,12 +65,12 @@ export const useBooksSync = () => {
if (!matchingBook.deletedAt && matchingBook.uploadedAt && !oldBook.downloadedAt) {
await appService?.downloadBook(oldBook, true);
}
return {
...oldBook,
...matchingBook,
updatedAt: oldBook.updatedAt,
progress: matchingBook.progress ?? oldBook.progress,
};
const mergedBook =
matchingBook.updatedAt > oldBook.updatedAt
? { ...oldBook, ...matchingBook, updatedAt: oldBook.updatedAt }
: { ...matchingBook, ...oldBook, updatedAt: oldBook.updatedAt };
mergedBook.progress = matchingBook.progress ?? oldBook.progress;
return mergedBook;
}
return oldBook;
};
@@ -91,6 +91,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
const { book } = getBookData(bookKey) || {};
const { isPrimary } = getViewState(bookKey) || {};
if (isPrimary && book && config) {
eventDispatcher.dispatch('sync-book-progress', { bookKey });
const settings = useSettingsStore.getState().settings;
await saveConfig(envConfig, bookKey, config, settings);
}
@@ -7,6 +7,7 @@ import { useReaderStore } from '@/store/readerStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { deserializeConfig, serializeConfig } from '@/utils/serializer';
import { CFI } from '@/libs/document';
import { eventDispatcher } from '@/utils/event';
import { DEFAULT_BOOK_SEARCH_CONFIG, SYNC_PROGRESS_INTERVAL_SEC } from '@/services/constants';
@@ -49,6 +50,21 @@ export const useProgressSync = (bookKey: string) => {
}
};
const handleSyncBookProgress = (event: CustomEvent) => {
const { bookKey: syncBookKey } = event.detail;
if (syncBookKey === bookKey) {
syncConfig();
}
};
useEffect(() => {
eventDispatcher.on('sync-book-progress', handleSyncBookProgress);
return () => {
eventDispatcher.off('sync-book-progress', handleSyncBookProgress);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bookKey]);
useEffect(() => {
if (!progress || firstPulled.current) return;
firstPulled.current = true;
@@ -96,12 +112,12 @@ export const useProgressSync = (bookKey: string) => {
DEFAULT_BOOK_SEARCH_CONFIG,
);
setConfig(bookKey, { ...config, ...newConfig });
if ((syncedConfig.progress?.[1] ?? 0) > 0 && (config?.progress?.[1] ?? 0) > 0) {
const syncedFraction = syncedConfig.progress![0] / syncedConfig.progress![1];
const configFraction = config!.progress![0] / config!.progress![1];
if (syncedFraction > configFraction) {
const syncedCFI = syncedConfig.location;
const configCFI = config?.location;
if (syncedCFI && configCFI) {
if (CFI.compare(configCFI, syncedCFI) < 0) {
if (view) {
view.goToFraction(syncedFraction);
view.goTo(syncedCFI);
eventDispatcher.dispatch('toast', {
type: 'success',
message: _('Reading Progress Synced'),
+18 -16
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useSyncContext } from '@/context/SyncContext';
import { SyncData, SyncOp, SyncResult, SyncType } from '@/libs/sync';
import { useSettingsStore } from '@/store/settingsStore';
@@ -40,22 +40,11 @@ export function useSync(bookKey?: string) {
const [syncingBooks, setSyncingBooks] = useState(false);
const [syncingConfigs, setSyncingConfigs] = useState(false);
const [syncingNotes, setSyncingNotes] = useState(false);
const [syncError, setSyncError] = useState<string | null>(null);
const lastSyncedBooksAt = settings.lastSyncedAtBooks ?? 0;
const lastSyncedConfigsAt = config?.lastSyncedAtConfig ?? settings.lastSyncedAtConfigs ?? 0;
const lastSyncedNotesAt = config?.lastSyncedAtNotes ?? settings.lastSyncedAtNotes ?? 0;
const [lastSyncedAtBooks, setLastSyncedAtBooks] = useState<number>(
lastSyncedBooksAt > 0 ? lastSyncedBooksAt - SEVEN_DAYS_IN_MS : 0,
);
const [lastSyncedAtConfigs, setLastSyncedAtConfigs] = useState<number>(
lastSyncedConfigsAt > 0 ? lastSyncedConfigsAt - SEVEN_DAYS_IN_MS : 0,
);
const [lastSyncedAtNotes, setLastSyncedAtNotes] = useState<number>(
lastSyncedNotesAt > 0 ? lastSyncedNotesAt - SEVEN_DAYS_IN_MS : 0,
);
const [lastSyncedAtBooks, setLastSyncedAtBooks] = useState<number>(0);
const [lastSyncedAtConfigs, setLastSyncedAtConfigs] = useState<number>(0);
const [lastSyncedAtNotes, setLastSyncedAtNotes] = useState<number>(0);
const lastSyncedAtInited = useRef(false);
const [syncing, setSyncing] = useState(false);
// null means unsynced, empty array means synced no changes
@@ -70,6 +59,19 @@ export function useSync(bookKey?: string) {
const { syncClient } = useSyncContext();
useEffect(() => {
if (!settings || !config) return;
if (lastSyncedAtInited.current) return;
lastSyncedAtInited.current = true;
const lastSyncedBooksAt = settings.lastSyncedAtBooks ?? 0;
const lastSyncedConfigsAt = config?.lastSyncedAtConfig ?? settings.lastSyncedAtConfigs ?? 0;
const lastSyncedNotesAt = config?.lastSyncedAtNotes ?? settings.lastSyncedAtNotes ?? 0;
setLastSyncedAtBooks(lastSyncedBooksAt > 0 ? lastSyncedBooksAt - SEVEN_DAYS_IN_MS : 0);
setLastSyncedAtConfigs(lastSyncedConfigsAt > 0 ? lastSyncedConfigsAt - SEVEN_DAYS_IN_MS : 0);
setLastSyncedAtNotes(lastSyncedNotesAt > 0 ? lastSyncedNotesAt - SEVEN_DAYS_IN_MS : 0);
}, [settings, config]);
// bookId is for configs and notes only, if bookId is provided, only pull changes for that book
// and update the lastSyncedAt for that book in the book config
const pullChanges = async (
+3 -3
View File
@@ -1,20 +1,20 @@
class EventDispatcher {
private syncListeners: Map<string, Set<(event: CustomEvent) => boolean>>;
private asyncListeners: Map<string, Set<(event: CustomEvent) => Promise<void>>>;
private asyncListeners: Map<string, Set<(event: CustomEvent) => Promise<void> | void>>;
constructor() {
this.syncListeners = new Map();
this.asyncListeners = new Map();
}
on(event: string, callback: (event: CustomEvent) => Promise<void>): void {
on(event: string, callback: (event: CustomEvent) => Promise<void> | void): void {
if (!this.asyncListeners.has(event)) {
this.asyncListeners.set(event, new Set());
}
this.asyncListeners.get(event)!.add(callback);
}
off(event: string, callback: (event: CustomEvent) => Promise<void>): void {
off(event: string, callback: (event: CustomEvent) => Promise<void> | void): void {
this.asyncListeners.get(event)?.delete(callback);
}