manga details done

This commit is contained in:
Aria Moradi 2021-01-19 20:20:28 +03:30
parent 590be4f04b
commit 0b2d49f3f6
12 changed files with 283 additions and 67 deletions

View File

@ -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))
}
}

View File

@ -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,

View File

@ -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
}
}

View File

@ -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,
)
}
}
}

View File

@ -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<MangaDataClass> {
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<MangaDataClass> {
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<MangaDataClass> {
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,
)
}
}
}

View File

@ -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() {
<Route path="/sources">
<Sources />
</Route>
<Route path="/manga/:id">
<Manga />
</Route>
<Route path="/">
<Home />
</Route>

View File

@ -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 (
<>
<li>
<Card>
<CardContent className={classes.root}>
<div style={{ display: 'flex' }}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" component="h2">
{name}
</Typography>
<Typography variant="caption" display="block" gutterBottom>
{relaseDate}
</Typography>
</div>
</div>
<div style={{ display: 'flex' }}>
<Button variant="outlined" style={{ marginLeft: 20 }} onClick={() => { /* window.location.href = 'sources/popular/'; */ }}>open</Button>
</div>
</CardContent>
</Card>
</li>
</>
);
}

View File

@ -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 (
<Card className={classes.root}>
<CardActionArea>
<div className={classes.wrapper}>
<CardMedia
className={classes.image}
component="img"
alt={title}
image={thumbnailUrl}
title={title}
/>
<div className={classes.gradient} />
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
</div>
</CardActionArea>
</Card>
<Link to={`/manga/${id}/`}>
<Card className={classes.root}>
<CardActionArea>
<div className={classes.wrapper}>
<CardMedia
className={classes.image}
component="img"
alt={title}
image={thumbnailUrl}
title={title}
/>
<div className={classes.gradient} />
<Typography className={classes.title} variant="h5" component="h2">{title}</Typography>
</div>
</CardActionArea>
</Card>
</Link>
);
}

View File

@ -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<IManga>();
useEffect(() => {
fetch(`http://127.0.0.1:4567/api/v1/manga/${id}/`)
.then((response) => response.json())
.then((data) => setManga(data));
}, []);
return (
<>
<h1>
{manga && manga.title}
</h1>
</>
);
}

View File

@ -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 (
<>
<MangaDetails id={id} />
<ol style={{ listStyle: 'none', padding: 0 }}>
<ChapterCard />
</ol>
</>
);
}

View File

@ -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 })),
));
}, []);

View File

@ -17,6 +17,7 @@ interface ISource {
}
interface IManga {
id: number
title: string
thumbnailUrl: string
}