diff --git a/server/src/main/kotlin/ir/armor/tachidesk/Main.kt b/server/src/main/kotlin/ir/armor/tachidesk/Main.kt index bf8d476..b731a67 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/Main.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/Main.kt @@ -9,8 +9,6 @@ import xyz.nulldev.androidcompat.AndroidCompat import xyz.nulldev.androidcompat.AndroidCompatInitializer import xyz.nulldev.ts.config.ConfigKodeinModule import xyz.nulldev.ts.config.GlobalConfigManager -import xyz.nulldev.ts.config.ServerConfig -import java.util.* class Main { companion object { @@ -62,16 +60,23 @@ class Main { ctx.json(getSourceList()) } - app.get("/api/v1/source/:source_id/popular/:pageNum") { ctx -> - val sourceId = ctx.pathParam("source_id") + app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx -> + val sourceId = ctx.pathParam("sourceId").toLong() val pageNum = ctx.pathParam("pageNum").toInt() - ctx.json(getPopularManga(sourceId,pageNum)) + ctx.json(getMangaList(sourceId,pageNum,popular = true)) } - app.get("/api/v1/source/:source_id/latest/:pageNum") { ctx -> - val sourceId = ctx.pathParam("source_id") + app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx -> + val sourceId = ctx.pathParam("sourceId").toLong() val pageNum = ctx.pathParam("pageNum").toInt() - ctx.json(getLatestManga(sourceId,pageNum)) + ctx.json(getMangaList(sourceId,pageNum,popular = false)) } + + app.get("/api/v1/manga/:mangaId/") { ctx -> + val mangaId = ctx.pathParam("mangaId").toInt() + ctx.json(getManga(mangaId)) + } + + } 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 5c11fe6..47df1f4 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 @@ -3,6 +3,7 @@ package ir.armor.tachidesk.database.dataclass import ir.armor.tachidesk.database.table.MangaStatus data class MangaDataClass( + val id: Int, val sourceId: Long, val url: String, diff --git a/server/src/main/kotlin/ir/armor/tachidesk/database/table/MangaTable.kt b/server/src/main/kotlin/ir/armor/tachidesk/database/table/MangaTable.kt index 3117e64..6c4a3da 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/database/table/MangaTable.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/database/table/MangaTable.kt @@ -1,5 +1,6 @@ package ir.armor.tachidesk.database.table +import eu.kanade.tachiyomi.source.model.SManga import org.jetbrains.exposed.dao.id.IntIdTable object MangaTable : IntIdTable() { @@ -11,7 +12,9 @@ object MangaTable : IntIdTable() { val author = varchar("author", 64).nullable() val description = varchar("description", 4096).nullable() val genre = varchar("genre", 1024).nullable() - val status = enumeration("status", MangaStatus::class).default(MangaStatus.UNKNOWN) + + // val status = enumeration("status", MangaStatus::class).default(MangaStatus.UNKNOWN) + val status = integer("status").default(SManga.UNKNOWN) val thumbnail_url = varchar("thumbnail_url", 2048).nullable() // source is used by some ancestor of IntIdTable @@ -22,5 +25,9 @@ enum class MangaStatus(val status: Int) { UNKNOWN(0), ONGOING(1), COMPLETED(2), - LICENSED(3), + LICENSED(3); + + companion object { + fun valueOf(value: Int): MangaStatus = values().find { it.status == value } ?: UNKNOWN + } } \ No newline at end of file diff --git a/server/src/main/kotlin/ir/armor/tachidesk/util/Manga.kt b/server/src/main/kotlin/ir/armor/tachidesk/util/Manga.kt new file mode 100644 index 0000000..c8bc4d8 --- /dev/null +++ b/server/src/main/kotlin/ir/armor/tachidesk/util/Manga.kt @@ -0,0 +1,80 @@ +package ir.armor.tachidesk.util + +import eu.kanade.tachiyomi.source.model.SManga +import ir.armor.tachidesk.database.dataclass.MangaDataClass +import ir.armor.tachidesk.database.table.MangaStatus +import ir.armor.tachidesk.database.table.MangaTable +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.update + +fun getManga(mangaId: Int): MangaDataClass { + return transaction { + var mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! + + return@transaction if (mangaEntry[MangaTable.initialized]) { + MangaDataClass( + mangaId, + mangaEntry[MangaTable.sourceReference].value, + + mangaEntry[MangaTable.url], + mangaEntry[MangaTable.title], + mangaEntry[MangaTable.thumbnail_url], + + true, + + mangaEntry[MangaTable.artist], + mangaEntry[MangaTable.author], + mangaEntry[MangaTable.description], + mangaEntry[MangaTable.genre], + MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, + ) + } else { // initialize manga + val source = getHttpSource(mangaEntry[MangaTable.sourceReference].value) + val fetchedManga = source.fetchMangaDetails( + SManga.create().apply { + url = mangaEntry[MangaTable.url] + title = mangaEntry[MangaTable.title] + } + ).toBlocking().first() + + // update database + MangaTable.update({ MangaTable.id eq mangaId }) { +// it[url] = fetchedManga.url +// it[title] = fetchedManga.title + it[initialized] = true + + it[artist] = fetchedManga.artist + it[author] = fetchedManga.author + it[description] = fetchedManga.description + it[genre] = fetchedManga.genre + it[status] = fetchedManga.status + if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty()) + it[thumbnail_url] = fetchedManga.thumbnail_url + } + + mangaEntry = MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! + + MangaDataClass( + mangaId, + mangaEntry[MangaTable.sourceReference].value, + + + mangaEntry[MangaTable.url], + mangaEntry[MangaTable.title], + mangaEntry[MangaTable.thumbnail_url], + + true, + + mangaEntry[MangaTable.artist], + mangaEntry[MangaTable.author], + mangaEntry[MangaTable.description], + mangaEntry[MangaTable.genre], + MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, + ) + } + } + + +} \ No newline at end of file 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 935377a..542c4f0 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/util/MangaList.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/util/MangaList.kt @@ -2,45 +2,60 @@ package ir.armor.tachidesk.util import ir.armor.tachidesk.database.dataclass.MangaDataClass import ir.armor.tachidesk.database.table.MangaStatus +import ir.armor.tachidesk.database.table.MangaTable +import ir.armor.tachidesk.database.table.SourceTable +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.insertAndGetId +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction -fun getPopularManga(sourceId: String, pageNum: Int = 1): List { - val manguasPage = getHttpSource(sourceId.toLong()).fetchPopularManga(pageNum).toBlocking().first() - return manguasPage.mangas.map { - MangaDataClass( - sourceId.toLong(), - - it.url, - it.title, - it.thumbnail_url, - - it.initialized, - - it.artist, - it.author, - it.description, - it.genre, - MangaStatus.values().first { that -> it.status == that.status }.name, - ) +fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): List { + val source = getHttpSource(sourceId.toLong()) + val mangasPage = if (popular) { + source.fetchPopularManga(pageNum).toBlocking().first() + } else { + if (source.supportsLatest) + source.fetchLatestUpdates(pageNum).toBlocking().first() + else + throw Exception("Source $source doesn't support latest") } -} + return 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 + MangaTable.insertAndGetId { + it[url] = manga.url + it[title] = manga.title -fun getLatestManga(sourceId: String, pageNum: Int = 1): List { - val manguasPage = getHttpSource(sourceId.toLong()).fetchLatestUpdates(pageNum).toBlocking().first() - return manguasPage.mangas.map { - MangaDataClass( - sourceId.toLong(), + it[artist] = manga.artist + it[author] = manga.author + it[description] = manga.description + it[genre] = manga.genre + it[status] = manga.status + it[thumbnail_url] = manga.genre - it.url, - it.title, - it.thumbnail_url, + it[sourceReference] = sourceId + }.value + } else { + mangaEntry[MangaTable.id].value + } - it.initialized, + MangaDataClass( + mangaEntityId, + sourceId.toLong(), - it.artist, - it.author, - it.description, - it.genre, - MangaStatus.values().first { that -> it.status == that.status }.name, - ) + manga.url, + manga.title, + manga.thumbnail_url, + + manga.initialized, + + manga.artist, + manga.author, + manga.description, + manga.genre, + MangaStatus.valueOf(manga.status).name, + ) + } } } \ No newline at end of file diff --git a/webUI/react/src/App.tsx b/webUI/react/src/App.tsx index 99b98f8..d618f5a 100644 --- a/webUI/react/src/App.tsx +++ b/webUI/react/src/App.tsx @@ -7,6 +7,7 @@ import Home from './screens/Home'; import Sources from './screens/Sources'; import Extensions from './screens/Extensions'; import MangaList from './screens/MangaList'; +import Manga from './screens/Manga'; export default function App() { return ( @@ -26,6 +27,9 @@ export default function App() { + + + diff --git a/webUI/react/src/components/ChapterCard.tsx b/webUI/react/src/components/ChapterCard.tsx new file mode 100644 index 0000000..5c3839e --- /dev/null +++ b/webUI/react/src/components/ChapterCard.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import Button from '@material-ui/core/Button'; +import Typography from '@material-ui/core/Typography'; + +const useStyles = makeStyles((theme) => ({ + root: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: 16, + }, + bullet: { + display: 'inline-block', + margin: '0 2px', + transform: 'scale(0.8)', + }, + title: { + fontSize: 14, + }, + pos: { + marginBottom: 12, + }, + icon: { + width: theme.spacing(7), + height: theme.spacing(7), + flex: '0 0 auto', + marginRight: 16, + }, +})); + +export default function ChapterCard() { + const name = 'Chapter 1'; + const relaseDate = '16/01/21'; + // const downloaded = false; + // const downloadedText = downloaded ? 'open' : 'download'; + const classes = useStyles(); + + return ( + <> +
  • + + +
    +
    + + {name} + + + {relaseDate} + +
    +
    +
    + +
    +
    +
    +
  • + + ); +} diff --git a/webUI/react/src/components/MangaCard.tsx b/webUI/react/src/components/MangaCard.tsx index 1695ef4..aefe032 100644 --- a/webUI/react/src/components/MangaCard.tsx +++ b/webUI/react/src/components/MangaCard.tsx @@ -4,6 +4,7 @@ import Card from '@material-ui/core/Card'; import CardActionArea from '@material-ui/core/CardActionArea'; import CardMedia from '@material-ui/core/CardMedia'; import Typography from '@material-ui/core/Typography'; +import { Link } from 'react-router-dom'; const useStyles = makeStyles({ root: { @@ -41,26 +42,28 @@ interface IProps { export default function MangaCard(props: IProps) { const { manga: { - title, thumbnailUrl, + id, title, thumbnailUrl, }, } = props; const classes = useStyles(); return ( - - -
    - -
    - {title} -
    - - + + + +
    + +
    + {title} +
    + + + ); } diff --git a/webUI/react/src/components/MangaDetails.tsx b/webUI/react/src/components/MangaDetails.tsx new file mode 100644 index 0000000..cef8044 --- /dev/null +++ b/webUI/react/src/components/MangaDetails.tsx @@ -0,0 +1,24 @@ +import React, { useEffect, useState } from 'react'; + +interface IProps{ + id: string +} + +export default function MangaDetails(props: IProps) { + const { id } = props; + const [manga, setManga] = useState(); + + useEffect(() => { + fetch(`http://127.0.0.1:4567/api/v1/manga/${id}/`) + .then((response) => response.json()) + .then((data) => setManga(data)); + }, []); + + return ( + <> +

    + {manga && manga.title} +

    + + ); +} diff --git a/webUI/react/src/screens/Manga.tsx b/webUI/react/src/screens/Manga.tsx new file mode 100644 index 0000000..a2a2f61 --- /dev/null +++ b/webUI/react/src/screens/Manga.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import ChapterCard from '../components/ChapterCard'; +import MangaDetails from '../components/MangaDetails'; + +export default function Manga() { + const { id } = useParams<{id: string}>(); + + return ( + <> + +
      + +
    + + ); +} diff --git a/webUI/react/src/screens/MangaList.tsx b/webUI/react/src/screens/MangaList.tsx index 720ed1e..d7d86f4 100644 --- a/webUI/react/src/screens/MangaList.tsx +++ b/webUI/react/src/screens/MangaList.tsx @@ -2,11 +2,6 @@ import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import MangaCard from '../components/MangaCard'; -interface IManga { - title: string - thumbnailUrl: string -} - export default function MangaList(props: { popular: boolean }) { const { sourceId } = useParams<{sourceId: string}>(); let mapped; @@ -17,8 +12,8 @@ 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: { title: string, thumbnail_url: string }[]) => setMangas( - data.map((it) => ({ title: it.title, thumbnailUrl: it.thumbnail_url })), + .then((data: { title: string, thumbnail_url: string, id:number }[]) => setMangas( + data.map((it) => ({ title: it.title, thumbnailUrl: it.thumbnail_url, id: it.id })), )); }, []); diff --git a/webUI/react/src/typings.d.ts b/webUI/react/src/typings.d.ts index beb0074..e93efcb 100644 --- a/webUI/react/src/typings.d.ts +++ b/webUI/react/src/typings.d.ts @@ -17,6 +17,7 @@ interface ISource { } interface IManga { + id: number title: string thumbnailUrl: string }