MangaMutiny - Migration from Gson to kotlinx.serialization (#7401)
* Initial serialization with kotlinx.serialization draft * Serialization without Serializable
This commit is contained in:
parent
897a5d94ba
commit
8dce249839
|
@ -1,11 +1,12 @@
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
extName = 'Manga Mutiny'
|
extName = 'Manga Mutiny'
|
||||||
pkgNameSuffix = "en.mangamutiny"
|
pkgNameSuffix = "en.mangamutiny"
|
||||||
extClass = '.MangaMutiny'
|
extClass = '.MangaMutiny'
|
||||||
extVersionCode = 7
|
extVersionCode = 8
|
||||||
libVersion = '1.2'
|
libVersion = '1.2'
|
||||||
containsNsfw = true
|
containsNsfw = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangamutiny
|
package eu.kanade.tachiyomi.extension.en.mangamutiny
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
@ -13,28 +10,12 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.text.SimpleDateFormat
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Locale
|
|
||||||
import java.util.TimeZone
|
|
||||||
|
|
||||||
fun JsonObject.getNullable(key: String): JsonElement? {
|
|
||||||
val value: JsonElement = this.get(key) ?: return null
|
|
||||||
|
|
||||||
if (value.isJsonNull) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Float.toStringWithoutDotZero(): String = when (this % 1) {
|
|
||||||
0F -> this.toInt().toString()
|
|
||||||
else -> this.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
class MangaMutiny : HttpSource() {
|
class MangaMutiny : HttpSource() {
|
||||||
|
|
||||||
|
@ -45,7 +26,7 @@ class MangaMutiny : HttpSource() {
|
||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
private val parser = JsonParser()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val baseUrlAPI = "https://api.mangamutiny.org"
|
private val baseUrlAPI = "https://api.mangamutiny.org"
|
||||||
|
|
||||||
|
@ -78,64 +59,15 @@ class MangaMutiny : HttpSource() {
|
||||||
mangaDetailsRequestCommon(manga, false)
|
mangaDetailsRequestCommon(manga, false)
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val chapterList = mutableListOf<SChapter>()
|
|
||||||
val responseBody = response.body
|
val responseBody = response.body
|
||||||
|
|
||||||
if (responseBody != null) {
|
return responseBody?.use {
|
||||||
val jsonChapters = JsonParser().parse(responseBody.charStream()).asJsonObject
|
json.decodeFromString(ListChapterDS, it.string()).also {
|
||||||
.get("chapters").asJsonArray
|
responseBody.close()
|
||||||
for (singleChapterJsonElement in jsonChapters) {
|
|
||||||
val singleChapterJsonObject = singleChapterJsonElement.asJsonObject
|
|
||||||
|
|
||||||
chapterList.add(
|
|
||||||
SChapter.create().apply {
|
|
||||||
name = chapterTitleBuilder(singleChapterJsonObject)
|
|
||||||
url = singleChapterJsonObject.get("slug").asString
|
|
||||||
date_upload = parseDate(singleChapterJsonObject.get("releasedAt").asString)
|
|
||||||
|
|
||||||
chapterNumberBuilder(singleChapterJsonObject)?.let { chapterNumber ->
|
|
||||||
chapter_number = chapterNumber
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
} ?: listOf()
|
||||||
responseBody.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapterList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun chapterNumberBuilder(rootNode: JsonObject): Float? =
|
|
||||||
rootNode.getNullable("chapter")?.asFloat
|
|
||||||
|
|
||||||
private fun chapterTitleBuilder(rootNode: JsonObject): String {
|
|
||||||
val volume = rootNode.getNullable("volume")?.asInt
|
|
||||||
|
|
||||||
val chapter = rootNode.getNullable("chapter")?.asFloat?.toStringWithoutDotZero()
|
|
||||||
|
|
||||||
val textTitle = rootNode.getNullable("title")?.asString
|
|
||||||
|
|
||||||
val chapterTitle = StringBuilder()
|
|
||||||
if (volume != null) chapterTitle.append("Vol. $volume")
|
|
||||||
if (chapter != null) {
|
|
||||||
if (volume != null) chapterTitle.append(" ")
|
|
||||||
chapterTitle.append("Chapter $chapter")
|
|
||||||
}
|
|
||||||
if (textTitle != null && textTitle != "") {
|
|
||||||
if (volume != null || chapter != null) chapterTitle.append(": ")
|
|
||||||
chapterTitle.append(textTitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
return chapterTitle.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
|
|
||||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
|
||||||
|
|
||||||
private fun parseDate(dateAsString: String): Long =
|
|
||||||
dateFormatter.parse(dateAsString)?.time ?: 0
|
|
||||||
|
|
||||||
// latest
|
// latest
|
||||||
override fun latestUpdatesRequest(page: Int): Request =
|
override fun latestUpdatesRequest(page: Int): Request =
|
||||||
mangaRequest(page, filters = FilterList(SortFilter().apply { this.state = 1 }))
|
mangaRequest(page, filters = FilterList(SortFilter().apply { this.state = 1 }))
|
||||||
|
@ -162,32 +94,15 @@ class MangaMutiny : HttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
val manga = SManga.create()
|
|
||||||
val responseBody = response.body
|
val responseBody = response.body
|
||||||
|
|
||||||
if (responseBody != null) {
|
if (responseBody != null) {
|
||||||
val rootNode = parser.parse(responseBody.charStream()).asJsonObject
|
return responseBody.use {
|
||||||
manga.apply {
|
json.decodeFromString(SMangaDS, it.string())
|
||||||
status = when (rootNode.get("status").asString) {
|
|
||||||
"ongoing" -> SManga.ONGOING
|
|
||||||
"completed" -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
description = rootNode.getNullable("summary")?.asString
|
|
||||||
thumbnail_url = rootNode.getNullable("thumbnail")?.asString
|
|
||||||
title = rootNode.get("title").asString
|
|
||||||
url = rootNode.get("slug").asString
|
|
||||||
artist = rootNode.getNullable("artists")?.asString
|
|
||||||
author = rootNode.get("authors").asString
|
|
||||||
|
|
||||||
genre = rootNode.get("tags").asJsonArray
|
|
||||||
.joinToString { singleGenre -> singleGenre.asString }
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
responseBody.close()
|
throw IllegalStateException("Response code ${response.code}")
|
||||||
}
|
}
|
||||||
|
|
||||||
return manga
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
@ -199,31 +114,11 @@ class MangaMutiny : HttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val pageList = ArrayList<Page>()
|
|
||||||
|
|
||||||
val responseBody = response.body
|
val responseBody = response.body
|
||||||
|
|
||||||
if (responseBody != null) {
|
return responseBody?.use {
|
||||||
val rootNode = parser.parse(responseBody.charStream()).asJsonObject
|
json.decodeFromString(ListPageDS, it.string())
|
||||||
|
} ?: listOf()
|
||||||
// Build chapter url for every image of this chapter
|
|
||||||
val storageLocation = rootNode.get("storage").asString
|
|
||||||
val manga = rootNode.get("manga").asString
|
|
||||||
val chapterId = rootNode.get("id").asString
|
|
||||||
|
|
||||||
val chapterUrl = "$storageLocation/$manga/$chapterId/"
|
|
||||||
|
|
||||||
// Process every image of this chapter
|
|
||||||
val images = rootNode.get("images").asJsonArray
|
|
||||||
|
|
||||||
for (i in 0 until images.size()) {
|
|
||||||
pageList.add(Page(i, "", chapterUrl + images[i].asString))
|
|
||||||
}
|
|
||||||
|
|
||||||
responseBody.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
|
@ -234,46 +129,20 @@ class MangaMutiny : HttpSource() {
|
||||||
|
|
||||||
// commonly used functions
|
// commonly used functions
|
||||||
private fun mangaParse(response: Response): MangasPage {
|
private fun mangaParse(response: Response): MangasPage {
|
||||||
val mangasPage = ArrayList<SManga>()
|
|
||||||
val responseBody = response.body
|
val responseBody = response.body
|
||||||
|
|
||||||
var totalObjects = 0
|
return if (responseBody != null) {
|
||||||
|
val deserializationResult = json.decodeFromString(PageInfoDS, responseBody.string())
|
||||||
|
val totalObjects = deserializationResult.second
|
||||||
|
val skipped = response.request.url.queryParameter("skip")?.toInt() ?: 0
|
||||||
|
val moreElementsToSkip = skipped + fetchAmount < totalObjects
|
||||||
|
val pageSizeEqualsFetchAmount = deserializationResult.first.size == fetchAmount
|
||||||
|
val hasMorePages = pageSizeEqualsFetchAmount && moreElementsToSkip
|
||||||
|
|
||||||
if (responseBody != null) {
|
MangasPage(deserializationResult.first, hasMorePages)
|
||||||
val rootNode = parser.parse(responseBody.charStream())
|
} else {
|
||||||
|
MangasPage(listOf(), false)
|
||||||
if (rootNode.isJsonObject) {
|
|
||||||
val rootObject = rootNode.asJsonObject
|
|
||||||
val itemsArray = rootObject.get("items").asJsonArray
|
|
||||||
|
|
||||||
for (singleItem in itemsArray) {
|
|
||||||
val mangaObject = singleItem.asJsonObject
|
|
||||||
mangasPage.add(
|
|
||||||
SManga.create().apply {
|
|
||||||
this.title = mangaObject.get("title").asString
|
|
||||||
this.thumbnail_url = mangaObject.getNullable("thumbnail")?.asString
|
|
||||||
this.url = mangaObject.get("slug").asString
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// total number of manga the server found in its database
|
|
||||||
// and is returning paginated page by page:
|
|
||||||
totalObjects = rootObject.getNullable("total")?.asInt ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
responseBody.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val skipped = response.request.url.queryParameter("skip")?.toInt() ?: 0
|
|
||||||
|
|
||||||
val moreElementsToSkip = skipped + fetchAmount < totalObjects
|
|
||||||
|
|
||||||
val pageSizeEqualsFetchAmount = mangasPage.size == fetchAmount
|
|
||||||
|
|
||||||
val hasMorePages = pageSizeEqualsFetchAmount && moreElementsToSkip
|
|
||||||
|
|
||||||
return MangasPage(mangasPage, hasMorePages)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.mangamutiny
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import kotlinx.serialization.DeserializationStrategy
|
||||||
|
import kotlinx.serialization.builtins.MapSerializer
|
||||||
|
import kotlinx.serialization.builtins.serializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonNull
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.float
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
private fun JsonElement?.primitiveContent(): String? {
|
||||||
|
if (this is JsonNull) return null
|
||||||
|
return this?.jsonPrimitive?.content
|
||||||
|
}
|
||||||
|
private fun JsonElement?.primitiveInt(): Int? {
|
||||||
|
if (this is JsonNull) return null
|
||||||
|
return this?.jsonPrimitive?.int
|
||||||
|
}
|
||||||
|
private fun JsonElement?.primitiveFloat(): Float? {
|
||||||
|
if (this is JsonNull) return null
|
||||||
|
return this?.jsonPrimitive?.float
|
||||||
|
}
|
||||||
|
|
||||||
|
private val jsonObjectToMapSerializer = MapSerializer(String.serializer(), JsonElement.serializer())
|
||||||
|
|
||||||
|
object PageInfoDS : DeserializationStrategy<Pair<List<SManga>, Int>> {
|
||||||
|
override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): Pair<List<SManga>, Int> {
|
||||||
|
require(decoder is JsonDecoder)
|
||||||
|
val json = decoder.json
|
||||||
|
val jsonElement = decoder.decodeJsonElement()
|
||||||
|
require(jsonElement is JsonObject)
|
||||||
|
val items = (jsonElement["items"] as JsonArray).map { json.decodeFromJsonElement(SMangaDS, it) }
|
||||||
|
val total = jsonElement["total"]?.jsonPrimitive?.int
|
||||||
|
|
||||||
|
require(total != null)
|
||||||
|
return Pair(items, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object SMangaDS : DeserializationStrategy<SManga> {
|
||||||
|
override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): SManga {
|
||||||
|
require(decoder is JsonDecoder)
|
||||||
|
val jsonElement = decoder.decodeJsonElement()
|
||||||
|
require(jsonElement is JsonObject)
|
||||||
|
val title = jsonElement["title"].primitiveContent()
|
||||||
|
val slug = jsonElement["slug"].primitiveContent()
|
||||||
|
val thumbnail = jsonElement["thumbnail"].primitiveContent()
|
||||||
|
|
||||||
|
val status: Int = jsonElement["status"].primitiveContent()?.let {
|
||||||
|
when (it) {
|
||||||
|
"ongoing" -> SManga.ONGOING
|
||||||
|
"completed" -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
} ?: SManga.UNKNOWN
|
||||||
|
|
||||||
|
val summary: String? = jsonElement["summary"].primitiveContent()
|
||||||
|
val artists: String? = jsonElement["artists"].primitiveContent()
|
||||||
|
val authors: String? = jsonElement["authors"].primitiveContent()
|
||||||
|
val tags: String? =
|
||||||
|
jsonElement["tags"]?.jsonArray?.mapNotNull { it.primitiveContent() }?.joinToString()
|
||||||
|
|
||||||
|
require(title != null && slug != null)
|
||||||
|
return SManga.create().apply {
|
||||||
|
this.title = title
|
||||||
|
this.url = slug
|
||||||
|
this.thumbnail_url = thumbnail
|
||||||
|
|
||||||
|
this.status = status
|
||||||
|
this.description = summary
|
||||||
|
this.artist = artists
|
||||||
|
this.author = authors
|
||||||
|
this.genre = tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ListChapterDS : DeserializationStrategy<List<SChapter>> {
|
||||||
|
override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): List<SChapter> {
|
||||||
|
require(decoder is JsonDecoder)
|
||||||
|
val json = decoder.json
|
||||||
|
val jsonElement = decoder.decodeJsonElement()
|
||||||
|
require(jsonElement is JsonObject)
|
||||||
|
|
||||||
|
val jsonElementChapters = jsonElement["chapters"]?.jsonArray
|
||||||
|
require(jsonElementChapters != null)
|
||||||
|
|
||||||
|
return jsonElementChapters.map { chapter ->
|
||||||
|
json.decodeFromJsonElement(SChapterDS, chapter)
|
||||||
|
}.apply {
|
||||||
|
if (this.size == 1) this.first().chapter_number = 1F
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object SChapterDS : DeserializationStrategy<SChapter> {
|
||||||
|
|
||||||
|
override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): SChapter {
|
||||||
|
require(decoder is JsonDecoder)
|
||||||
|
val jsonElement = decoder.decodeJsonElement()
|
||||||
|
require(jsonElement is JsonObject)
|
||||||
|
val volume: Int? = jsonElement["volume"].primitiveInt()
|
||||||
|
val chapter: Float? = jsonElement["chapter"].primitiveFloat()
|
||||||
|
val title: String? = jsonElement["title"].primitiveContent()
|
||||||
|
val slug: String? = jsonElement["slug"].primitiveContent()
|
||||||
|
val releasedAt: String? = jsonElement["releasedAt"].primitiveContent()
|
||||||
|
|
||||||
|
require(slug != null && releasedAt != null)
|
||||||
|
return SChapter.create().apply {
|
||||||
|
if (chapter != null) this.chapter_number = chapter
|
||||||
|
this.name = chapterTitleBuilder(volume, title, chapter)
|
||||||
|
this.url = slug
|
||||||
|
this.date_upload = dateFormatter.parse(releasedAt)?.time ?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
|
||||||
|
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this Float into a String, removing any trailing .0
|
||||||
|
*/
|
||||||
|
private fun Float.toStringWithoutDotZero(): String = when (this % 1) {
|
||||||
|
0F -> this.toInt().toString()
|
||||||
|
else -> this.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chapterTitleBuilder(volume: Int?, title: String?, chapter: Float?): String {
|
||||||
|
val chapterTitle = StringBuilder()
|
||||||
|
if (volume != null) {
|
||||||
|
chapterTitle.append("Vol. $volume ")
|
||||||
|
}
|
||||||
|
if (chapter != null) {
|
||||||
|
chapterTitle.append("Chapter ${chapter.toStringWithoutDotZero()}")
|
||||||
|
}
|
||||||
|
if (title != null && title != "") {
|
||||||
|
if (chapterTitle.isNotEmpty()) chapterTitle.append(": ")
|
||||||
|
chapterTitle.append(title)
|
||||||
|
}
|
||||||
|
return chapterTitle.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ListPageDS : DeserializationStrategy<List<Page>> {
|
||||||
|
override val descriptor: SerialDescriptor = jsonObjectToMapSerializer.descriptor
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): List<Page> {
|
||||||
|
require(decoder is JsonDecoder)
|
||||||
|
val jsonElement = decoder.decodeJsonElement()
|
||||||
|
require(jsonElement is JsonObject)
|
||||||
|
|
||||||
|
val storage: String? = jsonElement["storage"].primitiveContent()
|
||||||
|
val manga: String? = jsonElement["manga"].primitiveContent()
|
||||||
|
val id: String? = jsonElement["id"].primitiveContent()
|
||||||
|
val images: List<String>? =
|
||||||
|
jsonElement["images"]?.jsonArray?.mapNotNull { it.primitiveContent() }
|
||||||
|
|
||||||
|
require(storage != null && manga != null && id != null && images != null)
|
||||||
|
val chapterUrl = "$storage/$manga/$id/"
|
||||||
|
return images.mapIndexed { index, pageSuffix -> Page(index, "", chapterUrl + pageSuffix) }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue