mirror of
https://github.com/tachiyomiorg/tachiyomi-extensions-inspector.git
synced 2025-01-26 07:15:30 +01:00
manga details done
This commit is contained in:
parent
590be4f04b
commit
0b2d49f3f6
@ -9,8 +9,6 @@ import xyz.nulldev.androidcompat.AndroidCompat
|
|||||||
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
import xyz.nulldev.androidcompat.AndroidCompatInitializer
|
||||||
import xyz.nulldev.ts.config.ConfigKodeinModule
|
import xyz.nulldev.ts.config.ConfigKodeinModule
|
||||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||||
import xyz.nulldev.ts.config.ServerConfig
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class Main {
|
class Main {
|
||||||
companion object {
|
companion object {
|
||||||
@ -62,16 +60,23 @@ class Main {
|
|||||||
ctx.json(getSourceList())
|
ctx.json(getSourceList())
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("/api/v1/source/:source_id/popular/:pageNum") { ctx ->
|
app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx ->
|
||||||
val sourceId = ctx.pathParam("source_id")
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
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 ->
|
app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx ->
|
||||||
val sourceId = ctx.pathParam("source_id")
|
val sourceId = ctx.pathParam("sourceId").toLong()
|
||||||
val pageNum = ctx.pathParam("pageNum").toInt()
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package ir.armor.tachidesk.database.dataclass
|
|||||||
import ir.armor.tachidesk.database.table.MangaStatus
|
import ir.armor.tachidesk.database.table.MangaStatus
|
||||||
|
|
||||||
data class MangaDataClass(
|
data class MangaDataClass(
|
||||||
|
val id: Int,
|
||||||
val sourceId: Long,
|
val sourceId: Long,
|
||||||
|
|
||||||
val url: String,
|
val url: String,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package ir.armor.tachidesk.database.table
|
package ir.armor.tachidesk.database.table
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import org.jetbrains.exposed.dao.id.IntIdTable
|
import org.jetbrains.exposed.dao.id.IntIdTable
|
||||||
|
|
||||||
object MangaTable : IntIdTable() {
|
object MangaTable : IntIdTable() {
|
||||||
@ -11,7 +12,9 @@ object MangaTable : IntIdTable() {
|
|||||||
val author = varchar("author", 64).nullable()
|
val author = varchar("author", 64).nullable()
|
||||||
val description = varchar("description", 4096).nullable()
|
val description = varchar("description", 4096).nullable()
|
||||||
val genre = varchar("genre", 1024).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()
|
val thumbnail_url = varchar("thumbnail_url", 2048).nullable()
|
||||||
|
|
||||||
// source is used by some ancestor of IntIdTable
|
// source is used by some ancestor of IntIdTable
|
||||||
@ -22,5 +25,9 @@ enum class MangaStatus(val status: Int) {
|
|||||||
UNKNOWN(0),
|
UNKNOWN(0),
|
||||||
ONGOING(1),
|
ONGOING(1),
|
||||||
COMPLETED(2),
|
COMPLETED(2),
|
||||||
LICENSED(3),
|
LICENSED(3);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun valueOf(value: Int): MangaStatus = values().find { it.status == value } ?: UNKNOWN
|
||||||
|
}
|
||||||
}
|
}
|
80
server/src/main/kotlin/ir/armor/tachidesk/util/Manga.kt
Normal file
80
server/src/main/kotlin/ir/armor/tachidesk/util/Manga.kt
Normal 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -2,45 +2,60 @@ package ir.armor.tachidesk.util
|
|||||||
|
|
||||||
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
import ir.armor.tachidesk.database.dataclass.MangaDataClass
|
||||||
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.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 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
|
||||||
|
|
||||||
|
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[sourceReference] = sourceId
|
||||||
|
}.value
|
||||||
|
} else {
|
||||||
|
mangaEntry[MangaTable.id].value
|
||||||
|
}
|
||||||
|
|
||||||
fun getPopularManga(sourceId: String, pageNum: Int = 1): List<MangaDataClass> {
|
|
||||||
val manguasPage = getHttpSource(sourceId.toLong()).fetchPopularManga(pageNum).toBlocking().first()
|
|
||||||
return manguasPage.mangas.map {
|
|
||||||
MangaDataClass(
|
MangaDataClass(
|
||||||
|
mangaEntityId,
|
||||||
sourceId.toLong(),
|
sourceId.toLong(),
|
||||||
|
|
||||||
it.url,
|
manga.url,
|
||||||
it.title,
|
manga.title,
|
||||||
it.thumbnail_url,
|
manga.thumbnail_url,
|
||||||
|
|
||||||
it.initialized,
|
manga.initialized,
|
||||||
|
|
||||||
it.artist,
|
manga.artist,
|
||||||
it.author,
|
manga.author,
|
||||||
it.description,
|
manga.description,
|
||||||
it.genre,
|
manga.genre,
|
||||||
MangaStatus.values().first { that -> it.status == that.status }.name,
|
MangaStatus.valueOf(manga.status).name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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.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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,6 +7,7 @@ import Home from './screens/Home';
|
|||||||
import Sources from './screens/Sources';
|
import Sources from './screens/Sources';
|
||||||
import Extensions from './screens/Extensions';
|
import Extensions from './screens/Extensions';
|
||||||
import MangaList from './screens/MangaList';
|
import MangaList from './screens/MangaList';
|
||||||
|
import Manga from './screens/Manga';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@ -26,6 +27,9 @@ export default function App() {
|
|||||||
<Route path="/sources">
|
<Route path="/sources">
|
||||||
<Sources />
|
<Sources />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/manga/:id">
|
||||||
|
<Manga />
|
||||||
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<Home />
|
<Home />
|
||||||
</Route>
|
</Route>
|
||||||
|
64
webUI/react/src/components/ChapterCard.tsx
Normal file
64
webUI/react/src/components/ChapterCard.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -4,6 +4,7 @@ import Card from '@material-ui/core/Card';
|
|||||||
import CardActionArea from '@material-ui/core/CardActionArea';
|
import CardActionArea from '@material-ui/core/CardActionArea';
|
||||||
import CardMedia from '@material-ui/core/CardMedia';
|
import CardMedia from '@material-ui/core/CardMedia';
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
root: {
|
root: {
|
||||||
@ -41,12 +42,13 @@ interface IProps {
|
|||||||
export default function MangaCard(props: IProps) {
|
export default function MangaCard(props: IProps) {
|
||||||
const {
|
const {
|
||||||
manga: {
|
manga: {
|
||||||
title, thumbnailUrl,
|
id, title, thumbnailUrl,
|
||||||
},
|
},
|
||||||
} = props;
|
} = props;
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Link to={`/manga/${id}/`}>
|
||||||
<Card className={classes.root}>
|
<Card className={classes.root}>
|
||||||
<CardActionArea>
|
<CardActionArea>
|
||||||
<div className={classes.wrapper}>
|
<div className={classes.wrapper}>
|
||||||
@ -62,5 +64,6 @@ export default function MangaCard(props: IProps) {
|
|||||||
</div>
|
</div>
|
||||||
</CardActionArea>
|
</CardActionArea>
|
||||||
</Card>
|
</Card>
|
||||||
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
24
webUI/react/src/components/MangaDetails.tsx
Normal file
24
webUI/react/src/components/MangaDetails.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
17
webUI/react/src/screens/Manga.tsx
Normal file
17
webUI/react/src/screens/Manga.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -2,11 +2,6 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import MangaCard from '../components/MangaCard';
|
import MangaCard from '../components/MangaCard';
|
||||||
|
|
||||||
interface IManga {
|
|
||||||
title: string
|
|
||||||
thumbnailUrl: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MangaList(props: { popular: boolean }) {
|
export default function MangaList(props: { popular: boolean }) {
|
||||||
const { sourceId } = useParams<{sourceId: string}>();
|
const { sourceId } = useParams<{sourceId: string}>();
|
||||||
let mapped;
|
let mapped;
|
||||||
@ -17,8 +12,8 @@ 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: { title: string, thumbnail_url: string }[]) => setMangas(
|
.then((data: { title: string, thumbnail_url: string, id:number }[]) => setMangas(
|
||||||
data.map((it) => ({ title: it.title, thumbnailUrl: it.thumbnail_url })),
|
data.map((it) => ({ title: it.title, thumbnailUrl: it.thumbnail_url, id: it.id })),
|
||||||
));
|
));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
1
webUI/react/src/typings.d.ts
vendored
1
webUI/react/src/typings.d.ts
vendored
@ -17,6 +17,7 @@ interface ISource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IManga {
|
interface IManga {
|
||||||
|
id: number
|
||||||
title: string
|
title: string
|
||||||
thumbnailUrl: string
|
thumbnailUrl: string
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user