Comick: some improvements (#16611)
* Comick: some improvements * comick: thumbnail preference * comick: alt titles * use update date
This commit is contained in:
parent
5b5aa0af94
commit
b7c39c7a67
|
@ -6,7 +6,7 @@ ext {
|
||||||
extName = 'Comick'
|
extName = 'Comick'
|
||||||
pkgNameSuffix = 'all.comickfun'
|
pkgNameSuffix = 'all.comickfun'
|
||||||
extClass = '.ComickFunFactory'
|
extClass = '.ComickFunFactory'
|
||||||
extVersionCode = 25
|
extVersionCode = 26
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.comickfun
|
package eu.kanade.tachiyomi.extension.all.comickfun
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
@ -16,11 +21,15 @@ import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.text.ParseException
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
abstract class ComickFun(override val lang: String, private val comickFunLang: String) : HttpSource() {
|
abstract class ComickFun(
|
||||||
|
override val lang: String,
|
||||||
|
private val comickFunLang: String,
|
||||||
|
) : HttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
override val name = "Comick"
|
override val name = "Comick"
|
||||||
|
|
||||||
|
@ -28,8 +37,6 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
|
||||||
|
|
||||||
private val apiUrl = "https://api.comick.fun"
|
private val apiUrl = "https://api.comick.fun"
|
||||||
|
|
||||||
private val cdnUrl = "https://meo3.comick.pictures"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
private val json = Json {
|
private val json = Json {
|
||||||
|
@ -44,7 +51,14 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
|
||||||
add("User-Agent", "Tachiyomi ${System.getProperty("http.agent")}")
|
add("User-Agent", "Tachiyomi ${System.getProperty("http.agent")}")
|
||||||
}
|
}
|
||||||
|
|
||||||
override val client: OkHttpClient = network.client.newBuilder().rateLimit(4, 1).build()
|
override val client: OkHttpClient = network.client.newBuilder()
|
||||||
|
.addNetworkInterceptor(::thumbnailIntercept)
|
||||||
|
.rateLimit(4, 1)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
/** Popular Manga **/
|
/** Popular Manga **/
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
@ -79,7 +93,8 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
|
||||||
}
|
}
|
||||||
|
|
||||||
val slugOrHid = query.substringAfter(SLUG_SEARCH_PREFIX)
|
val slugOrHid = query.substringAfter(SLUG_SEARCH_PREFIX)
|
||||||
return fetchMangaDetails(SManga.create().apply { this.url = "/comic/$slugOrHid#" }).map {
|
val manga = SManga.create().apply { this.url = "/comic/$slugOrHid#" }
|
||||||
|
return fetchMangaDetails(manga).map {
|
||||||
MangasPage(listOf(it), false)
|
MangasPage(listOf(it), false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,18 +185,9 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
val isQueryPresent = response.request.url.queryParameterNames.contains("q")
|
val isQueryPresent = response.request.url.queryParameterNames.contains("q")
|
||||||
val result = json.decodeFromString<List<Manga>>(response.body.string())
|
val result = response.parseAs<List<SearchManga>>()
|
||||||
return MangasPage(
|
return MangasPage(
|
||||||
result.map { data ->
|
result.map { it.toSManga(useScaledCover) },
|
||||||
SManga.create().apply {
|
|
||||||
// appennding # at end as part of migration from slug to hid
|
|
||||||
url = "/comic/${data.hid}#"
|
|
||||||
title = data.title
|
|
||||||
thumbnail_url = runCatching {
|
|
||||||
"$cdnUrl/${data.md_covers.first().b2key}"
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/*
|
/*
|
||||||
api always returns `limit` amount of results
|
api always returns `limit` amount of results
|
||||||
for text search and page>=2 is always empty
|
for text search and page>=2 is always empty
|
||||||
|
@ -204,20 +210,8 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
val mangaData = json.decodeFromString<MangaDetails>(response.body.string())
|
val mangaData = response.parseAs<Manga>()
|
||||||
return SManga.create().apply {
|
return mangaData.toSManga(useScaledCover)
|
||||||
// appennding # at end as part of migration from slug to hid
|
|
||||||
url = "/comic/${mangaData.comic.hid}#"
|
|
||||||
title = mangaData.comic.title
|
|
||||||
artist = mangaData.artists.joinToString { it.name.trim() }
|
|
||||||
author = mangaData.authors.joinToString { it.name.trim() }
|
|
||||||
description = beautifyDescription(mangaData.comic.desc)
|
|
||||||
genre = mangaData.genres.joinToString { it.name.trim() }
|
|
||||||
status = parseStatus(mangaData.comic.status)
|
|
||||||
thumbnail_url = runCatching {
|
|
||||||
"$cdnUrl/${mangaData.comic.md_covers.first().b2key}"
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga): String {
|
override fun getMangaUrl(manga: SManga): String {
|
||||||
|
@ -247,7 +241,7 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val chapterListResponse = json.decodeFromString<ChapterList>(response.body.string())
|
val chapterListResponse = response.parseAs<ChapterList>()
|
||||||
|
|
||||||
val mangaUrl = response.request.url.toString()
|
val mangaUrl = response.request.url.toString()
|
||||||
.substringBefore("/chapters")
|
.substringBefore("/chapters")
|
||||||
|
@ -259,7 +253,7 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
|
||||||
while (chapterListResponse.total > resultSize) {
|
while (chapterListResponse.total > resultSize) {
|
||||||
val newRequest = paginatedChapterListRequest(mangaUrl, page)
|
val newRequest = paginatedChapterListRequest(mangaUrl, page)
|
||||||
val newResponse = client.newCall(newRequest).execute()
|
val newResponse = client.newCall(newRequest).execute()
|
||||||
val newChapterListResponse = json.decodeFromString<ChapterList>(newResponse.body.string())
|
val newChapterListResponse = newResponse.parseAs<ChapterList>()
|
||||||
|
|
||||||
chapterListResponse.chapters += newChapterListResponse.chapters
|
chapterListResponse.chapters += newChapterListResponse.chapters
|
||||||
|
|
||||||
|
@ -267,24 +261,7 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
|
||||||
page += 1
|
page += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return chapterListResponse.chapters.map { chapter ->
|
return chapterListResponse.chapters.map { it.toSChapter(mangaUrl) }
|
||||||
SChapter.create().apply {
|
|
||||||
url = "$mangaUrl/${chapter.hid}-chapter-${chapter.chap}-$comickFunLang"
|
|
||||||
name = beautifyChapterName(chapter.vol, chapter.chap, chapter.title)
|
|
||||||
date_upload = chapter.created_at.let {
|
|
||||||
try {
|
|
||||||
dateFormat.parse(it)?.time ?: 0L
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scanlator = chapter.group_name.joinToString().takeUnless { it.isBlank() } ?: "Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val dateFormat by lazy {
|
|
||||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter): String {
|
override fun getChapterUrl(chapter: SChapter): String {
|
||||||
|
@ -298,14 +275,14 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val result = json.decodeFromString<PageList>(response.body.string())
|
val result = response.parseAs<PageList>()
|
||||||
return result.chapter.images.mapIndexedNotNull { index, data ->
|
return result.chapter.images.mapIndexedNotNull { index, data ->
|
||||||
if (data.url == null) null else Page(index = index, imageUrl = data.url)
|
if (data.url == null) null else Page(index = index, imageUrl = data.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
private inline fun <reified T> Response.parseAs(): T {
|
||||||
const val SLUG_SEARCH_PREFIX = "id:"
|
return json.decodeFromString(body.string())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
override fun imageUrlParse(response: Response): String {
|
||||||
|
@ -318,4 +295,30 @@ abstract class ComickFun(override val lang: String, private val comickFunLang: S
|
||||||
override fun getFilterList() = FilterList(
|
override fun getFilterList() = FilterList(
|
||||||
getFilters(),
|
getFilters(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = coverQualityPref
|
||||||
|
title = "Cover Quality"
|
||||||
|
entries = arrayOf("Original", "Scaled")
|
||||||
|
entryValues = arrayOf("orig", "scaled")
|
||||||
|
setDefaultValue("orig")
|
||||||
|
summary = "%s"
|
||||||
|
}.let { screen.addPreference(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val useScaledCover: Boolean by lazy {
|
||||||
|
preferences.getString(coverQualityPref, "orig") != "orig"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SLUG_SEARCH_PREFIX = "id:"
|
||||||
|
val dateFormat by lazy {
|
||||||
|
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)
|
||||||
|
}
|
||||||
|
val markdownLinksRegex = "\\[([^]]+)\\]\\(([^)]+)\\)".toRegex()
|
||||||
|
val markdownItalicBoldRegex = "\\*+\\s*([^\\*]*)\\s*\\*+".toRegex()
|
||||||
|
val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex()
|
||||||
|
private const val coverQualityPref = "pref_cover_quality"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,64 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.comickfun
|
package eu.kanade.tachiyomi.extension.all.comickfun
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Manga(
|
data class SearchManga(
|
||||||
val hid: String,
|
val hid: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
val md_covers: List<MDcovers>,
|
val md_covers: List<MDcovers>,
|
||||||
|
val cover_url: String? = null,
|
||||||
|
|
||||||
|
) {
|
||||||
|
fun toSManga(useScaledCover: Boolean) = SManga.create().apply {
|
||||||
|
// appennding # at end as part of migration from slug to hid
|
||||||
|
url = "/comic/$hid#"
|
||||||
|
title = this@SearchManga.title
|
||||||
|
thumbnail_url = parseCover(cover_url, md_covers, useScaledCover)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Manga(
|
||||||
|
val comic: Comic,
|
||||||
|
val artists: List<Artist> = emptyList(),
|
||||||
|
val authors: List<Author> = emptyList(),
|
||||||
|
val genres: List<Genre> = emptyList(),
|
||||||
|
) {
|
||||||
|
fun toSManga(useScaledCover: Boolean) = SManga.create().apply {
|
||||||
|
// appennding # at end as part of migration from slug to hid
|
||||||
|
url = "/comic/${comic.hid}#"
|
||||||
|
title = comic.title
|
||||||
|
description = comic.desc.beautifyDescription()
|
||||||
|
if (comic.altTitles.isNotEmpty()) {
|
||||||
|
description += comic.altTitles.joinToString(
|
||||||
|
separator = "\n",
|
||||||
|
prefix = "\n\nAlternative Titles:\n",
|
||||||
|
) {
|
||||||
|
it.title.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status = comic.status.parseStatus(comic.translation_completed)
|
||||||
|
thumbnail_url = parseCover(comic.cover_url, comic.md_covers, useScaledCover)
|
||||||
|
artist = artists.joinToString { it.name.trim() }
|
||||||
|
author = authors.joinToString { it.name.trim() }
|
||||||
|
genre = genres.joinToString { it.name.trim() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Comic(
|
||||||
|
val hid: String,
|
||||||
|
val title: String,
|
||||||
|
@SerialName("md_titles") val altTitles: List<MDtitles>,
|
||||||
|
val desc: String = "N/A",
|
||||||
|
val status: Int = 0,
|
||||||
|
val translation_completed: Boolean = true,
|
||||||
|
val md_covers: List<MDcovers>,
|
||||||
|
val cover_url: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -15,20 +67,8 @@ data class MDcovers(
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MangaDetails(
|
data class MDtitles(
|
||||||
val comic: Comic,
|
val title: String?,
|
||||||
val artists: Array<Artist>,
|
|
||||||
val authors: Array<Author>,
|
|
||||||
val genres: Array<Genre>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Comic(
|
|
||||||
val hid: String,
|
|
||||||
val title: String,
|
|
||||||
val desc: String = "N/A",
|
|
||||||
val status: Int,
|
|
||||||
val md_covers: List<MDcovers>,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -58,12 +98,20 @@ data class ChapterList(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Chapter(
|
data class Chapter(
|
||||||
val hid: String,
|
val hid: String,
|
||||||
|
val lang: String,
|
||||||
val title: String = "",
|
val title: String = "",
|
||||||
val created_at: String = "",
|
val updated_at: String = "",
|
||||||
val chap: String = "",
|
val chap: String = "",
|
||||||
val vol: String = "",
|
val vol: String = "",
|
||||||
val group_name: Array<String> = arrayOf(""),
|
val group_name: List<String> = emptyList(),
|
||||||
)
|
) {
|
||||||
|
fun toSChapter(mangaUrl: String) = SChapter.create().apply {
|
||||||
|
url = "$mangaUrl/$hid-chapter-$chap-$lang"
|
||||||
|
name = beautifyChapterName(vol, chap, title)
|
||||||
|
date_upload = updated_at.parseDate()
|
||||||
|
scanlator = group_name.joinToString().takeUnless { it.isBlank() } ?: "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PageList(
|
data class PageList(
|
||||||
|
@ -72,7 +120,7 @@ data class PageList(
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ChapterPageData(
|
data class ChapterPageData(
|
||||||
val images: Array<Page>,
|
val images: List<Page>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
@ -1,27 +1,68 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.comickfun
|
package eu.kanade.tachiyomi.extension.all.comickfun
|
||||||
|
|
||||||
import android.os.Build
|
import eu.kanade.tachiyomi.extension.all.comickfun.ComickFun.Companion.dateFormat
|
||||||
import android.text.Html
|
import eu.kanade.tachiyomi.extension.all.comickfun.ComickFun.Companion.markdownItalicBoldRegex
|
||||||
|
import eu.kanade.tachiyomi.extension.all.comickfun.ComickFun.Companion.markdownItalicRegex
|
||||||
|
import eu.kanade.tachiyomi.extension.all.comickfun.ComickFun.Companion.markdownLinksRegex
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import org.jsoup.Jsoup
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.parser.Parser
|
||||||
|
|
||||||
internal fun beautifyDescription(description: String): String {
|
internal fun String.beautifyDescription(): String {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
return Parser.unescapeEntities(this, false)
|
||||||
return Html.fromHtml(description, Html.FROM_HTML_MODE_LEGACY).toString()
|
.replace(markdownLinksRegex, "")
|
||||||
}
|
.replace(markdownItalicBoldRegex, "")
|
||||||
return Jsoup.parse(description).text()
|
.replace(markdownItalicRegex, "")
|
||||||
|
.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun parseStatus(status: Int): Int {
|
internal fun Int.parseStatus(translationComplete: Boolean): Int {
|
||||||
return when (status) {
|
return when (this) {
|
||||||
1 -> SManga.ONGOING
|
1 -> SManga.ONGOING
|
||||||
2 -> SManga.COMPLETED
|
2 -> {
|
||||||
|
if (translationComplete) {
|
||||||
|
SManga.COMPLETED
|
||||||
|
} else {
|
||||||
|
SManga.PUBLISHING_FINISHED
|
||||||
|
}
|
||||||
|
}
|
||||||
3 -> SManga.CANCELLED
|
3 -> SManga.CANCELLED
|
||||||
4 -> SManga.ON_HIATUS
|
4 -> SManga.ON_HIATUS
|
||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun parseCover(thumbnailUrl: String?, mdCovers: List<MDcovers>, useScaled: Boolean): String? {
|
||||||
|
val b2key = runCatching { mdCovers.first().b2key }
|
||||||
|
.getOrNull() ?: ""
|
||||||
|
|
||||||
|
return if (useScaled) {
|
||||||
|
"$thumbnailUrl#$b2key"
|
||||||
|
} else {
|
||||||
|
thumbnailUrl?.replaceAfterLast("/", b2key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun thumbnailIntercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val frag = request.url.fragment
|
||||||
|
if (frag.isNullOrEmpty()) return chain.proceed(request)
|
||||||
|
val response = chain.proceed(request)
|
||||||
|
if (!response.isSuccessful && response.code == 404) {
|
||||||
|
response.close()
|
||||||
|
val url = request.url.toString()
|
||||||
|
.replaceAfterLast("/", frag)
|
||||||
|
|
||||||
|
return chain.proceed(
|
||||||
|
request.newBuilder()
|
||||||
|
.url(url)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
internal fun beautifyChapterName(vol: String, chap: String, title: String): String {
|
internal fun beautifyChapterName(vol: String, chap: String, title: String): String {
|
||||||
return buildString {
|
return buildString {
|
||||||
if (vol.isNotEmpty()) {
|
if (vol.isNotEmpty()) {
|
||||||
|
@ -35,3 +76,8 @@ internal fun beautifyChapterName(vol: String, chap: String, title: String): Stri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun String.parseDate(): Long {
|
||||||
|
return runCatching { dateFormat.parse(this)?.time }
|
||||||
|
.getOrNull() ?: 0L
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue