Merge pull request 'refactor: improve reading progress synchronization logic and add implementation guidelines' (#36) from feat/progress-track into main

Reviewed-on: #36
This commit is contained in:
rov 2026-04-16 14:52:32 -03:00
commit fe7d5957e3
2 changed files with 64 additions and 31 deletions

View File

@ -15,19 +15,14 @@ export const useReadingProgressSync = (mangaId: number, chapterId: number) => {
if (jsonString) { if (jsonString) {
try { try {
const data: ReadingTrackerData = JSON.parse(jsonString); const data: ReadingTrackerData = JSON.parse(jsonString);
return data.chapterPage[chapterId] || 1; return data.chapterPage[chapterId] ?? null;
} catch (e) { } catch (e) {
console.error("Failed to parse local progress", e); console.error("Failed to parse local progress", e);
} }
} }
return 1; return null;
}); });
const [serverProgress, setServerProgress] = useState<{
pageNumber: number;
updatedAt: string;
} | null>(null);
const lastSyncedPage = useRef<number | null>(null); const lastSyncedPage = useRef<number | null>(null);
const syncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); const syncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@ -38,36 +33,36 @@ export const useReadingProgressSync = (mangaId: number, chapterId: number) => {
); );
const { mutate: updateProgress } = useUpdateProgress(); const { mutate: updateProgress } = useUpdateProgress();
const serverProgress =
progressData &&
progressData.pageNumber !== undefined &&
progressData.updatedAt !== undefined
? {
pageNumber: progressData.pageNumber,
updatedAt: progressData.updatedAt,
}
: null;
// Sync local progress when chapterId changes // Sync local progress when chapterId changes
useEffect(() => { useEffect(() => {
const jsonString = localStorage.getItem("readingTrackerData"); const jsonString = localStorage.getItem("readingTrackerData");
if (jsonString) { if (jsonString) {
try { try {
const data: ReadingTrackerData = JSON.parse(jsonString); const data: ReadingTrackerData = JSON.parse(jsonString);
const localPage = data.chapterPage[chapterId] || 1; const localPage = data.chapterPage[chapterId] ?? null;
setCurrentPage(localPage); setCurrentPage(localPage);
lastSyncedPage.current = localPage; lastSyncedPage.current = localPage;
} catch (e) { } catch (e) {
// ignore // ignore
} }
} else { } else {
setCurrentPage(1); setCurrentPage(null);
lastSyncedPage.current = 1; lastSyncedPage.current = null;
} }
}, [chapterId]); }, [chapterId]);
// Sync server progress when available
useEffect(() => {
if (progressData && progressData.pageNumber !== undefined && progressData.updatedAt !== undefined) {
setServerProgress({
pageNumber: progressData.pageNumber,
updatedAt: progressData.updatedAt,
});
}
}, [progressData]);
const saveLocalProgress = useCallback( const saveLocalProgress = useCallback(
(page: number) => { (page: number, forceSync = false) => {
setCurrentPage(page); setCurrentPage(page);
const jsonString = localStorage.getItem("readingTrackerData"); const jsonString = localStorage.getItem("readingTrackerData");
let data: ReadingTrackerData = { chapterPage: {}, updatedAt: {} }; let data: ReadingTrackerData = { chapterPage: {}, updatedAt: {} };
@ -85,8 +80,9 @@ export const useReadingProgressSync = (mangaId: number, chapterId: number) => {
// Debounced backend sync // Debounced backend sync
if (syncTimerRef.current) clearTimeout(syncTimerRef.current); if (syncTimerRef.current) clearTimeout(syncTimerRef.current);
syncTimerRef.current = setTimeout(() => {
if (page !== lastSyncedPage.current) { const performSync = () => {
if (forceSync || page !== lastSyncedPage.current) {
updateProgress({ updateProgress({
data: { data: {
mangaId, mangaId,
@ -96,11 +92,23 @@ export const useReadingProgressSync = (mangaId: number, chapterId: number) => {
}); });
lastSyncedPage.current = page; lastSyncedPage.current = page;
} }
}, 5000); };
if (forceSync) {
performSync();
} else {
syncTimerRef.current = setTimeout(performSync, 5000);
}
}, },
[mangaId, chapterId, updateProgress], [mangaId, chapterId, updateProgress],
); );
useEffect(() => {
return () => {
if (syncTimerRef.current) clearTimeout(syncTimerRef.current);
};
}, []);
const applyServerProgress = useCallback(() => { const applyServerProgress = useCallback(() => {
if (serverProgress) { if (serverProgress) {
saveLocalProgress(serverProgress.pageNumber); saveLocalProgress(serverProgress.pageNumber);

View File

@ -53,15 +53,33 @@ const Chapter = () => {
const localPage = savedPage; const localPage = savedPage;
const serverPage = serverProgress?.pageNumber; const serverPage = serverProgress?.pageNumber;
if (!localPage && !serverPage) { if (localPage === null) {
// No local history for this chapter, use server or default to 1
const targetPage = serverPage || 1;
setCurrentPage(targetPage);
setVisibleCount(targetPage);
if (targetPage > 1) {
setIsAutoScrolling(true);
}
setHasInitialized(true); setHasInitialized(true);
return; return;
} }
if (localPage && serverPage && localPage !== serverPage) { if (serverPage && localPage !== serverPage) {
setShowConflictDialog(true); // If local is just starting out (page 1), just take server
if (localPage <= 1) {
const targetPage = serverPage;
setCurrentPage(targetPage);
setVisibleCount(targetPage);
if (targetPage > 1) {
setIsAutoScrolling(true);
}
setHasInitialized(true);
} else {
setShowConflictDialog(true);
}
} else { } else {
const targetPage = localPage || serverPage || 1; const targetPage = localPage || 1;
setCurrentPage(targetPage); setCurrentPage(targetPage);
setVisibleCount(targetPage); setVisibleCount(targetPage);
if (targetPage > 1) { if (targetPage > 1) {
@ -181,7 +199,7 @@ const Chapter = () => {
} }
}, [infiniteScroll]); }, [infiniteScroll]);
if (isLoading || !hasInitialized) { if (isLoading || (isLoadingProgress && !hasInitialized)) {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-background"> <div className="flex min-h-screen items-center justify-center bg-background">
<MangaLoadingState /> <MangaLoadingState />
@ -245,7 +263,7 @@ const Chapter = () => {
<Dialog open={showConflictDialog} onOpenChange={setShowConflictDialog}> <Dialog open={showConflictDialog} onOpenChange={setShowConflictDialog}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Progress Conflict</DialogTitle> <DialogTitle>Reading Progress Conflict</DialogTitle>
<DialogDescription> <DialogDescription>
You have different progress saved locally and on the server for You have different progress saved locally and on the server for
this chapter. this chapter.
@ -258,10 +276,17 @@ const Chapter = () => {
</span> </span>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter className="gap-2 sm:gap-0"> <DialogFooter className="flex flex-row justify-end gap-3">
<Button <Button
variant="outline" variant="outline"
onClick={() => { onClick={() => {
if (savedPage && savedPage > 1) {
setCurrentPage(savedPage);
setVisibleCount(savedPage);
setIsAutoScrolling(true);
initialJumpDone.current = false;
saveLocalProgress(savedPage, true);
}
setHasInitialized(true); setHasInitialized(true);
setShowConflictDialog(false); setShowConflictDialog(false);
}} }}