chapter updates when pressing UI buttons

This commit is contained in:
Aria Moradi 2021-05-15 13:43:26 +04:30
parent 866b01f865
commit 01d5c2540d
10 changed files with 141 additions and 69 deletions

View File

@ -15,7 +15,9 @@ import ir.armor.tachidesk.impl.util.awaitSingle
import ir.armor.tachidesk.model.database.table.ChapterTable import ir.armor.tachidesk.model.database.table.ChapterTable
import ir.armor.tachidesk.model.database.table.MangaTable import ir.armor.tachidesk.model.database.table.MangaTable
import ir.armor.tachidesk.model.database.table.PageTable import ir.armor.tachidesk.model.database.table.PageTable
import ir.armor.tachidesk.model.database.table.toDataClass
import ir.armor.tachidesk.model.dataclass.ChapterDataClass import ir.armor.tachidesk.model.dataclass.ChapterDataClass
import org.jetbrains.exposed.sql.SortOrder.DESC
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
@ -26,42 +28,51 @@ import org.jetbrains.exposed.sql.update
object Chapter { object Chapter {
/** get chapter list when showing a manga */ /** get chapter list when showing a manga */
suspend fun getChapterList(mangaId: Int): List<ChapterDataClass> { suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean): List<ChapterDataClass> {
val mangaDetails = getManga(mangaId) return if (!onlineFetch) {
val source = getHttpSource(mangaDetails.sourceId.toLong()) transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }.orderBy(ChapterTable.chapterIndex to DESC)
val chapterList = source.fetchChapterList( .map {
SManga.create().apply { ChapterTable.toDataClass(it)
title = mangaDetails.title
url = mangaDetails.url
}
).awaitSingle()
val chapterCount = chapterList.count()
return transaction {
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
if (chapterEntry == null) {
ChapterTable.insert {
it[url] = fetchedChapter.url
it[name] = fetchedChapter.name
it[date_upload] = fetchedChapter.date_upload
it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1
it[manga] = mangaId
} }
} else { }
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) { } else {
it[name] = fetchedChapter.name
it[date_upload] = fetchedChapter.date_upload
it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1 val mangaDetails = getManga(mangaId)
it[manga] = mangaId val source = getHttpSource(mangaDetails.sourceId.toLong())
val chapterList = source.fetchChapterList(
SManga.create().apply {
title = mangaDetails.title
url = mangaDetails.url
}
).awaitSingle()
val chapterCount = chapterList.count()
transaction {
chapterList.reversed().forEachIndexed { index, fetchedChapter ->
val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull()
if (chapterEntry == null) {
ChapterTable.insert {
it[url] = fetchedChapter.url
it[name] = fetchedChapter.name
it[date_upload] = fetchedChapter.date_upload
it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1
it[manga] = mangaId
}
} else {
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
it[name] = fetchedChapter.name
it[date_upload] = fetchedChapter.date_upload
it[chapter_number] = fetchedChapter.chapter_number
it[scanlator] = fetchedChapter.scanlator
it[chapterIndex] = index + 1
it[manga] = mangaId
}
} }
} }
} }
@ -83,10 +94,12 @@ object Chapter {
} }
} }
val dbChapterMap = transaction { ChapterTable.selectAll() } val dbChapterMap = transaction {
.associateBy({ it[ChapterTable.url] }, { it }) ChapterTable.select { ChapterTable.manga eq mangaId }
.associateBy({ it[ChapterTable.url] }, { it })
}
chapterList.mapIndexed { index, it -> return chapterList.mapIndexed { index, it ->
val dbChapter = dbChapterMap.getValue(it.url) val dbChapter = dbChapterMap.getValue(it.url)

View File

@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl
import ir.armor.tachidesk.impl.Source.getSource import ir.armor.tachidesk.impl.Source.getSource
import ir.armor.tachidesk.impl.util.CachedImageResponse.clearCachedImage
import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse
import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource
import ir.armor.tachidesk.impl.util.await import ir.armor.tachidesk.impl.util.await
@ -35,17 +36,17 @@ object Manga {
text text
} }
suspend fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass { suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass {
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
return if (mangaEntry[MangaTable.initialized]) { return if (mangaEntry[MangaTable.initialized] && !onlineFetch) {
MangaDataClass( MangaDataClass(
mangaId, mangaId,
mangaEntry[MangaTable.sourceReference].toString(), mangaEntry[MangaTable.sourceReference].toString(),
mangaEntry[MangaTable.url], mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title], mangaEntry[MangaTable.title],
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url], proxyThumbnailUrl(mangaId),
true, true,
@ -55,7 +56,8 @@ object Manga {
mangaEntry[MangaTable.genre], mangaEntry[MangaTable.genre],
MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
mangaEntry[MangaTable.inLibrary], mangaEntry[MangaTable.inLibrary],
getSource(mangaEntry[MangaTable.sourceReference]) getSource(mangaEntry[MangaTable.sourceReference]),
false
) )
} else { // initialize manga } else { // initialize manga
val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) val source = getHttpSource(mangaEntry[MangaTable.sourceReference])
@ -81,8 +83,9 @@ object Manga {
} }
} }
clearMangaThumbnail(mangaId)
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val newThumbnail = mangaEntry[MangaTable.thumbnail_url]
MangaDataClass( MangaDataClass(
mangaId, mangaId,
@ -90,7 +93,7 @@ object Manga {
mangaEntry[MangaTable.url], mangaEntry[MangaTable.url],
mangaEntry[MangaTable.title], mangaEntry[MangaTable.title],
if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail, proxyThumbnailUrl(mangaId),
true, true,
@ -100,28 +103,37 @@ object Manga {
fetchedManga.genre, fetchedManga.genre,
MangaStatus.valueOf(fetchedManga.status).name, MangaStatus.valueOf(fetchedManga.status).name,
false, false,
getSource(mangaEntry[MangaTable.sourceReference]) getSource(mangaEntry[MangaTable.sourceReference]),
true
) )
} }
} }
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> { suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val saveDir = applicationDirs.thumbnailsRoot val saveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString() val fileName = mangaId.toString()
return getCachedImageResponse(saveDir, fileName) { return getCachedImageResponse(saveDir, fileName) {
getManga(mangaId) // make sure is initialized
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val sourceId = mangaEntry[MangaTable.sourceReference] val sourceId = mangaEntry[MangaTable.sourceReference]
val source = getHttpSource(sourceId) val source = getHttpSource(sourceId)
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) { val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]!!
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
}
source.client.newCall( source.client.newCall(
GET(thumbnailUrl, source.headers) GET(thumbnailUrl, source.headers)
).await() ).await()
} }
} }
suspend fun clearMangaThumbnail(mangaId: Int) {
val saveDir = applicationDirs.thumbnailsRoot
val fileName = mangaId.toString()
clearCachedImage(saveDir, fileName)
}
} }

View File

@ -64,4 +64,11 @@ object CachedImageResponse {
throw Exception("request error! ${response.code}") throw Exception("request error! ${response.code}")
} }
} }
suspend fun clearCachedImage(saveDir: String, fileName: String) {
val cachedFile = findFileNameStartingWith(saveDir, fileName)
cachedFile?.also {
File(it).delete()
}
}
} }

View File

@ -7,7 +7,9 @@ package ir.armor.tachidesk.model.database.table
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ir.armor.tachidesk.model.dataclass.ChapterDataClass
import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.ResultRow
object ChapterTable : IntIdTable() { object ChapterTable : IntIdTable() {
val url = varchar("url", 2048) val url = varchar("url", 2048)
@ -25,3 +27,17 @@ object ChapterTable : IntIdTable() {
val manga = reference("manga", MangaTable) val manga = reference("manga", MangaTable)
} }
fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
ChapterDataClass(
chapterEntry[ChapterTable.url],
chapterEntry[ChapterTable.name],
chapterEntry[ChapterTable.date_upload],
chapterEntry[ChapterTable.chapter_number],
chapterEntry[ChapterTable.scanlator],
chapterEntry[ChapterTable.manga].value,
chapterEntry[ChapterTable.isRead],
chapterEntry[ChapterTable.isBookmarked],
chapterEntry[ChapterTable.lastPageRead],
chapterEntry[ChapterTable.chapterIndex],
)

View File

@ -25,7 +25,9 @@ data class MangaDataClass(
val genre: String? = null, val genre: String? = null,
val status: String = MangaStatus.UNKNOWN.name, val status: String = MangaStatus.UNKNOWN.name,
val inLibrary: Boolean = false, val inLibrary: Boolean = false,
val source: SourceDataClass? = null val source: SourceDataClass? = null,
val freshData: Boolean = false
) )
data class PagedMangaListDataClass( data class PagedMangaListDataClass(

View File

@ -191,9 +191,11 @@ object JavalinSetup {
// get manga info // get manga info
app.get("/api/v1/manga/:mangaId/") { ctx -> app.get("/api/v1/manga/:mangaId/") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
ctx.json( ctx.json(
future { future {
getManga(mangaId) getManga(mangaId, onlineFetch)
} }
) )
} }
@ -254,7 +256,10 @@ object JavalinSetup {
// get chapter list when showing a manga // get chapter list when showing a manga
app.get("/api/v1/manga/:mangaId/chapters") { ctx -> app.get("/api/v1/manga/:mangaId/chapters") { ctx ->
val mangaId = ctx.pathParam("mangaId").toInt() val mangaId = ctx.pathParam("mangaId").toInt()
ctx.json(future { getChapterList(mangaId) })
val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean()
ctx.json(future { getChapterList(mangaId, onlineFetch) })
} }
// used to display a chapter, get a chapter in order to show it's pages // used to display a chapter, get a chapter in order to show it's pages

View File

@ -50,13 +50,14 @@ const useStyles = makeStyles((theme) => ({
interface IProps{ interface IProps{
chapter: IChapter chapter: IChapter
triggerChaptersUpdate: () => void
} }
export default function ChapterCard(props: IProps) { export default function ChapterCard(props: IProps) {
const classes = useStyles(); const classes = useStyles();
const history = useHistory(); const history = useHistory();
const theme = useTheme(); const theme = useTheme();
const { chapter } = props; const { chapter, triggerChaptersUpdate } = props;
const dateStr = chapter.uploadDate && new Date(chapter.uploadDate).toISOString().slice(0, 10); const dateStr = chapter.uploadDate && new Date(chapter.uploadDate).toISOString().slice(0, 10);
@ -71,12 +72,12 @@ export default function ChapterCard(props: IProps) {
}; };
const sendChange = (key: string, value: any) => { const sendChange = (key: string, value: any) => {
console.log(`${key} -> ${value}`);
handleClose(); handleClose();
const formData = new FormData(); const formData = new FormData();
formData.append(key, value); formData.append(key, value);
client.patch(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`, formData); client.patch(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`, formData)
.then(() => triggerChaptersUpdate());
}; };
return ( return (

View File

@ -198,7 +198,7 @@ export default function MangaDetails(props: IProps) {
<div className={classes.top}> <div className={classes.top}>
<div className={classes.leftRight}> <div className={classes.leftRight}>
<div className={classes.leftSide}> <div className={classes.leftSide}>
<img src={serverAddress + manga.thumbnailUrl} alt="Manga Thumbnail" /> <img src={`${serverAddress}${manga.thumbnailUrl}?x=${Math.random()}`} alt="Manga Thumbnail" />
</div> </div>
<div className={classes.rightSide}> <div className={classes.rightSide}>
<h1> <h1>

View File

@ -43,9 +43,9 @@ const useStyles = makeStyles((theme: Theme) => ({
}, },
})); }));
const InnerItem = React.memo(({ chapters, index }: any) => ( // const InnerItem = React.memo(({ chapters, index }: any) => (
<ChapterCard chapter={chapters[index]} /> // <ChapterCard chapter={chapters[index]} />
)); // ));
export default function Manga() { export default function Manga() {
const classes = useStyles(); const classes = useStyles();
@ -58,23 +58,37 @@ export default function Manga() {
const [manga, setManga] = useState<IManga>(); const [manga, setManga] = useState<IManga>();
const [chapters, setChapters] = useState<IChapter[]>([]); const [chapters, setChapters] = useState<IChapter[]>([]);
const [chapterUpdateTriggerer, setChapterUpdateTriggerer] = useState(0);
function triggerChaptersUpdate() {
setChapterUpdateTriggerer(chapterUpdateTriggerer + 1);
}
useEffect(() => { useEffect(() => {
client.get(`/api/v1/manga/${id}/`) if (manga === undefined || !manga.freshData) {
.then((response) => response.data) client.get(`/api/v1/manga/${id}/?onlineFetch=${manga !== undefined}`)
.then((data: IManga) => { .then((response) => response.data)
setManga(data); .then((data: IManga) => {
setTitle(data.title); setManga(data);
}); setTitle(data.title);
}, []); });
}
}, [manga]);
useEffect(() => { useEffect(() => {
client.get(`/api/v1/manga/${id}/chapters`) const shouldFetchOnline = chapters.length > 0 && chapterUpdateTriggerer === 0;
client.get(`/api/v1/manga/${id}/chapters?onlineFetch=${shouldFetchOnline}`)
.then((response) => response.data) .then((response) => response.data)
.then((data) => setChapters(data)); .then((data) => setChapters(data));
}, []); }, [chapters.length, chapterUpdateTriggerer]);
const itemContent = (index:any) => <InnerItem chapters={chapters} index={index} />; // const itemContent = (index:any) => <InnerItem chapters={chapters} index={index} />;
const itemContent = (index:any) => (
<ChapterCard
chapter={chapters[index]}
triggerChaptersUpdate={triggerChaptersUpdate}
/>
);
return ( return (
<div className={classes.root}> <div className={classes.root}>

View File

@ -50,6 +50,8 @@ interface IManga {
inLibrary: boolean inLibrary: boolean
source: ISource source: ISource
freshData: boolean
} }
interface IChapter { interface IChapter {