frontend/src/features/chapter/hooks/useReadingProgressSync.ts

128 lines
3.2 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from "react";
import {
useGetProgress1,
useUpdateProgress,
} from "@/api/generated/reading-progress/reading-progress";
interface ReadingTrackerData {
chapterPage: { [chapterId: number]: number };
updatedAt?: { [chapterId: number]: string };
}
export const useReadingProgressSync = (mangaId: number, chapterId: number) => {
const [currentPage, setCurrentPage] = useState<number | null>(() => {
const jsonString = localStorage.getItem("readingTrackerData");
if (jsonString) {
try {
const data: ReadingTrackerData = JSON.parse(jsonString);
return data.chapterPage[chapterId] ?? null;
} catch (e) {
console.error("Failed to parse local progress", e);
}
}
return null;
});
const lastSyncedPage = useRef<number | null>(null);
const syncTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const { data: progressData, isLoading: isLoadingProgress } = useGetProgress1(
mangaId,
chapterId,
{ query: { retry: false } },
);
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
useEffect(() => {
const jsonString = localStorage.getItem("readingTrackerData");
if (jsonString) {
try {
const data: ReadingTrackerData = JSON.parse(jsonString);
const localPage = data.chapterPage[chapterId] ?? null;
setCurrentPage(localPage);
lastSyncedPage.current = localPage;
} catch (e) {
// ignore
}
} else {
setCurrentPage(null);
lastSyncedPage.current = null;
}
}, [chapterId]);
const saveLocalProgress = useCallback(
(page: number, forceSync = false) => {
setCurrentPage(page);
const jsonString = localStorage.getItem("readingTrackerData");
let data: ReadingTrackerData = { chapterPage: {}, updatedAt: {} };
if (jsonString) {
try {
data = JSON.parse(jsonString);
} catch (e) {
// fallback to empty
}
}
data.chapterPage[chapterId] = page;
data.updatedAt = data.updatedAt || {};
data.updatedAt[chapterId] = new Date().toISOString();
localStorage.setItem("readingTrackerData", JSON.stringify(data));
// Debounced backend sync
if (syncTimerRef.current) clearTimeout(syncTimerRef.current);
const performSync = () => {
if (forceSync || page !== lastSyncedPage.current) {
updateProgress({
data: {
mangaId,
chapterId,
pageNumber: page,
},
});
lastSyncedPage.current = page;
}
};
if (forceSync) {
performSync();
} else {
syncTimerRef.current = setTimeout(performSync, 5000);
}
},
[mangaId, chapterId, updateProgress],
);
useEffect(() => {
return () => {
if (syncTimerRef.current) clearTimeout(syncTimerRef.current);
};
}, []);
const applyServerProgress = useCallback(() => {
if (serverProgress) {
saveLocalProgress(serverProgress.pageNumber);
return serverProgress.pageNumber;
}
return null;
}, [serverProgress, saveLocalProgress]);
return {
currentPage,
serverProgress,
isLoadingProgress,
saveLocalProgress,
applyServerProgress,
};
};