mirror of
https://github.com/readest/readest
synced 2026-04-21 13:37:44 +00:00
feat(kosync): defer push after resume (#3892)
* feat(kosync): defer push after resume * PR feedback updates: * follow established patterns from other hooks * error handling * fix typos
This commit is contained in:
parent
31e44d2e4d
commit
5c97b2e9d8
2 changed files with 94 additions and 6 deletions
|
|
@ -11,6 +11,8 @@ import { debounce } from '@/utils/debounce';
|
|||
import { eventDispatcher } from '@/utils/event';
|
||||
import { getCFIFromXPointer, XCFI } from '@/utils/xcfi';
|
||||
|
||||
import { useWindowActiveChanged } from './useWindowActiveChanged';
|
||||
|
||||
type SyncState = 'idle' | 'checking' | 'conflict' | 'synced' | 'error';
|
||||
|
||||
export interface SyncDetails {
|
||||
|
|
@ -252,53 +254,75 @@ export const useKOSync = (bookKey: string) => {
|
|||
[bookKey, appService, kosyncClient, settings.kosync, progress],
|
||||
);
|
||||
|
||||
// use a ref to track the current push/pull functions so they can change without triggering effects
|
||||
const syncRefs = useRef({ pushProgress, pullProgress });
|
||||
useEffect(() => {
|
||||
syncRefs.current = { pushProgress, pullProgress };
|
||||
}, [pushProgress, pullProgress]);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePushProgress = (event: CustomEvent) => {
|
||||
const { pushProgress } = syncRefs.current;
|
||||
if (event.detail.bookKey !== bookKey) return;
|
||||
pushProgress();
|
||||
pushProgress.flush();
|
||||
};
|
||||
const handleFlush = (event: CustomEvent) => {
|
||||
const { pushProgress } = syncRefs.current;
|
||||
if (event.detail.bookKey !== bookKey) return;
|
||||
pushProgress.flush();
|
||||
};
|
||||
eventDispatcher.on('push-kosync', handlePushProgress);
|
||||
eventDispatcher.on('flush-kosync', handleFlush);
|
||||
return () => {
|
||||
const { pushProgress } = syncRefs.current;
|
||||
eventDispatcher.off('push-kosync', handlePushProgress);
|
||||
eventDispatcher.off('flush-kosync', handleFlush);
|
||||
pushProgress.flush();
|
||||
};
|
||||
}, [bookKey, pushProgress]);
|
||||
}, [bookKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePullProgress = (event: CustomEvent) => {
|
||||
if (event.detail.bookKey !== bookKey) return;
|
||||
const { pullProgress } = syncRefs.current;
|
||||
pullProgress();
|
||||
};
|
||||
eventDispatcher.on('pull-kosync', handlePullProgress);
|
||||
return () => {
|
||||
eventDispatcher.off('pull-kosync', handlePullProgress);
|
||||
};
|
||||
}, [bookKey, pullProgress]);
|
||||
}, [bookKey]);
|
||||
|
||||
// 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]);
|
||||
syncRefs.current.pullProgress();
|
||||
}, [appService, kosyncClient, progress?.location]);
|
||||
|
||||
// Push: auto-push progress when progress changes with a debounce
|
||||
useEffect(() => {
|
||||
if (syncState === 'synced' && progress) {
|
||||
const { strategy, enabled } = settings.kosync;
|
||||
if (strategy !== 'receive' && enabled) {
|
||||
pushProgress();
|
||||
syncRefs.current.pushProgress();
|
||||
}
|
||||
}
|
||||
}, [progress, syncState, settings.kosync, pushProgress]);
|
||||
}, [progress, syncState, settings.kosync]);
|
||||
|
||||
useWindowActiveChanged((isActive) => {
|
||||
const { pushProgress, pullProgress } = syncRefs.current;
|
||||
|
||||
if (isActive) {
|
||||
hasPulledOnce.current = false;
|
||||
pullProgress();
|
||||
} else {
|
||||
pushProgress();
|
||||
pushProgress.flush();
|
||||
}
|
||||
});
|
||||
|
||||
const resolveWithLocal = () => {
|
||||
pushProgress();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
// used to execute a callback when the "active" state of the current window changes.
|
||||
// On web and mobile, "active" means "is visible". On desktop, the 'visibilitychange'
|
||||
// event is unreliable, so "active" means "has focus".
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
|
||||
export type ActiveCallback = (isActive: boolean) => void;
|
||||
|
||||
type Cleanup = () => void;
|
||||
async function activeChangedDesktop(onChange: ActiveCallback): Promise<Cleanup> {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
const appWindow = getCurrentWindow();
|
||||
|
||||
const unlisten = await appWindow.onFocusChanged(({ payload: isActive }) => onChange(isActive));
|
||||
|
||||
return () => {
|
||||
unlisten();
|
||||
};
|
||||
}
|
||||
async function activeChangedOther(onChange: ActiveCallback): Promise<Cleanup> {
|
||||
const handler = () => onChange(document.visibilityState === 'visible');
|
||||
|
||||
document.addEventListener('visibilitychange', handler);
|
||||
return () => document.removeEventListener('visibilitychange', handler);
|
||||
}
|
||||
|
||||
export function useWindowActiveChanged(callback: ActiveCallback) {
|
||||
const onActiveChanged = useRef<ActiveCallback>(callback);
|
||||
const { appService } = useEnv();
|
||||
|
||||
const subscribe = appService?.isDesktopApp ? activeChangedDesktop : activeChangedOther;
|
||||
|
||||
useEffect(() => {
|
||||
onActiveChanged.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => {
|
||||
let isAlive = true;
|
||||
let unsub: Cleanup | undefined;
|
||||
const onChange = (isActive: boolean) => {
|
||||
onActiveChanged.current?.(isActive);
|
||||
};
|
||||
|
||||
subscribe(onChange)
|
||||
.then((cleanup) => {
|
||||
if (isAlive) {
|
||||
unsub = cleanup;
|
||||
} else {
|
||||
// component was already unmounted, just clean up immediately
|
||||
cleanup();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Could not listen for window active changes', e);
|
||||
});
|
||||
|
||||
return () => {
|
||||
isAlive = false;
|
||||
unsub?.();
|
||||
};
|
||||
}, [subscribe]);
|
||||
}
|
||||
Loading…
Reference in a new issue