diff --git a/server/src/main/kotlin/ir/armor/tachidesk/database/dataclass/MangaDataClass.kt b/server/src/main/kotlin/ir/armor/tachidesk/database/dataclass/MangaDataClass.kt index 0ef9b40..07e7057 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/database/dataclass/MangaDataClass.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/database/dataclass/MangaDataClass.kt @@ -18,3 +18,8 @@ data class MangaDataClass( val genre: String? = null, val status: String = MangaStatus.UNKNOWN.name ) + +data class PagedMangaListDataClass( + val mangaList: List, + val hasNextPage: Boolean +) diff --git a/server/src/main/kotlin/ir/armor/tachidesk/util/MangaList.kt b/server/src/main/kotlin/ir/armor/tachidesk/util/MangaList.kt index d881a35..54af911 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/util/MangaList.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/util/MangaList.kt @@ -2,13 +2,14 @@ package ir.armor.tachidesk.util import eu.kanade.tachiyomi.source.model.MangasPage import ir.armor.tachidesk.database.dataclass.MangaDataClass +import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass import ir.armor.tachidesk.database.table.MangaStatus import ir.armor.tachidesk.database.table.MangaTable import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction -fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List { +fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass { val source = getHttpSource(sourceId.toLong()) val mangasPage = if (popular) { source.fetchPopularManga(pageNum).toBlocking().first() @@ -21,9 +22,9 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List { +fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass { val mangasPage = this - return transaction { + val mangaList = transaction { return@transaction mangasPage.mangas.map { manga -> var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull() var mangaEntityId = if (mangaEntry == null) { // create manga entry @@ -62,4 +63,8 @@ fun MangasPage.processEntries(sourceId: Long): List { ) } } + return PagedMangaListDataClass( + mangaList, + mangasPage.hasNextPage + ) } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/util/Search.kt b/server/src/main/kotlin/ir/armor/tachidesk/util/Search.kt index 4947f97..5c21ab3 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/util/Search.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/util/Search.kt @@ -1,13 +1,13 @@ package ir.armor.tachidesk.util -import ir.armor.tachidesk.database.dataclass.MangaDataClass +import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass fun sourceFilters(sourceId: Long) { val source = getHttpSource(sourceId) // source.getFilterList().toItems() } -fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): List { +fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass { val source = getHttpSource(sourceId) val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first() return searchManga.processEntries(sourceId) diff --git a/webUI/react/src/components/MangaCard.tsx b/webUI/react/src/components/MangaCard.tsx index aefe032..d6505b6 100644 --- a/webUI/react/src/components/MangaCard.tsx +++ b/webUI/react/src/components/MangaCard.tsx @@ -38,8 +38,10 @@ const useStyles = makeStyles({ interface IProps { manga: IManga + // eslint-disable-next-line react/no-unused-prop-types, react/require-default-props + // ref?: false | React.MutableRefObject } -export default function MangaCard(props: IProps) { +const MangaCard = React.forwardRef((props: IProps, ref) => { const { manga: { id, title, thumbnailUrl, @@ -49,7 +51,7 @@ export default function MangaCard(props: IProps) { return ( - +
); -} +}); + +export default MangaCard; diff --git a/webUI/react/src/components/MangaGrid.tsx b/webUI/react/src/components/MangaGrid.tsx index 891397a..519dd00 100644 --- a/webUI/react/src/components/MangaGrid.tsx +++ b/webUI/react/src/components/MangaGrid.tsx @@ -1,26 +1,54 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import MangaCard from './MangaCard'; interface IProps{ mangas: IManga[] message?: string + hasNextPage: boolean + lastPageNum: number + setLastPageNum: (lastPageNum: number) => void } export default function MangaGrid(props: IProps) { - const { mangas, message } = props; + const { + mangas, message, hasNextPage, lastPageNum, setLastPageNum, + } = props; let mapped; + const lastManga = useRef(); + + const scrollHandler = () => { + if (lastManga.current) { + const rect = lastManga.current.getBoundingClientRect(); + if (((rect.y + rect.height) / window.innerHeight < 2) && hasNextPage) { + setLastPageNum(lastPageNum + 1); + } + } + }; + useEffect(() => { + window.addEventListener('scroll', scrollHandler, true); + return () => { + window.removeEventListener('scroll', scrollHandler, true); + }; + }, [hasNextPage, mangas]); if (mangas.length === 0) { - mapped =

{message !== undefined ? message : 'loading...'}

; + mapped =

{message}

; } else { - mapped = ( -
- {mangas.map((it) => ( - - ))} -
- ); + mapped = mangas.map((it, idx) => { + if (idx === mangas.length - 1) { + return ; + } + return ; + }); } - return mapped; + return ( +
+ {mapped} +
+ ); } + +MangaGrid.defaultProps = { + message: 'loading...', +}; diff --git a/webUI/react/src/screens/MangaList.tsx b/webUI/react/src/screens/MangaList.tsx index fd44c62..a3d854c 100644 --- a/webUI/react/src/screens/MangaList.tsx +++ b/webUI/react/src/screens/MangaList.tsx @@ -7,7 +7,8 @@ export default function MangaList(props: { popular: boolean }) { const { sourceId } = useParams<{sourceId: string}>(); const { setTitle } = useContext(NavBarTitle); const [mangas, setMangas] = useState([]); - const [lastPageNum] = useState(1); + const [hasNextPage, setHasNextPage] = useState(false); + const [lastPageNum, setLastPageNum] = useState(1); useEffect(() => { fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}`) @@ -19,10 +20,22 @@ export default function MangaList(props: { popular: boolean }) { const sourceType = props.popular ? 'popular' : 'latest'; fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`) .then((response) => response.json()) - .then((data: IManga[]) => setMangas( - data.map((it) => ({ title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id })), - )); - }, []); + .then((data: { mangaList: IManga[], hasNextPage: boolean }) => { + setMangas([ + ...mangas, + ...data.mangaList.map((it) => ({ + title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id, + }))]); + setHasNextPage(data.hasNextPage); + }); + }, [lastPageNum]); - return ; + return ( + + ); } diff --git a/webUI/react/src/screens/SearchSingle.tsx b/webUI/react/src/screens/SearchSingle.tsx index a0f9b02..6773676 100644 --- a/webUI/react/src/screens/SearchSingle.tsx +++ b/webUI/react/src/screens/SearchSingle.tsx @@ -22,8 +22,9 @@ export default function SearchSingle() { const [error, setError] = useState(false); const [mangas, setMangas] = useState([]); const [message, setMessage] = useState(''); - const [lastPageNum] = useState(1); const [searchTerm, setSearchTerm] = useState(''); + const [hasNextPage, setHasNextPage] = useState(false); + const [lastPageNum, setLastPageNum] = useState(1); const textInput = React.createRef(); @@ -51,13 +52,14 @@ export default function SearchSingle() { if (searchTerm.length > 0) { fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`) .then((response) => response.json()) - .then((data: IManga[]) => { - if (data.length > 0) { - setMangas( - data.map((it) => ( - { title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id } - )), - ); + .then((data: { mangaList: IManga[], hasNextPage: boolean }) => { + if (data.mangaList.length > 0) { + setMangas([ + ...mangas, + ...data.mangaList.map((it) => ({ + title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id, + }))]); + setHasNextPage(data.hasNextPage); } else { setMessage('search qeury returned nothing.'); } @@ -65,7 +67,15 @@ export default function SearchSingle() { } }, [searchTerm]); - const mangaGrid = ; + const mangaGrid = ( + + ); return ( <>