implemented infinite scroll

This commit is contained in:
Aria Moradi 2021-01-22 21:11:00 +03:30
parent 2c76ad9b74
commit 0757ea5d0d
7 changed files with 99 additions and 34 deletions

View File

@ -18,3 +18,8 @@ data class MangaDataClass(
val genre: String? = null, val genre: String? = null,
val status: String = MangaStatus.UNKNOWN.name val status: String = MangaStatus.UNKNOWN.name
) )
data class PagedMangaListDataClass(
val mangaList: List<MangaDataClass>,
val hasNextPage: Boolean
)

View File

@ -2,13 +2,14 @@ package ir.armor.tachidesk.util
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import ir.armor.tachidesk.database.dataclass.MangaDataClass 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.MangaStatus
import ir.armor.tachidesk.database.table.MangaTable import ir.armor.tachidesk.database.table.MangaTable
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<MangaDataClass> { fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
val source = getHttpSource(sourceId.toLong()) val source = getHttpSource(sourceId.toLong())
val mangasPage = if (popular) { val mangasPage = if (popular) {
source.fetchPopularManga(pageNum).toBlocking().first() source.fetchPopularManga(pageNum).toBlocking().first()
@ -21,9 +22,9 @@ fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List<Manga
return mangasPage.processEntries(sourceId) return mangasPage.processEntries(sourceId)
} }
fun MangasPage.processEntries(sourceId: Long): List<MangaDataClass> { fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
val mangasPage = this val mangasPage = this
return transaction { val mangaList = transaction {
return@transaction mangasPage.mangas.map { manga -> return@transaction mangasPage.mangas.map { manga ->
var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull() var mangaEntry = MangaTable.select { MangaTable.url eq manga.url }.firstOrNull()
var mangaEntityId = if (mangaEntry == null) { // create manga entry var mangaEntityId = if (mangaEntry == null) { // create manga entry
@ -62,4 +63,8 @@ fun MangasPage.processEntries(sourceId: Long): List<MangaDataClass> {
) )
} }
} }
return PagedMangaListDataClass(
mangaList,
mangasPage.hasNextPage
)
} }

View File

@ -1,13 +1,13 @@
package ir.armor.tachidesk.util package ir.armor.tachidesk.util
import ir.armor.tachidesk.database.dataclass.MangaDataClass import ir.armor.tachidesk.database.dataclass.PagedMangaListDataClass
fun sourceFilters(sourceId: Long) { fun sourceFilters(sourceId: Long) {
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
// source.getFilterList().toItems() // source.getFilterList().toItems()
} }
fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): List<MangaDataClass> { fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first() val searchManga = source.fetchSearchManga(pageNum, searchTerm, source.getFilterList()).toBlocking().first()
return searchManga.processEntries(sourceId) return searchManga.processEntries(sourceId)

View File

@ -38,8 +38,10 @@ const useStyles = makeStyles({
interface IProps { interface IProps {
manga: IManga manga: IManga
// eslint-disable-next-line react/no-unused-prop-types, react/require-default-props
// ref?: false | React.MutableRefObject<HTMLInputElement | undefined>
} }
export default function MangaCard(props: IProps) { const MangaCard = React.forwardRef((props: IProps, ref) => {
const { const {
manga: { manga: {
id, title, thumbnailUrl, id, title, thumbnailUrl,
@ -49,7 +51,7 @@ export default function MangaCard(props: IProps) {
return ( return (
<Link to={`/manga/${id}/`}> <Link to={`/manga/${id}/`}>
<Card className={classes.root}> <Card className={classes.root} ref={ref}>
<CardActionArea> <CardActionArea>
<div className={classes.wrapper}> <div className={classes.wrapper}>
<CardMedia <CardMedia
@ -66,4 +68,6 @@ export default function MangaCard(props: IProps) {
</Card> </Card>
</Link> </Link>
); );
} });
export default MangaCard;

View File

@ -1,26 +1,54 @@
import React from 'react'; import React, { useEffect, useRef } from 'react';
import MangaCard from './MangaCard'; import MangaCard from './MangaCard';
interface IProps{ interface IProps{
mangas: IManga[] mangas: IManga[]
message?: string message?: string
hasNextPage: boolean
lastPageNum: number
setLastPageNum: (lastPageNum: number) => void
} }
export default function MangaGrid(props: IProps) { export default function MangaGrid(props: IProps) {
const { mangas, message } = props; const {
mangas, message, hasNextPage, lastPageNum, setLastPageNum,
} = props;
let mapped; let mapped;
const lastManga = useRef<HTMLInputElement>();
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) { if (mangas.length === 0) {
mapped = <h3>{message !== undefined ? message : 'loading...'}</h3>; mapped = <h3>{message}</h3>;
} else { } else {
mapped = ( mapped = mangas.map((it, idx) => {
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, auto)', gridGap: '1em' }}> if (idx === mangas.length - 1) {
{mangas.map((it) => ( return <MangaCard manga={it} ref={lastManga} />;
<MangaCard manga={it} /> }
))} return <MangaCard manga={it} />;
</div> });
);
} }
return mapped; return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, auto)', gridGap: '1em' }}>
{mapped}
</div>
);
} }
MangaGrid.defaultProps = {
message: 'loading...',
};

View File

@ -7,7 +7,8 @@ export default function MangaList(props: { popular: boolean }) {
const { sourceId } = useParams<{sourceId: string}>(); const { sourceId } = useParams<{sourceId: string}>();
const { setTitle } = useContext(NavBarTitle); const { setTitle } = useContext(NavBarTitle);
const [mangas, setMangas] = useState<IManga[]>([]); const [mangas, setMangas] = useState<IManga[]>([]);
const [lastPageNum] = useState<number>(1); const [hasNextPage, setHasNextPage] = useState<boolean>(false);
const [lastPageNum, setLastPageNum] = useState<number>(1);
useEffect(() => { useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}`) 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'; const sourceType = props.popular ? 'popular' : 'latest';
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`) fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/${sourceType}/${lastPageNum}`)
.then((response) => response.json()) .then((response) => response.json())
.then((data: IManga[]) => setMangas( .then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
data.map((it) => ({ title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id })), setMangas([
)); ...mangas,
}, []); ...data.mangaList.map((it) => ({
title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id,
}))]);
setHasNextPage(data.hasNextPage);
});
}, [lastPageNum]);
return <MangaGrid mangas={mangas} />; return (
<MangaGrid
mangas={mangas}
hasNextPage={hasNextPage}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
/>
);
} }

View File

@ -22,8 +22,9 @@ export default function SearchSingle() {
const [error, setError] = useState<boolean>(false); const [error, setError] = useState<boolean>(false);
const [mangas, setMangas] = useState<IManga[]>([]); const [mangas, setMangas] = useState<IManga[]>([]);
const [message, setMessage] = useState<string>(''); const [message, setMessage] = useState<string>('');
const [lastPageNum] = useState<number>(1);
const [searchTerm, setSearchTerm] = useState<string>(''); const [searchTerm, setSearchTerm] = useState<string>('');
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
const [lastPageNum, setLastPageNum] = useState<number>(1);
const textInput = React.createRef<HTMLInputElement>(); const textInput = React.createRef<HTMLInputElement>();
@ -51,13 +52,14 @@ export default function SearchSingle() {
if (searchTerm.length > 0) { if (searchTerm.length > 0) {
fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`) fetch(`http://127.0.0.1:4567/api/v1/source/${sourceId}/search/${searchTerm}/${lastPageNum}`)
.then((response) => response.json()) .then((response) => response.json())
.then((data: IManga[]) => { .then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
if (data.length > 0) { if (data.mangaList.length > 0) {
setMangas( setMangas([
data.map((it) => ( ...mangas,
{ title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id } ...data.mangaList.map((it) => ({
)), title: it.title, thumbnailUrl: it.thumbnailUrl, id: it.id,
); }))]);
setHasNextPage(data.hasNextPage);
} else { } else {
setMessage('search qeury returned nothing.'); setMessage('search qeury returned nothing.');
} }
@ -65,7 +67,15 @@ export default function SearchSingle() {
} }
}, [searchTerm]); }, [searchTerm]);
const mangaGrid = <MangaGrid mangas={mangas} message={message} />; const mangaGrid = (
<MangaGrid
mangas={mangas}
message={message}
hasNextPage={hasNextPage}
lastPageNum={lastPageNum}
setLastPageNum={setLastPageNum}
/>
);
return ( return (
<> <>