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:
commit
fe7d5957e3
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
}}
|
}}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user