feat(manga): resume location

This commit is contained in:
5rahim
2026-04-07 12:33:09 +02:00
parent 10366a71a4
commit f814ecf9b8
3 changed files with 180 additions and 8 deletions

View File

@@ -76,6 +76,11 @@ export function MangaVerticalReader({ pageContainer }: MangaVerticalReaderProps)
pageDiv?.scrollIntoView()
}, [scrollSignal])
useUpdateEffect(() => {
const pageDiv = containerRef.current?.querySelector(`#page-${currentPageIndex}`)
pageDiv?.scrollIntoView()
}, [pageContainer?.pages])
/**
* Function to handle scroll event
*
@@ -197,7 +202,7 @@ export function MangaVerticalReader({ pageContainer }: MangaVerticalReaderProps)
onFinishedLoading={() => {
// If the first page is loaded, set the current page index to 0
// This is to avoid the current page index to remain incorrect when multiple pages are loading
if (index === 0) {
if (index === 0 && currentPageIndex === 0) {
setCurrentPageIndex(0)
}
handlePageLoad(index)

View File

@@ -23,6 +23,8 @@ import {
__manga_paginationMapAtom,
__manga_readingDirectionAtom,
__manga_readingModeAtom,
__manga_resumeLocationsAtom,
cleanupMangaResumeLocations,
MangaReadingDirection,
MangaReadingMode,
} from "@/app/(main)/manga/_lib/manga-chapter-reader.atoms"
@@ -62,6 +64,7 @@ export function ChapterReaderDrawer(props: ChapterDrawerProps) {
const currentChapter = useCurrentChapter()
const setCurrentChapter = useSetCurrentChapter()
const currentPageIndex = useAtomValue(__manga_currentPageIndexAtom)
const setCurrentPageIndex = useSetAtom(__manga_currentPageIndexAtom)
const setCurrentPaginationMapIndex = useSetAtom(__manga_currentPaginationMapIndexAtom)
@@ -74,8 +77,40 @@ export function ChapterReaderDrawer(props: ChapterDrawerProps) {
const [hiddenBar, setHideBar] = useAtom(__manga_hiddenBarAtom)
// resume locations
const [resumeLocations, setResumeLocations] = useAtom(__manga_resumeLocationsAtom)
const currentMediaResumeKey = React.useMemo(() => {
if (!currentChapter) return undefined
return String(currentChapter.mediaId)
}, [currentChapter])
const currentChapterResumeKey = React.useMemo(() => {
if (!currentChapter) return undefined
return `${currentChapter.mediaId}:${currentChapter.provider}:${currentChapter.chapterId}`
}, [currentChapter])
const currentResumeLocation = React.useMemo(() => {
if (!currentMediaResumeKey) return undefined
return resumeLocations[currentMediaResumeKey]
}, [currentMediaResumeKey, resumeLocations])
const shouldRestoreSavedPage = React.useMemo(() => {
if (!currentChapter || !currentResumeLocation) return false
return (
currentResumeLocation.chapterId === currentChapter.chapterId
&& currentResumeLocation.provider === currentChapter.provider
)
}, [currentChapter, currentResumeLocation])
const restoredChapterResumeKeyRef = React.useRef<string | null>(null)
useSwitchSettingsWithKeys()
React.useEffect(() => {
setResumeLocations(prev => cleanupMangaResumeLocations(prev))
}, [])
const { inject, remove } = useSeaCommandInject()
/**
@@ -176,18 +211,96 @@ export function ChapterReaderDrawer(props: ChapterDrawerProps) {
pageContainerLoading])
/**
* Reset the current page index when the pageContainer or chapterContainer changes
* This signals that the user has switched chapters
* Restore the saved page when the user opens a chapter
* This runs before the new page container settles so horizontal mode can hydrate the right map index
*/
const previousChapterId = React.useRef(currentChapter?.chapterId)
React.useEffect(() => {
// Avoid resetting the page index when we're still on the same chapter
if (currentChapter?.chapterId !== previousChapterId.current) {
if (!currentChapterResumeKey) {
restoredChapterResumeKeyRef.current = null
setCurrentPageIndex(0)
setCurrentPaginationMapIndex(0)
previousChapterId.current = currentChapter?.chapterId
return
}
}, [pageContainer?.pages, chapterContainer?.chapters])
restoredChapterResumeKeyRef.current = null
setCurrentPageIndex(shouldRestoreSavedPage ? currentResumeLocation?.pageIndex ?? 0 : 0)
setCurrentPaginationMapIndex(0)
}, [currentChapterResumeKey])
/**
* Clamp the restored page once the chapter pages are available
* Providers can return a different page count between sessions
*/
React.useEffect(() => {
if (!currentChapterResumeKey || pageContainerLoading || pageContainerError || !pageContainer?.pages?.length) return
if (restoredChapterResumeKeyRef.current === currentChapterResumeKey) return
const maxPageIndex = pageContainer.pages.length - 1
const savedPageIndex = shouldRestoreSavedPage ? currentResumeLocation?.pageIndex ?? 0 : 0
const nextPageIndex = Math.min(savedPageIndex, maxPageIndex)
setCurrentPageIndex(nextPageIndex)
restoredChapterResumeKeyRef.current = currentChapterResumeKey
if (savedPageIndex !== nextPageIndex && currentMediaResumeKey && currentChapter) {
setResumeLocations(prev => cleanupMangaResumeLocations({
...prev,
[currentMediaResumeKey]: {
chapterId: currentChapter.chapterId,
provider: currentChapter.provider,
pageIndex: nextPageIndex,
updatedAt: Date.now(),
},
}))
}
}, [
currentChapterResumeKey,
currentMediaResumeKey,
currentResumeLocation,
currentChapter,
pageContainer?.pages,
pageContainerError,
pageContainerLoading,
shouldRestoreSavedPage,
])
/**
* Autosave the current page after the chapter resume point has been restored
*/
React.useEffect(() => {
if (!currentChapterResumeKey || !currentMediaResumeKey || !currentChapter || pageContainerLoading || pageContainerError || !pageContainer?.pages?.length) return
if (restoredChapterResumeKeyRef.current !== currentChapterResumeKey) return
if (currentPageIndex < 0 || currentPageIndex > pageContainer.pages.length - 1) return
setResumeLocations(prev => {
const currentLocation = prev[currentMediaResumeKey]
if (
currentLocation?.chapterId === currentChapter.chapterId
&& currentLocation?.provider === currentChapter.provider
&& currentLocation?.pageIndex === currentPageIndex
) {
return prev
}
return cleanupMangaResumeLocations({
...prev,
[currentMediaResumeKey]: {
chapterId: currentChapter.chapterId,
provider: currentChapter.provider,
pageIndex: currentPageIndex,
updatedAt: Date.now(),
},
})
})
}, [
currentChapterResumeKey,
currentMediaResumeKey,
currentChapter,
currentPageIndex,
pageContainer?.pages,
pageContainerError,
pageContainerLoading,
])
// Progress update keyboard shortcuts
React.useEffect(() => {

View File

@@ -5,6 +5,60 @@ export const __manga_currentPageIndexAtom = atom(0)
export const __manga_currentPaginationMapIndexAtom = atom(0) // HORIZONTAL MODE
export const __manga_paginationMapAtom = atom<Record<number, number[]>>({})
export type MangaReaderResumeLocation = {
chapterId: string
provider: string
pageIndex: number
updatedAt: number
}
export const MANGA_READER_RESUME_MAX_ENTRIES = 250
export const MANGA_READER_RESUME_MAX_AGE_MS = 1000 * 60 * 60 * 24 * 90
export function cleanupMangaResumeLocations(locations: Record<string, MangaReaderResumeLocation>) {
const now = Date.now()
const trimmedLocations = Object.entries(locations)
.filter(([key, value]) => {
return (
!!key
&& typeof value?.chapterId === "string"
&& value.chapterId.length > 0
&& typeof value?.provider === "string"
&& value.provider.length > 0
&& Number.isInteger(value?.pageIndex)
&& value.pageIndex >= 0
&& Number.isFinite(value?.updatedAt)
&& now - value.updatedAt <= MANGA_READER_RESUME_MAX_AGE_MS
)
})
.sort((a, b) => b[1].updatedAt - a[1].updatedAt)
.slice(0, MANGA_READER_RESUME_MAX_ENTRIES)
const nextLocations = Object.fromEntries(trimmedLocations)
const sameSize = Object.keys(nextLocations).length === Object.keys(locations).length
if (!sameSize) return nextLocations
for (const [key, value] of Object.entries(nextLocations)) {
if (
locations[key]?.pageIndex !== value.pageIndex
|| locations[key]?.updatedAt !== value.updatedAt
) {
return nextLocations
}
}
return locations
}
export const __manga_resumeLocationsAtom = atomWithStorage<Record<string, MangaReaderResumeLocation>>(
"sea-manga-resume-locations",
{},
undefined,
{ getOnInit: true },
)
export const __manga_hiddenBarAtom = atom(false)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////