refactor(kosync): move kosync into reader sidebar menu and add manual pull/push trigger (#1912)
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { md5 } from 'js-md5';
|
||||
import { type as osType } from '@tauri-apps/plugin-os';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { KOSyncClient, KoSyncProgress } from '@/services/sync/KOSyncClient';
|
||||
import { Book, FIXED_LAYOUT_FORMATS } from '@/types/book';
|
||||
import { Book, BookProgress, FIXED_LAYOUT_FORMATS } from '@/types/book';
|
||||
import { BookDoc } from '@/libs/document';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
@@ -30,386 +28,257 @@ export interface SyncDetails {
|
||||
|
||||
export const useKOSync = (bookKey: string) => {
|
||||
const _ = useTranslation();
|
||||
const { appService } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { getProgress, getView } = useReaderStore();
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { appService } = useEnv();
|
||||
const progress = getProgress(bookKey);
|
||||
|
||||
const [kosyncClient, setKOSyncClient] = useState<KOSyncClient | null>(null);
|
||||
const [syncState, setSyncState] = useState<SyncState>('idle');
|
||||
const [conflictDetails, setConflictDetails] = useState<SyncDetails | null>(null);
|
||||
const [errorMessage] = useState<string | null>(null);
|
||||
const hasPulledOnce = useRef(false);
|
||||
|
||||
const syncCompletedForKey = useRef<string | null>(null);
|
||||
const lastPushedCfiRef = useRef<string | null>(null);
|
||||
const bookData = getBookData(bookKey);
|
||||
const book = bookData?.book;
|
||||
const bookDoc = bookData?.bookDoc;
|
||||
const progress = getProgress(bookKey);
|
||||
|
||||
useEffect(() => {
|
||||
lastPushedCfiRef.current = null;
|
||||
syncCompletedForKey.current = null;
|
||||
setSyncState('idle');
|
||||
}, [bookKey]);
|
||||
if (!settings.kosync.username || !settings.kosync.userkey) {
|
||||
setKOSyncClient(null);
|
||||
return;
|
||||
}
|
||||
const client = new KOSyncClient(settings.kosync);
|
||||
setKOSyncClient(client);
|
||||
}, [settings]);
|
||||
|
||||
const mapProgressToServerFormat = useCallback(() => {
|
||||
const currentProgress = getProgress(bookKey);
|
||||
const currentBook = getBookData(bookKey)?.book;
|
||||
if (!currentProgress || !currentBook) return null;
|
||||
const generateKOProgress = useCallback(() => {
|
||||
const progress = getProgress(bookKey);
|
||||
const book = getBookData(bookKey)?.book;
|
||||
if (!progress || !book) return null;
|
||||
|
||||
let progressStr: string;
|
||||
let koProgress = '';
|
||||
let percentage: number;
|
||||
|
||||
if (FIXED_LAYOUT_FORMATS.has(currentBook.format)) {
|
||||
const page = (currentProgress.section?.current ?? 0) + 1;
|
||||
const totalPages = currentProgress.section?.total ?? 0;
|
||||
progressStr = page.toString();
|
||||
percentage = totalPages > 0 ? page / totalPages : 0;
|
||||
if (FIXED_LAYOUT_FORMATS.has(book.format)) {
|
||||
const page = progress.section?.current ?? 0;
|
||||
const totalPages = progress.section?.total ?? 0;
|
||||
koProgress = page.toString();
|
||||
percentage = totalPages > 0 ? (page + 1) / totalPages : 0;
|
||||
} else {
|
||||
progressStr = currentProgress.location;
|
||||
const view = getView(bookKey);
|
||||
|
||||
if (view && progressStr) {
|
||||
try {
|
||||
const content = view.renderer.getContents()[0];
|
||||
if (content) {
|
||||
const { doc, index: spineIndex } = content;
|
||||
const converter = new XCFI(doc, spineIndex || 0);
|
||||
const xpointerResult = converter.cfiToXPointer(progressStr);
|
||||
|
||||
progressStr = xpointerResult.xpointer;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to convert CFI to XPointer. Progress will be sent as percentage only.',
|
||||
error,
|
||||
);
|
||||
const cfi = progress.location;
|
||||
if (!view || !cfi) return null;
|
||||
try {
|
||||
const content = view.renderer.getContents()[0];
|
||||
if (content) {
|
||||
const { doc, index: spineIndex } = content;
|
||||
const converter = new XCFI(doc, spineIndex || 0);
|
||||
const xpointerResult = converter.cfiToXPointer(cfi);
|
||||
koProgress = xpointerResult.xpointer;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to convert CFI to XPointer', error);
|
||||
}
|
||||
|
||||
const page = currentProgress.pageinfo?.current ?? 0;
|
||||
const totalPages = currentProgress.pageinfo?.total ?? 0;
|
||||
const page = progress.pageinfo?.current ?? 0;
|
||||
const totalPages = progress.pageinfo?.total ?? 0;
|
||||
percentage = totalPages > 0 ? (page + 1) / totalPages : 0;
|
||||
}
|
||||
|
||||
return { progressStr, percentage };
|
||||
return { koProgress, percentage };
|
||||
}, [bookKey, getProgress, getBookData, getView]);
|
||||
|
||||
const applyRemoteProgress = async (book: Book, bookDoc: BookDoc, remote: KoSyncProgress) => {
|
||||
const view = getView(bookKey);
|
||||
if (FIXED_LAYOUT_FORMATS.has(book.format)) {
|
||||
const pageToGo = parseInt(remote.progress!, 10);
|
||||
if (isNaN(pageToGo)) return;
|
||||
view?.select(pageToGo - 1);
|
||||
} else {
|
||||
if (!remote.progress?.startsWith('/body')) return;
|
||||
try {
|
||||
const content = view?.renderer.getContents()[0];
|
||||
const cfi = await getCFIFromXPointer(
|
||||
remote.progress!,
|
||||
content?.doc,
|
||||
content?.index,
|
||||
bookDoc,
|
||||
);
|
||||
view?.goTo(cfi);
|
||||
} catch (error) {
|
||||
console.error('Failed to convert XPointer to CFI', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
eventDispatcher.dispatch('toast', { message: _('Reading Progress Synced'), type: 'info' });
|
||||
};
|
||||
|
||||
const promptedSync = async (
|
||||
book: Book,
|
||||
bookDoc: BookDoc,
|
||||
local: BookProgress,
|
||||
remote: KoSyncProgress,
|
||||
) => {
|
||||
let localPreview = '';
|
||||
let remotePreview = '';
|
||||
const remotePercentage = remote.percentage || 0;
|
||||
|
||||
if (FIXED_LAYOUT_FORMATS.has(book.format)) {
|
||||
const localPageInfo = local.section;
|
||||
const localPercentage =
|
||||
localPageInfo && localPageInfo.total > 0
|
||||
? Math.round(((localPageInfo.current + 1) / localPageInfo.total) * 100)
|
||||
: 0;
|
||||
localPreview = localPageInfo
|
||||
? _('Page {{page}} of {{total}} ({{percentage}}%)', {
|
||||
page: localPageInfo.current + 1,
|
||||
total: localPageInfo.total,
|
||||
percentage: localPercentage,
|
||||
})
|
||||
: _('Current position');
|
||||
|
||||
const remotePage = parseInt(remote.progress!, 10);
|
||||
if (!isNaN(remotePage) && remotePercentage > 0) {
|
||||
const localTotalPages = localPageInfo?.total ?? 0;
|
||||
const remoteTotalPages = Math.round(remotePage / remotePercentage);
|
||||
const pagesMatch = Math.abs(localTotalPages - remoteTotalPages) <= 1;
|
||||
|
||||
if (pagesMatch) {
|
||||
remotePreview = _('Page {{page}} of {{total}} ({{percentage}}%)', {
|
||||
page: remotePage,
|
||||
total: remoteTotalPages,
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
} else {
|
||||
remotePreview = _('Approximately page {{page}} of {{total}} ({{percentage}}%)', {
|
||||
page: remotePage,
|
||||
total: remoteTotalPages,
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
remotePreview = _('Approximately {{percentage}}%', {
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const localPageInfo = local.pageinfo;
|
||||
const localPercentage =
|
||||
localPageInfo && localPageInfo.total > 0
|
||||
? Math.round(((localPageInfo.current + 1) / localPageInfo.total) * 100)
|
||||
: 0;
|
||||
localPreview = `${local.sectionLabel} (${localPercentage}%)`;
|
||||
|
||||
remotePreview = _('Approximately {{percentage}}%', {
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
}
|
||||
|
||||
setConflictDetails({
|
||||
book,
|
||||
bookDoc,
|
||||
local: { cfi: local.location, preview: localPreview },
|
||||
remote: { ...remote, preview: remotePreview },
|
||||
});
|
||||
};
|
||||
|
||||
const pushProgress = useMemo(
|
||||
() =>
|
||||
debounce(async () => {
|
||||
const { settings: currentSettings } = useSettingsStore.getState();
|
||||
if (!bookKey || !appService || !kosyncClient || !hasPulledOnce.current) return;
|
||||
const { settings } = useSettingsStore.getState();
|
||||
if (['receive', 'disable'].includes(settings.kosync.strategy)) return;
|
||||
|
||||
const currentBook = getBookData(bookKey)?.book;
|
||||
const progress = generateKOProgress();
|
||||
if (!currentBook || !progress || !progress.koProgress) return;
|
||||
|
||||
const { koreaderSyncUsername, koreaderSyncUserkey, koreaderSyncStrategy } = currentSettings;
|
||||
if (
|
||||
!koreaderSyncUsername ||
|
||||
!koreaderSyncUserkey ||
|
||||
['receive', 'disable'].includes(koreaderSyncStrategy) ||
|
||||
!currentBook
|
||||
)
|
||||
return;
|
||||
|
||||
const getDocumentDigest = (bookToDigest: Book): string => {
|
||||
if (currentSettings.koreaderSyncChecksumMethod === 'filename') {
|
||||
const filename = bookToDigest.sourceTitle || bookToDigest.title;
|
||||
const normalizedPath = filename.replace(/\\/g, '/');
|
||||
return md5(
|
||||
normalizedPath.split('/').pop()?.split('.').slice(0, -1).join('.') || normalizedPath,
|
||||
);
|
||||
}
|
||||
return bookToDigest.hash;
|
||||
};
|
||||
|
||||
const getDeviceName = async () => {
|
||||
if (currentSettings.koreaderSyncDeviceName) return currentSettings.koreaderSyncDeviceName;
|
||||
if (appService?.appPlatform === 'tauri') {
|
||||
const name = await osType();
|
||||
return `Readest (${name.charAt(0).toUpperCase() + name.slice(1)})`;
|
||||
}
|
||||
return 'Readest';
|
||||
};
|
||||
|
||||
const digest = getDocumentDigest(currentBook);
|
||||
const progressData = mapProgressToServerFormat();
|
||||
if (!digest || !progressData) return;
|
||||
|
||||
if (progressData.progressStr === lastPushedCfiRef.current) return;
|
||||
|
||||
const deviceName = await getDeviceName();
|
||||
const client = new KOSyncClient(
|
||||
currentSettings.koreaderSyncServerUrl,
|
||||
currentSettings.koreaderSyncUsername,
|
||||
currentSettings.koreaderSyncUserkey,
|
||||
currentSettings.koreaderSyncChecksumMethod,
|
||||
currentSettings.koreaderSyncDeviceId,
|
||||
deviceName,
|
||||
);
|
||||
|
||||
await client.updateProgress(currentBook, progressData.progressStr, progressData.percentage);
|
||||
lastPushedCfiRef.current = progressData.progressStr;
|
||||
await kosyncClient.updateProgress(currentBook, progress.koProgress, progress.percentage);
|
||||
}, 5000),
|
||||
[bookKey, appService, getBookData, mapProgressToServerFormat],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[bookKey, appService, kosyncClient, generateKOProgress],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleFlush = (event: CustomEvent) => {
|
||||
const { bookKey: syncBookKey } = event.detail;
|
||||
if (syncBookKey === bookKey) {
|
||||
pushProgress.flush();
|
||||
}
|
||||
};
|
||||
eventDispatcher.on('flush-koreader-sync', handleFlush);
|
||||
return () => {
|
||||
eventDispatcher.off('flush-koreader-sync', handleFlush);
|
||||
pushProgress.flush();
|
||||
};
|
||||
}, [bookKey, pushProgress]);
|
||||
const pullProgress = useCallback(
|
||||
async () => {
|
||||
if (!progress?.location || !appService || !kosyncClient) return;
|
||||
|
||||
useEffect(() => {
|
||||
const performInitialSync = async () => {
|
||||
const { koreaderSyncUsername, koreaderSyncUserkey, koreaderSyncStrategy } = settings;
|
||||
if (
|
||||
!book ||
|
||||
!bookDoc ||
|
||||
!progress ||
|
||||
!koreaderSyncUsername ||
|
||||
!koreaderSyncUserkey ||
|
||||
koreaderSyncStrategy === 'disabled'
|
||||
)
|
||||
return;
|
||||
const bookData = getBookData(bookKey);
|
||||
const book = bookData?.book;
|
||||
const bookDoc = bookData?.bookDoc;
|
||||
if (!book || !bookDoc) return;
|
||||
|
||||
if (koreaderSyncStrategy === 'send') {
|
||||
syncCompletedForKey.current = bookKey;
|
||||
const { strategy, enabled } = settings.kosync;
|
||||
if (!enabled) return;
|
||||
|
||||
hasPulledOnce.current = true;
|
||||
if (strategy === 'send') {
|
||||
setSyncState('synced');
|
||||
return;
|
||||
}
|
||||
|
||||
setSyncState('checking');
|
||||
|
||||
const getDeviceName = async () => {
|
||||
if (settings.koreaderSyncDeviceName) return settings.koreaderSyncDeviceName;
|
||||
if (appService?.appPlatform === 'tauri') {
|
||||
const name = await osType();
|
||||
return `Readest (${name.charAt(0).toUpperCase() + name.slice(1)})`;
|
||||
}
|
||||
return 'Readest';
|
||||
};
|
||||
|
||||
const deviceName = await getDeviceName();
|
||||
const client = new KOSyncClient(
|
||||
settings.koreaderSyncServerUrl,
|
||||
settings.koreaderSyncUsername,
|
||||
settings.koreaderSyncUserkey,
|
||||
settings.koreaderSyncChecksumMethod,
|
||||
settings.koreaderSyncDeviceId,
|
||||
deviceName,
|
||||
);
|
||||
const remote = await client.getProgress(book);
|
||||
lastPushedCfiRef.current = progress.location;
|
||||
|
||||
if (!remote?.progress || !remote?.timestamp) {
|
||||
syncCompletedForKey.current = bookKey;
|
||||
const remoteProgress = await kosyncClient.getProgress(book);
|
||||
if (!remoteProgress || !remoteProgress.progress || !remoteProgress.timestamp) {
|
||||
setSyncState('synced');
|
||||
if (settings.koreaderSyncStrategy !== 'receive') {
|
||||
pushProgress();
|
||||
pushProgress.flush();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const localTimestamp = bookData?.config?.updatedAt || book.updatedAt;
|
||||
const remoteIsNewer = remote.timestamp * 1000 > localTimestamp;
|
||||
|
||||
const localIdentifier = FIXED_LAYOUT_FORMATS.has(book.format)
|
||||
? progress.section?.current.toString()
|
||||
: progress.location;
|
||||
const isLocalCFI = localIdentifier?.startsWith('epubcfi');
|
||||
|
||||
const remoteIdentifier = FIXED_LAYOUT_FORMATS.has(book.format)
|
||||
? (parseInt(remote.progress, 10) - 1).toString()
|
||||
: remote.progress.startsWith('epubcfi')
|
||||
? remote.progress
|
||||
: null;
|
||||
const isRemoteCFI = remoteIdentifier?.startsWith('epubcfi');
|
||||
|
||||
let isProgressIdentical = false;
|
||||
if (isLocalCFI && isRemoteCFI) {
|
||||
isProgressIdentical = localIdentifier === remoteIdentifier;
|
||||
}
|
||||
|
||||
if (!isProgressIdentical) {
|
||||
const localPercentage = mapProgressToServerFormat()?.percentage ?? 0;
|
||||
const remotePercentage = remote.percentage;
|
||||
|
||||
if (remotePercentage !== undefined && remotePercentage !== null) {
|
||||
const tolerance = settings.koreaderSyncPercentageTolerance;
|
||||
const percentageDifference = Math.abs(localPercentage - remotePercentage);
|
||||
isProgressIdentical = percentageDifference < tolerance;
|
||||
}
|
||||
}
|
||||
|
||||
if (isProgressIdentical) {
|
||||
lastPushedCfiRef.current = localIdentifier;
|
||||
syncCompletedForKey.current = bookKey;
|
||||
const remoteIsNewer = remoteProgress.timestamp * 1000 > localTimestamp;
|
||||
if (strategy === 'receive' || (strategy === 'silent' && remoteIsNewer)) {
|
||||
applyRemoteProgress(book, bookDoc, remoteProgress);
|
||||
setSyncState('synced');
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
settings.koreaderSyncStrategy === 'receive' ||
|
||||
(settings.koreaderSyncStrategy === 'silent' && remoteIsNewer)
|
||||
) {
|
||||
const applyRemoteProgress = async () => {
|
||||
const view = getView(bookKey);
|
||||
if (view && remote.progress) {
|
||||
if (FIXED_LAYOUT_FORMATS.has(book.format)) {
|
||||
const pageToGo = parseInt(remote.progress, 10);
|
||||
if (!isNaN(pageToGo)) view.select(pageToGo - 1);
|
||||
} else {
|
||||
const isXPointer = remote.progress.startsWith('/body');
|
||||
if (isXPointer) {
|
||||
try {
|
||||
const content = view.renderer.getContents()[0];
|
||||
if (content) {
|
||||
const { doc, index } = content;
|
||||
const cfi = await getCFIFromXPointer(remote.progress, doc, index || 0, bookDoc);
|
||||
view.goTo(cfi);
|
||||
eventDispatcher.dispatch('toast', {
|
||||
message: _('Reading Progress Synced'),
|
||||
type: 'info',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to convert XPointer to CFI, falling back to percentage.',
|
||||
error,
|
||||
);
|
||||
if (remote.percentage !== undefined && remote.percentage !== null) {
|
||||
view.goToFraction(remote.percentage);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (remote.percentage !== undefined && remote.percentage !== null) {
|
||||
view.goToFraction(remote.percentage);
|
||||
}
|
||||
}
|
||||
}
|
||||
eventDispatcher.dispatch('toast', {
|
||||
message: _('Reading Progress Synced'),
|
||||
type: 'info',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
applyRemoteProgress();
|
||||
syncCompletedForKey.current = bookKey;
|
||||
setSyncState('synced');
|
||||
} else if (settings.koreaderSyncStrategy === 'prompt') {
|
||||
let localPreview = '';
|
||||
let remotePreview = '';
|
||||
const remotePercentage = remote.percentage || 0;
|
||||
|
||||
if (FIXED_LAYOUT_FORMATS.has(book.format)) {
|
||||
const localPageInfo = progress.section;
|
||||
const localPercentage =
|
||||
localPageInfo && localPageInfo.total > 0
|
||||
? Math.round(((localPageInfo.current + 1) / localPageInfo.total) * 100)
|
||||
: 0;
|
||||
localPreview = localPageInfo
|
||||
? _('Page {{page}} of {{total}} ({{percentage}}%)', {
|
||||
page: localPageInfo.current + 1,
|
||||
total: localPageInfo.total,
|
||||
percentage: localPercentage,
|
||||
})
|
||||
: _('Current position');
|
||||
|
||||
const remotePage = parseInt(remote.progress, 10);
|
||||
if (!isNaN(remotePage) && remotePercentage > 0) {
|
||||
const localTotalPages = localPageInfo?.total ?? 0;
|
||||
const remoteTotalPages = Math.round(remotePage / remotePercentage);
|
||||
const pagesMatch = Math.abs(localTotalPages - remoteTotalPages) <= 1;
|
||||
|
||||
if (pagesMatch) {
|
||||
remotePreview = _('Page {{page}} of {{total}} ({{percentage}}%)', {
|
||||
page: remotePage,
|
||||
total: remoteTotalPages,
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
} else {
|
||||
remotePreview = _('Approximately page {{page}} of {{total}} ({{percentage}}%)', {
|
||||
page: remotePage,
|
||||
total: remoteTotalPages,
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
remotePreview = _('Approximately {{percentage}}%', {
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const localPageInfo = progress.pageinfo;
|
||||
const localPercentage =
|
||||
localPageInfo && localPageInfo.total > 0
|
||||
? Math.round(((localPageInfo.current + 1) / localPageInfo.total) * 100)
|
||||
: 0;
|
||||
localPreview = `${progress.sectionLabel} (${localPercentage}%)`;
|
||||
|
||||
remotePreview = _('Approximately {{percentage}}%', {
|
||||
percentage: Math.round(remotePercentage * 100),
|
||||
});
|
||||
}
|
||||
|
||||
setConflictDetails({
|
||||
book,
|
||||
bookDoc,
|
||||
local: { cfi: progress.location, preview: localPreview },
|
||||
remote: { ...remote, preview: remotePreview, percentage: remote.percentage },
|
||||
});
|
||||
} else if (strategy === 'prompt') {
|
||||
promptedSync(book, bookDoc, progress, remoteProgress);
|
||||
setSyncState('conflict');
|
||||
} else {
|
||||
syncCompletedForKey.current = bookKey;
|
||||
setSyncState('synced');
|
||||
}
|
||||
};
|
||||
|
||||
if (bookKey && book && progress && syncCompletedForKey.current !== bookKey) {
|
||||
syncCompletedForKey.current = bookKey;
|
||||
performInitialSync();
|
||||
}
|
||||
}, [
|
||||
bookKey,
|
||||
book,
|
||||
bookDoc,
|
||||
progress,
|
||||
settings,
|
||||
appService,
|
||||
getBookData,
|
||||
getProgress,
|
||||
getView,
|
||||
mapProgressToServerFormat,
|
||||
pushProgress,
|
||||
_,
|
||||
bookData?.config?.updatedAt,
|
||||
]);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[bookKey, appService, kosyncClient, settings.kosync, progress],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePullProgress = (event: CustomEvent) => {
|
||||
if (event.detail.bookKey !== bookKey) return;
|
||||
pullProgress();
|
||||
};
|
||||
const handlePushProgress = (event: CustomEvent) => {
|
||||
if (event.detail.bookKey !== bookKey) return;
|
||||
pushProgress();
|
||||
};
|
||||
const handleFlush = (event: CustomEvent) => {
|
||||
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();
|
||||
};
|
||||
}, [bookKey, pushProgress, pullProgress]);
|
||||
|
||||
// Pull: pull progress once when the book is opened
|
||||
useEffect(() => {
|
||||
if (!appService || !kosyncClient || !progress?.location) return;
|
||||
if (hasPulledOnce.current) return;
|
||||
|
||||
pullProgress();
|
||||
}, [appService, kosyncClient, progress?.location, pushProgress, pullProgress]);
|
||||
|
||||
// Push: auto-push progress when progress changes with a debounce
|
||||
useEffect(() => {
|
||||
if (syncState === 'synced' && progress) {
|
||||
if (
|
||||
settings.koreaderSyncStrategy !== 'receive' &&
|
||||
settings.koreaderSyncStrategy !== 'disabled'
|
||||
) {
|
||||
const { strategy, enabled } = settings.kosync;
|
||||
if (strategy !== 'receive' && enabled) {
|
||||
pushProgress();
|
||||
}
|
||||
}
|
||||
}, [progress, syncState, settings.koreaderSyncStrategy, pushProgress]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
pushProgress.flush();
|
||||
};
|
||||
}, [pushProgress]);
|
||||
}, [progress, syncState, settings.kosync, pushProgress]);
|
||||
|
||||
const resolveConflictWithLocal = () => {
|
||||
pushProgress();
|
||||
@@ -421,50 +290,12 @@ export const useKOSync = (bookKey: string) => {
|
||||
const resolveConflictWithRemote = async () => {
|
||||
const view = getView(bookKey);
|
||||
const remote = conflictDetails?.remote;
|
||||
const currentBook = conflictDetails?.book;
|
||||
const book = conflictDetails?.book;
|
||||
const bookDoc = conflictDetails?.bookDoc;
|
||||
|
||||
if (view && remote?.progress && currentBook) {
|
||||
if (FIXED_LAYOUT_FORMATS.has(currentBook.format)) {
|
||||
const localTotalPages = getProgress(bookKey)?.section?.total ?? 0;
|
||||
const remotePage = parseInt(remote.progress, 10);
|
||||
const remotePercentage = remote.percentage || 0;
|
||||
const remoteTotalPages =
|
||||
remotePercentage > 0 ? Math.round(remotePage / remotePercentage) : 0;
|
||||
if (!book || !bookDoc || !remote || !remote.progress || !view) return;
|
||||
|
||||
if (!isNaN(remotePage) && Math.abs(localTotalPages - remoteTotalPages) <= 1) {
|
||||
console.log('Going to remote page:', remotePage);
|
||||
view.select(remotePage - 1);
|
||||
} else if (remote.percentage !== undefined && remote.percentage !== null) {
|
||||
console.log('Going to remote percentage:', remote.percentage);
|
||||
view.goToFraction(remote.percentage);
|
||||
}
|
||||
} else {
|
||||
const isXPointer = remote.progress.startsWith('/body');
|
||||
const isCFI = remote.progress.startsWith('epubcfi');
|
||||
|
||||
if (isXPointer) {
|
||||
try {
|
||||
const content = view.renderer.getContents()[0];
|
||||
if (content) {
|
||||
const { doc, index } = content;
|
||||
const cfi = await getCFIFromXPointer(remote.progress, doc, index || 0, bookDoc);
|
||||
view.goTo(cfi);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to convert XPointer to CFI, falling back to percentage.', error);
|
||||
if (remote.percentage !== undefined && remote.percentage !== null) {
|
||||
view.goToFraction(remote.percentage);
|
||||
}
|
||||
}
|
||||
} else if (isCFI) {
|
||||
view.goTo(remote.progress);
|
||||
} else if (remote.percentage !== undefined && remote.percentage !== null) {
|
||||
view.goToFraction(remote.percentage);
|
||||
}
|
||||
}
|
||||
eventDispatcher.dispatch('toast', { message: _('Reading Progress Synced'), type: 'info' });
|
||||
}
|
||||
applyRemoteProgress(book, bookDoc, remote);
|
||||
setSyncState('synced');
|
||||
setConflictDetails(null);
|
||||
};
|
||||
@@ -473,8 +304,9 @@ export const useKOSync = (bookKey: string) => {
|
||||
syncState,
|
||||
conflictDetails,
|
||||
errorMessage,
|
||||
pushProgress,
|
||||
pullProgress,
|
||||
resolveConflictWithLocal,
|
||||
resolveConflictWithRemote,
|
||||
pushProgress,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user