forked from akai/readest
Support reading progress synchronization across platforms (#34)
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
|
||||
export const useAutoHideScrollbar = () => {
|
||||
const shouldAutoHideScrollbar = ['macos', 'ios'].includes(getOSPlatform());
|
||||
const handleScrollbarAutoHide = (doc: Document) => {
|
||||
if (doc && doc.defaultView && doc.defaultView.frameElement) {
|
||||
const iframe = doc.defaultView.frameElement as HTMLIFrameElement;
|
||||
const container = iframe.parentElement?.parentElement;
|
||||
if (!container) return;
|
||||
|
||||
let hideScrollbarTimeout: ReturnType<typeof setTimeout>;
|
||||
const showScrollbar = () => {
|
||||
container.style.overflow = 'auto';
|
||||
container.style.scrollbarWidth = 'thin';
|
||||
};
|
||||
|
||||
const hideScrollbar = () => {
|
||||
container.style.overflow = 'hidden';
|
||||
container.style.scrollbarWidth = 'none';
|
||||
requestAnimationFrame(() => {
|
||||
container.style.overflow = 'auto';
|
||||
});
|
||||
};
|
||||
container.addEventListener('scroll', () => {
|
||||
showScrollbar();
|
||||
clearTimeout(hideScrollbarTimeout);
|
||||
hideScrollbarTimeout = setTimeout(hideScrollbar, 1000);
|
||||
});
|
||||
hideScrollbar();
|
||||
}
|
||||
};
|
||||
|
||||
return { shouldAutoHideScrollbar, handleScrollbarAutoHide };
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useEffect } from 'react';
|
||||
import { FoliateView } from '@/types/view';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
|
||||
export const useClickEvent = (
|
||||
bookKey: string,
|
||||
viewRef: React.MutableRefObject<FoliateView | null>,
|
||||
containerRef: React.RefObject<HTMLDivElement>,
|
||||
) => {
|
||||
const { getViewSettings } = useReaderStore();
|
||||
const { hoveredBookKey, setHoveredBookKey } = useReaderStore();
|
||||
const handleTurnPage = (msg: MessageEvent | React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
if (msg instanceof MessageEvent) {
|
||||
if (msg.data && msg.data.bookKey === bookKey) {
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
if (msg.data.type === 'iframe-single-click') {
|
||||
if (viewSettings.disableClick!) {
|
||||
return;
|
||||
}
|
||||
const viewElement = containerRef.current;
|
||||
if (viewElement) {
|
||||
const rect = viewElement.getBoundingClientRect();
|
||||
const { screenX, screenY } = msg.data;
|
||||
const consumed = eventDispatcher.dispatchSync('iframe-single-click', {
|
||||
screenX,
|
||||
screenY,
|
||||
});
|
||||
if (!consumed) {
|
||||
const centerStartX = rect.left + rect.width * 0.375;
|
||||
const centerEndX = rect.left + rect.width * 0.625;
|
||||
const centerStartY = rect.top + rect.height * 0.375;
|
||||
const centerEndY = rect.top + rect.height * 0.625;
|
||||
if (
|
||||
screenX >= centerStartX &&
|
||||
screenX <= centerEndX &&
|
||||
screenY >= centerStartY &&
|
||||
screenY <= centerEndY
|
||||
) {
|
||||
// toggle visibility of the header bar and the footer bar
|
||||
setHoveredBookKey(hoveredBookKey ? '' : bookKey);
|
||||
} else if (screenX >= rect.left + rect.width / 2) {
|
||||
viewRef.current?.goRight();
|
||||
} else if (screenX < rect.left + rect.width / 2) {
|
||||
viewRef.current?.goLeft();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (msg.data.type === 'iframe-wheel' && !viewSettings.scrolled) {
|
||||
const { deltaY } = msg.data;
|
||||
if (deltaY > 0) {
|
||||
viewRef.current?.next(1);
|
||||
} else if (deltaY < 0) {
|
||||
viewRef.current?.prev(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const { clientX } = msg;
|
||||
const width = window.innerWidth;
|
||||
const leftThreshold = width * 0.5;
|
||||
const rightThreshold = width * 0.5;
|
||||
if (clientX < leftThreshold) {
|
||||
viewRef.current?.goLeft();
|
||||
} else if (clientX > rightThreshold) {
|
||||
viewRef.current?.goRight();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', handleTurnPage);
|
||||
return () => {
|
||||
window.removeEventListener('message', handleTurnPage);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hoveredBookKey, viewRef]);
|
||||
|
||||
return {
|
||||
handleTurnPage,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { FoliateView } from '@/app/reader/components/FoliateViewer';
|
||||
import { FoliateView } from '@/types/view';
|
||||
|
||||
type FoliateEventHandler = {
|
||||
onLoad?: (event: Event) => void;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useSync } from '@/hooks/useSync';
|
||||
import { BookConfig } from '@/types/book';
|
||||
import { deserializeConfig, serializeConfig } from '@/utils/serializer';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { DEFAULT_BOOK_SEARCH_CONFIG, SYNC_PROGRESS_INTERVAL_SEC } from '@/services/constants';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
|
||||
export const useProgressSync = (
|
||||
bookKey: string,
|
||||
config: BookConfig,
|
||||
setToastMessage?: React.Dispatch<React.SetStateAction<string>>,
|
||||
) => {
|
||||
const { setConfig } = useBookDataStore();
|
||||
const { getView } = useReaderStore();
|
||||
const { settings } = useSettingsStore();
|
||||
const { syncedConfigs, syncConfigs } = useSync();
|
||||
const { user } = useAuth();
|
||||
const view = getView(bookKey);
|
||||
|
||||
const pushConfig = (bookKey: string, config: BookConfig) => {
|
||||
const bookHash = bookKey.split('-')[0]!;
|
||||
const newConfig = { bookHash, ...config };
|
||||
const compressedConfig = JSON.parse(
|
||||
serializeConfig(newConfig, settings.globalViewSettings, DEFAULT_BOOK_SEARCH_CONFIG),
|
||||
);
|
||||
syncConfigs([compressedConfig], bookHash, 'push');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
const bookHash = bookKey.split('-')[0]!;
|
||||
syncConfigs([], bookHash, 'pull');
|
||||
return () => {
|
||||
pushConfig(bookKey, config);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const lastProgressSyncTime = useRef<number>(0);
|
||||
const syncTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
const now = Date.now();
|
||||
const timeSinceLastSync = now - lastProgressSyncTime.current;
|
||||
if (configSynced.current && timeSinceLastSync > SYNC_PROGRESS_INTERVAL_SEC * 1000) {
|
||||
lastProgressSyncTime.current = now;
|
||||
pushConfig(bookKey, config);
|
||||
} else {
|
||||
if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current);
|
||||
syncTimeoutRef.current = setTimeout(
|
||||
() => {
|
||||
lastProgressSyncTime.current = Date.now();
|
||||
pushConfig(bookKey, config);
|
||||
syncTimeoutRef.current = null;
|
||||
},
|
||||
SYNC_PROGRESS_INTERVAL_SEC * 1000 - timeSinceLastSync,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config]);
|
||||
|
||||
// sync progress once when the book is opened
|
||||
const configSynced = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!configSynced.current && syncedConfigs?.length > 0) {
|
||||
const syncedConfig = syncedConfigs.filter((c) => c.bookHash === bookKey.split('-')[0])[0];
|
||||
if (syncedConfig) {
|
||||
const newConfig = deserializeConfig(
|
||||
JSON.stringify(syncedConfig),
|
||||
settings.globalViewSettings,
|
||||
DEFAULT_BOOK_SEARCH_CONFIG,
|
||||
);
|
||||
setConfig(bookKey, { ...config, ...newConfig });
|
||||
configSynced.current = true;
|
||||
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) {
|
||||
view?.goToFraction(syncedFraction);
|
||||
setToastMessage?.('Progress synced');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [syncedConfigs]);
|
||||
};
|
||||
Reference in New Issue
Block a user