Add support to i18n in MangaDex (#11901)
* Add support to i18n in MangaDex. * Update the filters and lint some stuff. * Reword 'tags' into 'tags mode' in the filter
This commit is contained in:
parent
1a8556a280
commit
cb9a898787
|
@ -6,7 +6,7 @@ ext {
|
|||
extName = 'MangaDex'
|
||||
pkgNameSuffix = 'all.mangadex'
|
||||
extClass = '.MangaDexFactory'
|
||||
extVersionCode = 158
|
||||
extVersionCode = 159
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,8 @@ object MDConstants {
|
|||
return "${coverQualityPref}_$dexLang"
|
||||
}
|
||||
|
||||
fun getCoverQualityPreferenceEntries() = arrayOf("Original", "Medium", "Low")
|
||||
fun getCoverQualityPreferenceEntries(intl: MangaDexIntl) =
|
||||
arrayOf(intl.coverQualityOriginal, intl.coverQualityMedium, intl.coverQualityLow)
|
||||
|
||||
fun getCoverQualityPreferenceEntryValues() = arrayOf("", ".512.jpg", ".256.jpg")
|
||||
|
||||
|
@ -76,10 +77,11 @@ object MDConstants {
|
|||
}
|
||||
|
||||
private const val originalLanguagePref = "originalLanguage"
|
||||
const val originalLanguagePrefValJapanese = "ja"
|
||||
const val originalLanguagePrefValChinese = "zh"
|
||||
const val originalLanguagePrefValJapanese = MangaDexIntl.JAPANESE
|
||||
const val originalLanguagePrefValChinese = MangaDexIntl.CHINESE
|
||||
const val originalLanguagePrefValChineseHk = "zh-hk"
|
||||
const val originalLanguagePrefValKorean = "ko"
|
||||
const val originalLanguagePrefValKorean = MangaDexIntl.KOREAN
|
||||
val originalLanguagePrefDefaults = emptySet<String>()
|
||||
|
||||
fun getOriginalLanguagePrefKey(dexLang: String): String {
|
||||
return "${originalLanguagePref}_$dexLang"
|
||||
|
@ -100,4 +102,10 @@ object MDConstants {
|
|||
fun getBlockedUploaderPrefKey(dexLang: String): String {
|
||||
return "${blockedUploaderPref}_$dexLang"
|
||||
}
|
||||
|
||||
const val tagGroupContent = "content"
|
||||
const val tagGroupFormat = "format"
|
||||
const val tagGroupGenre = "genre"
|
||||
const val tagGroupTheme = "theme"
|
||||
val tagGroupsOrder = arrayOf(tagGroupContent, tagGroupFormat, tagGroupGenre, tagGroupTheme)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterListDto
|
|||
import eu.kanade.tachiyomi.extension.all.mangadex.dto.ListDto
|
||||
import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaDto
|
||||
import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaListDto
|
||||
import eu.kanade.tachiyomi.extension.all.mangadex.dto.RelationshipDto
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
|
@ -28,6 +29,7 @@ import kotlinx.serialization.SerializationException
|
|||
import kotlinx.serialization.decodeFromString
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Request
|
||||
|
@ -37,10 +39,11 @@ import uy.kohesive.injekt.Injekt
|
|||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
abstract class MangaDex(override val lang: String, val dexLang: String) :
|
||||
abstract class MangaDex(final override val lang: String, private val dexLang: String) :
|
||||
ConfigurableSource,
|
||||
HttpSource() {
|
||||
override val name = "MangaDex"
|
||||
|
||||
override val name = MangaDexIntl.MANGADEX_NAME
|
||||
override val baseUrl = "https://mangadex.org"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
@ -49,9 +52,9 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val helper = MangaDexHelper()
|
||||
private val helper = MangaDexHelper(lang)
|
||||
|
||||
override fun headersBuilder() = Headers.Builder()
|
||||
final override fun headersBuilder() = Headers.Builder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
.add("User-Agent", "Tachiyomi " + System.getProperty("http.agent"))
|
||||
|
||||
|
@ -70,37 +73,44 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
addQueryParameter("limit", MDConstants.mangaLimit.toString())
|
||||
addQueryParameter("offset", helper.getMangaListOffset(page))
|
||||
addQueryParameter("includes[]", MDConstants.coverArt)
|
||||
preferences.getStringSet(
|
||||
MDConstants.getContentRatingPrefKey(dexLang),
|
||||
MDConstants.contentRatingPrefDefaults
|
||||
)?.forEach { addQueryParameter("contentRating[]", it) }
|
||||
preferences.getStringSet(
|
||||
|
||||
addQueryParameter(
|
||||
"contentRating[]",
|
||||
preferences.getStringSet(
|
||||
MDConstants.getContentRatingPrefKey(dexLang),
|
||||
MDConstants.contentRatingPrefDefaults
|
||||
)
|
||||
)
|
||||
|
||||
val originalLanguages = preferences.getStringSet(
|
||||
MDConstants.getOriginalLanguagePrefKey(dexLang),
|
||||
setOf()
|
||||
)?.forEach {
|
||||
addQueryParameter("originalLanguage[]", it)
|
||||
// dex has zh and zh-hk for chinese manhua
|
||||
if (it == MDConstants.originalLanguagePrefValChinese) {
|
||||
addQueryParameter("originalLanguage[]", MDConstants.originalLanguagePrefValChineseHk)
|
||||
}
|
||||
MDConstants.originalLanguagePrefDefaults
|
||||
)
|
||||
|
||||
addQueryParameter("originalLanguage[]", originalLanguages)
|
||||
|
||||
// Dex has zh and zh-hk for Chinese manhua
|
||||
if (MDConstants.originalLanguagePrefValChinese in originalLanguages!!) {
|
||||
addQueryParameter(
|
||||
"originalLanguage[]",
|
||||
MDConstants.originalLanguagePrefValChineseHk
|
||||
)
|
||||
}
|
||||
}.build().toUrl().toString()
|
||||
}
|
||||
|
||||
return GET(
|
||||
url = url,
|
||||
url = url.build().toString(),
|
||||
headers = headers,
|
||||
cache = CacheControl.FORCE_NETWORK
|
||||
)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
if (response.isSuccessful.not()) {
|
||||
throw Exception("HTTP ${response.code}")
|
||||
}
|
||||
|
||||
if (response.code == 204) {
|
||||
return MangasPage(emptyList(), false)
|
||||
}
|
||||
val mangaListDto = helper.json.decodeFromString<MangaListDto>(response.body!!.string())
|
||||
|
||||
val mangaListDto = response.parseAs<MangaListDto>()
|
||||
val hasMoreResults = mangaListDto.limit + mangaListDto.offset < mangaListDto.total
|
||||
|
||||
val coverSuffix = preferences.getString(MDConstants.getCoverQualityPreferenceKey(dexLang), "")
|
||||
|
@ -117,28 +127,34 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
|
||||
// LATEST section API can't sort by date yet so not implemented
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val chapterListDto = helper.json.decodeFromString<ChapterListDto>(response.body!!.string())
|
||||
val chapterListDto = response.parseAs<ChapterListDto>()
|
||||
val hasMoreResults = chapterListDto.limit + chapterListDto.offset < chapterListDto.total
|
||||
|
||||
val mangaIds = chapterListDto.data.map { it.relationships }.flatten()
|
||||
.filter { it.type == MDConstants.manga }.map { it.id }.distinct()
|
||||
val mangaIds = chapterListDto.data
|
||||
.flatMap { it.relationships }
|
||||
.filter { it.type == MDConstants.manga }
|
||||
.map { it.id }
|
||||
.distinct()
|
||||
.toSet()
|
||||
|
||||
val mangaUrl = MDConstants.apiMangaUrl.toHttpUrlOrNull()!!.newBuilder().apply {
|
||||
addQueryParameter("includes[]", MDConstants.coverArt)
|
||||
addQueryParameter("limit", mangaIds.size.toString())
|
||||
|
||||
preferences.getStringSet(
|
||||
MDConstants.getContentRatingPrefKey(dexLang),
|
||||
MDConstants.contentRatingPrefDefaults
|
||||
)?.forEach { addQueryParameter("contentRating[]", it) }
|
||||
addQueryParameter(
|
||||
"contentRating[]",
|
||||
preferences.getStringSet(
|
||||
MDConstants.getContentRatingPrefKey(dexLang),
|
||||
MDConstants.contentRatingPrefDefaults
|
||||
)
|
||||
)
|
||||
|
||||
mangaIds.forEach { id ->
|
||||
addQueryParameter("ids[]", id)
|
||||
}
|
||||
}.build().toString()
|
||||
addQueryParameter("ids[]", mangaIds)
|
||||
}
|
||||
|
||||
val mangaResponse = client.newCall(GET(mangaUrl, headers, CacheControl.FORCE_NETWORK)).execute()
|
||||
val mangaListDto = helper.json.decodeFromString<MangaListDto>(mangaResponse.body!!.string())
|
||||
val mangaRequest = GET(mangaUrl.build().toString(), headers, CacheControl.FORCE_NETWORK)
|
||||
val mangaResponse = client.newCall(mangaRequest).execute()
|
||||
val mangaListDto = mangaResponse.parseAs<MangaListDto>()
|
||||
|
||||
val mangaDtoMap = mangaListDto.data.associateBy({ it.id }, { it })
|
||||
|
||||
|
@ -154,6 +170,7 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
|
||||
return MangasPage(mangaList, hasMoreResults)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = MDConstants.apiChapterUrl.toHttpUrlOrNull()!!.newBuilder().apply {
|
||||
addQueryParameter("offset", helper.getLatestChapterOffset(page))
|
||||
|
@ -161,32 +178,52 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
addQueryParameter("translatedLanguage[]", dexLang)
|
||||
addQueryParameter("order[publishAt]", "desc")
|
||||
addQueryParameter("includeFutureUpdates", "0")
|
||||
preferences.getStringSet(
|
||||
|
||||
val originalLanguages = preferences.getStringSet(
|
||||
MDConstants.getOriginalLanguagePrefKey(dexLang),
|
||||
setOf()
|
||||
)?.forEach {
|
||||
addQueryParameter("originalLanguage[]", it)
|
||||
// dex has zh and zh-hk for chinese manhua
|
||||
if (it == MDConstants.originalLanguagePrefValChinese) {
|
||||
addQueryParameter("originalLanguage[]", MDConstants.originalLanguagePrefValChineseHk)
|
||||
}
|
||||
MDConstants.originalLanguagePrefDefaults
|
||||
)
|
||||
|
||||
addQueryParameter("originalLanguage[]", originalLanguages)
|
||||
|
||||
// Dex has zh and zh-hk for Chinese manhua
|
||||
if (MDConstants.originalLanguagePrefValChinese in originalLanguages!!) {
|
||||
addQueryParameter(
|
||||
"originalLanguage[]",
|
||||
MDConstants.originalLanguagePrefValChineseHk
|
||||
)
|
||||
}
|
||||
preferences.getStringSet(
|
||||
MDConstants.getContentRatingPrefKey(dexLang),
|
||||
MDConstants.contentRatingPrefDefaults
|
||||
)?.forEach { addQueryParameter("contentRating[]", it) }
|
||||
MDConstants.defaultBlockedGroups.forEach {
|
||||
addQueryParameter("excludedGroups[]", it)
|
||||
}
|
||||
preferences.getString(
|
||||
MDConstants.getBlockedGroupsPrefKey(dexLang), ""
|
||||
)?.split(",")?.sorted()?.forEach { if (it.isNotEmpty()) addQueryParameter("excludedGroups[]", it.trim()) }
|
||||
preferences.getString(
|
||||
MDConstants.getBlockedUploaderPrefKey(dexLang),
|
||||
""
|
||||
)?.split(", ")?.sorted()?.forEach { if (it.isNotEmpty()) addQueryParameter("excludedUploaders[]", it.trim()) }
|
||||
}.build().toString()
|
||||
return GET(url, headers, CacheControl.FORCE_NETWORK)
|
||||
|
||||
addQueryParameter(
|
||||
"contentRating[]",
|
||||
preferences.getStringSet(
|
||||
MDConstants.getContentRatingPrefKey(dexLang),
|
||||
MDConstants.contentRatingPrefDefaults
|
||||
)
|
||||
)
|
||||
|
||||
val excludedGroups = MDConstants.defaultBlockedGroups +
|
||||
preferences.getString(MDConstants.getBlockedGroupsPrefKey(dexLang), "")
|
||||
?.split(",")
|
||||
?.map(String::trim)
|
||||
?.filter(String::isNotEmpty)
|
||||
?.sorted()
|
||||
.orEmpty()
|
||||
|
||||
addQueryParameter("excludedGroups[]", excludedGroups)
|
||||
|
||||
val excludedUploaders = preferences
|
||||
.getString(MDConstants.getBlockedUploaderPrefKey(dexLang), "")
|
||||
?.split(",")
|
||||
?.map(String::trim)
|
||||
?.filter(String::isNotEmpty)
|
||||
?.sorted()
|
||||
?.toSet()
|
||||
|
||||
addQueryParameter("excludedUploaders[]", excludedUploaders)
|
||||
}
|
||||
|
||||
return GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
// SEARCH section
|
||||
|
@ -197,14 +234,17 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
return getMangaIdFromChapterId(query.removePrefix(MDConstants.prefixChSearch)).flatMap { manga_id ->
|
||||
super.fetchSearchManga(page, MDConstants.prefixIdSearch + manga_id, filters)
|
||||
}
|
||||
|
||||
query.startsWith(MDConstants.prefixUsrSearch) ->
|
||||
return client.newCall(searchMangaUploaderRequest(page, query.removePrefix(MDConstants.prefixUsrSearch)))
|
||||
.asObservableSuccess()
|
||||
.map { latestUpdatesParse(it) }
|
||||
|
||||
query.startsWith(MDConstants.prefixListSearch) ->
|
||||
return client.newCall(GET(MDConstants.apiListUrl + "/" + query.removePrefix(MDConstants.prefixListSearch), headers, CacheControl.FORCE_NETWORK))
|
||||
.asObservableSuccess()
|
||||
.map { searchMangaListRequest(it, page) }
|
||||
|
||||
else ->
|
||||
return super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
@ -215,20 +255,18 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
if (response.isSuccessful.not()) {
|
||||
throw Exception("Unable to process Chapter request. HTTP code: ${response.code}")
|
||||
throw Exception(helper.intl.unableToProcessChapterRequest(response.code))
|
||||
}
|
||||
|
||||
helper.json.decodeFromString<ChapterDto>(response.body!!.string()).data.relationships
|
||||
.find {
|
||||
it.type == MDConstants.manga
|
||||
}!!.id
|
||||
response.parseAs<ChapterDto>().data.relationships
|
||||
.find { it.type == MDConstants.manga }!!.id
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val tempUrl = MDConstants.apiMangaUrl.toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("limit", MDConstants.mangaLimit.toString())
|
||||
addQueryParameter("offset", (helper.getMangaListOffset(page)))
|
||||
addQueryParameter("offset", helper.getMangaListOffset(page))
|
||||
addQueryParameter("includes[]", MDConstants.coverArt)
|
||||
}
|
||||
|
||||
|
@ -241,24 +279,26 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
addQueryParameter("contentRating[]", "suggestive")
|
||||
addQueryParameter("contentRating[]", "erotica")
|
||||
addQueryParameter("contentRating[]", "pornographic")
|
||||
}.build().toString()
|
||||
}
|
||||
|
||||
return GET(url, headers, CacheControl.FORCE_NETWORK)
|
||||
return GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
query.startsWith(MDConstants.prefixGrpSearch) -> {
|
||||
val groupID = query.removePrefix(MDConstants.prefixGrpSearch)
|
||||
if (!helper.containsUuid(groupID)) {
|
||||
throw Exception("Not a valid group ID")
|
||||
throw Exception(helper.intl.invalidGroupId)
|
||||
}
|
||||
|
||||
tempUrl.apply {
|
||||
addQueryParameter("group", groupID)
|
||||
}
|
||||
}
|
||||
|
||||
query.startsWith(MDConstants.prefixAuthSearch) -> {
|
||||
val authorID = query.removePrefix(MDConstants.prefixAuthSearch)
|
||||
if (!helper.containsUuid(authorID)) {
|
||||
throw Exception("Not a valid author ID")
|
||||
throw Exception(helper.intl.invalidAuthorId)
|
||||
}
|
||||
|
||||
tempUrl.apply {
|
||||
|
@ -266,6 +306,7 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
addQueryParameter("artists[]", authorID)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
tempUrl.apply {
|
||||
val actualQuery = query.replace(MDConstants.whitespaceRegex, " ")
|
||||
|
@ -284,16 +325,14 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
|
||||
|
||||
private fun searchMangaListRequest(response: Response, page: Int): MangasPage {
|
||||
if (response.isSuccessful.not()) {
|
||||
throw Exception("HTTP ${response.code}")
|
||||
}
|
||||
|
||||
val listDto = helper.json.decodeFromString<ListDto>(response.body!!.string())
|
||||
val listDto = response.parseAs<ListDto>()
|
||||
val listDtoFiltered = listDto.data.relationships.filter { it.type != "Manga" }
|
||||
val amount = listDtoFiltered.count()
|
||||
|
||||
if (amount < 1) {
|
||||
throw Exception("No Manga in List")
|
||||
throw Exception(helper.intl.noSeriesInList)
|
||||
}
|
||||
|
||||
val minIndex = (page - 1) * MDConstants.mangaLimit
|
||||
|
||||
val url = MDConstants.apiMangaUrl.toHttpUrl().newBuilder().apply {
|
||||
|
@ -301,23 +340,29 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
addQueryParameter("offset", "0")
|
||||
addQueryParameter("includes[]", MDConstants.coverArt)
|
||||
}
|
||||
listDtoFiltered.forEachIndexed() { index, relationshipDto ->
|
||||
if (index >= minIndex && index < (minIndex + MDConstants.mangaLimit)) {
|
||||
url.addQueryParameter("ids[]", relationshipDto.id)
|
||||
}
|
||||
}
|
||||
|
||||
val request = client.newCall(GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK))
|
||||
val mangaList = searchMangaListParse(request.execute())
|
||||
return MangasPage(mangaList, amount.toFloat() / MDConstants.mangaLimit - (page.toFloat() - 1) > 1)
|
||||
val ids = listDtoFiltered
|
||||
.filterIndexed { i, _ -> i >= minIndex && i < (minIndex + MDConstants.mangaLimit) }
|
||||
.map(RelationshipDto::id)
|
||||
.toSet()
|
||||
|
||||
url.addQueryParameter("ids[]", ids)
|
||||
|
||||
val mangaRequest = GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK)
|
||||
val mangaResponse = client.newCall(mangaRequest).execute()
|
||||
val mangaList = searchMangaListParse(mangaResponse)
|
||||
|
||||
val hasNextPage = amount.toFloat() / MDConstants.mangaLimit - (page.toFloat() - 1) > 1
|
||||
|
||||
return MangasPage(mangaList, hasNextPage)
|
||||
}
|
||||
|
||||
private fun searchMangaListParse(response: Response): List<SManga> {
|
||||
if (response.isSuccessful.not()) {
|
||||
throw Exception("HTTP ${response.code}")
|
||||
throw Exception("HTTP error ${response.code}")
|
||||
}
|
||||
|
||||
val mangaListDto = helper.json.decodeFromString<MangaListDto>(response.body!!.string())
|
||||
val mangaListDto = response.parseAs<MangaListDto>()
|
||||
|
||||
val coverSuffix = preferences.getString(MDConstants.getCoverQualityPreferenceKey(dexLang), "")
|
||||
|
||||
|
@ -339,32 +384,52 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
addQueryParameter("order[publishAt]", "desc")
|
||||
addQueryParameter("includeFutureUpdates", "0")
|
||||
addQueryParameter("uploader", uploader)
|
||||
preferences.getStringSet(
|
||||
|
||||
val originalLanguages = preferences.getStringSet(
|
||||
MDConstants.getOriginalLanguagePrefKey(dexLang),
|
||||
setOf()
|
||||
)?.forEach {
|
||||
addQueryParameter("originalLanguage[]", it)
|
||||
// dex has zh and zh-hk for chinese manhua
|
||||
if (it == MDConstants.originalLanguagePrefValChinese) {
|
||||
addQueryParameter("originalLanguage[]", MDConstants.originalLanguagePrefValChineseHk)
|
||||
}
|
||||
MDConstants.originalLanguagePrefDefaults
|
||||
)
|
||||
|
||||
addQueryParameter("originalLanguage[]", originalLanguages)
|
||||
|
||||
// Dex has zh and zh-hk for Chinese manhua
|
||||
if (MDConstants.originalLanguagePrefValChinese in originalLanguages!!) {
|
||||
addQueryParameter(
|
||||
"originalLanguage[]",
|
||||
MDConstants.originalLanguagePrefValChineseHk
|
||||
)
|
||||
}
|
||||
preferences.getStringSet(
|
||||
MDConstants.getContentRatingPrefKey(dexLang),
|
||||
MDConstants.contentRatingPrefDefaults
|
||||
)?.forEach { addQueryParameter("contentRating[]", it) }
|
||||
MDConstants.defaultBlockedGroups.forEach {
|
||||
addQueryParameter("excludedGroups[]", it)
|
||||
}
|
||||
preferences.getString(
|
||||
MDConstants.getBlockedGroupsPrefKey(dexLang), ""
|
||||
)?.split(",")?.sorted()?.forEach { if (it.isNotEmpty()) addQueryParameter("excludedGroups[]", it.trim()) }
|
||||
preferences.getString(
|
||||
MDConstants.getBlockedUploaderPrefKey(dexLang),
|
||||
""
|
||||
)?.split(", ")?.sorted()?.forEach { if (it.isNotEmpty()) addQueryParameter("excludedUploaders[]", it.trim()) }
|
||||
}.build().toString()
|
||||
return GET(url, headers, CacheControl.FORCE_NETWORK)
|
||||
|
||||
addQueryParameter(
|
||||
"contentRating[]",
|
||||
preferences.getStringSet(
|
||||
MDConstants.getContentRatingPrefKey(dexLang),
|
||||
MDConstants.contentRatingPrefDefaults
|
||||
)
|
||||
)
|
||||
|
||||
val excludedGroups = MDConstants.defaultBlockedGroups +
|
||||
preferences.getString(MDConstants.getBlockedGroupsPrefKey(dexLang), "")
|
||||
?.split(",")
|
||||
?.map(String::trim)
|
||||
?.filter(String::isNotEmpty)
|
||||
?.sorted()
|
||||
.orEmpty()
|
||||
|
||||
addQueryParameter("excludedGroups[]", excludedGroups)
|
||||
|
||||
val excludedUploaders = preferences
|
||||
.getString(MDConstants.getBlockedUploaderPrefKey(dexLang), "")
|
||||
?.split(",")
|
||||
?.map(String::trim)
|
||||
?.filter(String::isNotEmpty)
|
||||
?.sorted()
|
||||
?.toSet()
|
||||
|
||||
addQueryParameter("excludedUploaders[]", excludedUploaders)
|
||||
}
|
||||
|
||||
return GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
// Manga Details section
|
||||
|
@ -391,20 +456,28 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
*/
|
||||
private fun apiMangaDetailsRequest(manga: SManga): Request {
|
||||
if (!helper.containsUuid(manga.url.trim())) {
|
||||
throw Exception("Migrate this manga from MangaDex to MangaDex to update it")
|
||||
throw Exception(helper.intl.migrateWarning)
|
||||
}
|
||||
|
||||
val url = (MDConstants.apiUrl + manga.url).toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("includes[]", MDConstants.coverArt)
|
||||
addQueryParameter("includes[]", MDConstants.author)
|
||||
addQueryParameter("includes[]", MDConstants.artist)
|
||||
}.build().toString()
|
||||
return GET(url, headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
return GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val manga = helper.json.decodeFromString<MangaDto>(response.body!!.string())
|
||||
val manga = response.parseAs<MangaDto>()
|
||||
val coverSuffix = preferences.getString(MDConstants.getCoverQualityPreferenceKey(dexLang), "")
|
||||
return helper.createManga(manga.data, fetchSimpleChapterList(manga, dexLang), dexLang, coverSuffix)
|
||||
|
||||
return helper.createManga(
|
||||
manga.data,
|
||||
fetchSimpleChapterList(manga, dexLang),
|
||||
dexLang,
|
||||
coverSuffix
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -418,13 +491,18 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
val url = "${MDConstants.apiMangaUrl}/${manga.data.id}/aggregate?translatedLanguage[]=$langCode"
|
||||
val response = client.newCall(GET(url, headers)).execute()
|
||||
val chapters: AggregateDto
|
||||
|
||||
try {
|
||||
chapters = helper.json.decodeFromString(response.body!!.string())
|
||||
chapters = response.parseAs()
|
||||
} catch (e: SerializationException) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
if (chapters.volumes.isNullOrEmpty()) return emptyList()
|
||||
return chapters.volumes.values.flatMap { it.chapters.values }.map { it.chapter }
|
||||
|
||||
return chapters.volumes.values
|
||||
.flatMap { it.chapters.values }
|
||||
.map { it.chapter }
|
||||
}
|
||||
|
||||
// Chapter list section
|
||||
|
@ -433,8 +511,9 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
*/
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
if (!helper.containsUuid(manga.url)) {
|
||||
throw Exception("Migrate this manga from MangaDex to MangaDex to update it")
|
||||
throw Exception(helper.intl.migrateWarning)
|
||||
}
|
||||
|
||||
return actualChapterListRequest(helper.getUUIDFromUrl(manga.url), 0)
|
||||
}
|
||||
|
||||
|
@ -447,31 +526,44 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
addQueryParameter("contentRating[]", "suggestive")
|
||||
addQueryParameter("contentRating[]", "erotica")
|
||||
addQueryParameter("contentRating[]", "pornographic")
|
||||
preferences.getString(
|
||||
MDConstants.getBlockedGroupsPrefKey(dexLang), ""
|
||||
)?.split(",")?.sorted()?.forEach { if (it.isNotEmpty()) addQueryParameter("excludedGroups[]", it.trim()) }
|
||||
preferences.getString(
|
||||
MDConstants.getBlockedUploaderPrefKey(dexLang),
|
||||
""
|
||||
)?.split(",")?.sorted()?.forEach { if (it.isNotEmpty()) addQueryParameter("excludedUploaders[]", it.trim()) }
|
||||
}.build().toString()
|
||||
return GET(url, headers = headers, cache = CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
if (response.isSuccessful.not()) {
|
||||
throw Exception("HTTP ${response.code}")
|
||||
|
||||
val excludedGroups = preferences
|
||||
.getString(MDConstants.getBlockedGroupsPrefKey(dexLang), "")
|
||||
?.split(",")
|
||||
?.map(String::trim)
|
||||
?.filter(String::isNotEmpty)
|
||||
?.sorted()
|
||||
?.toSet()
|
||||
|
||||
addQueryParameter("excludedGroups[]", excludedGroups)
|
||||
|
||||
val excludedUploaders = preferences
|
||||
.getString(MDConstants.getBlockedUploaderPrefKey(dexLang), "")
|
||||
?.split(",")
|
||||
?.map(String::trim)
|
||||
?.filter(String::isNotEmpty)
|
||||
?.sorted()
|
||||
?.toSet()
|
||||
|
||||
addQueryParameter("excludedUploaders[]", excludedUploaders)
|
||||
}
|
||||
|
||||
return GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
if (response.code == 204) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
try {
|
||||
val chapterListResponse = helper.json.decodeFromString<ChapterListDto>(response.body!!.string())
|
||||
val chapterListResponse = response.parseAs<ChapterListDto>()
|
||||
|
||||
val chapterListResults = chapterListResponse.data.toMutableList()
|
||||
|
||||
val mangaId =
|
||||
response.request.url.toString().substringBefore("/feed")
|
||||
.substringAfter("${MDConstants.apiMangaUrl}/")
|
||||
val mangaId = response.request.url.toString()
|
||||
.substringBefore("/feed")
|
||||
.substringAfter("${MDConstants.apiMangaUrl}/")
|
||||
|
||||
val limit = chapterListResponse.limit
|
||||
|
||||
|
@ -479,31 +571,32 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
|
||||
var hasMoreResults = (limit + offset) < chapterListResponse.total
|
||||
|
||||
// max results that can be returned is 500 so need to make more api calls if limit+offset > total chapters
|
||||
// Max results that can be returned is 500 so need to make more API
|
||||
// calls if limit + offset > total chapters
|
||||
while (hasMoreResults) {
|
||||
offset += limit
|
||||
val newResponse =
|
||||
client.newCall(actualChapterListRequest(mangaId, offset)).execute()
|
||||
val newChapterList = helper.json.decodeFromString<ChapterListDto>(newResponse.body!!.string())
|
||||
val newRequest = actualChapterListRequest(mangaId, offset)
|
||||
val newResponse = client.newCall(newRequest).execute()
|
||||
val newChapterList = newResponse.parseAs<ChapterListDto>()
|
||||
chapterListResults.addAll(newChapterList.data)
|
||||
hasMoreResults = (limit + offset) < newChapterList.total
|
||||
}
|
||||
|
||||
val now = Date().time
|
||||
|
||||
return chapterListResults.mapNotNull { helper.createChapter(it) }
|
||||
.filter {
|
||||
it.date_upload <= now
|
||||
}
|
||||
return chapterListResults
|
||||
.mapNotNull { helper.createChapter(it) }
|
||||
.filter { it.date_upload <= now }
|
||||
} catch (e: Exception) {
|
||||
Log.e("MangaDex", "error parsing chapter list", e)
|
||||
throw(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
if (!helper.containsUuid(chapter.url)) {
|
||||
throw Exception("Migrate this manga from MangaDex to MangaDex to update it")
|
||||
throw Exception(helper.intl.migrateWarning)
|
||||
}
|
||||
|
||||
val chapterId = chapter.url.substringAfter("/chapter/")
|
||||
val usingStandardHTTPS =
|
||||
preferences.getBoolean(MDConstants.getStandardHttpsPreferenceKey(dexLang), false)
|
||||
|
@ -518,7 +611,7 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val atHomeRequestUrl = response.request.url
|
||||
val atHomeDto = helper.json.decodeFromString<AtHomeDto>(response.body!!.string())
|
||||
val atHomeDto = response.parseAs<AtHomeDto>()
|
||||
val host = atHomeDto.baseUrl
|
||||
val usingDataSaver =
|
||||
preferences.getBoolean(MDConstants.getDataSaverPreferenceKey(dexLang), false)
|
||||
|
@ -548,8 +641,8 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val coverQualityPref = ListPreference(screen.context).apply {
|
||||
key = MDConstants.getCoverQualityPreferenceKey(dexLang)
|
||||
title = "Manga Cover Quality"
|
||||
entries = MDConstants.getCoverQualityPreferenceEntries()
|
||||
title = helper.intl.coverQuality
|
||||
entries = MDConstants.getCoverQualityPreferenceEntries(helper.intl)
|
||||
entryValues = MDConstants.getCoverQualityPreferenceEntryValues()
|
||||
setDefaultValue(MDConstants.getCoverQualityPreferenceDefaultValue())
|
||||
summary = "%s"
|
||||
|
@ -558,18 +651,22 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(MDConstants.getCoverQualityPreferenceKey(dexLang), entry).commit()
|
||||
|
||||
preferences.edit()
|
||||
.putString(MDConstants.getCoverQualityPreferenceKey(dexLang), entry)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
val dataSaverPref = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = MDConstants.getDataSaverPreferenceKey(dexLang)
|
||||
title = "Data saver"
|
||||
summary = "Enables smaller, more compressed images"
|
||||
title = helper.intl.dataSaver
|
||||
summary = helper.intl.dataSaverSummary
|
||||
setDefaultValue(false)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val checkValue = newValue as Boolean
|
||||
|
||||
preferences.edit()
|
||||
.putBoolean(MDConstants.getDataSaverPreferenceKey(dexLang), checkValue)
|
||||
.commit()
|
||||
|
@ -578,13 +675,13 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
|
||||
val standardHttpsPortPref = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = MDConstants.getStandardHttpsPreferenceKey(dexLang)
|
||||
title = "Use HTTPS port 443 only"
|
||||
summary =
|
||||
"Enable to only request image servers that use port 443. This allows users with stricter firewall restrictions to access MangaDex images"
|
||||
title = helper.intl.standardHttpsPort
|
||||
summary = helper.intl.standardHttpsPortSummary
|
||||
setDefaultValue(false)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val checkValue = newValue as Boolean
|
||||
|
||||
preferences.edit()
|
||||
.putBoolean(MDConstants.getStandardHttpsPreferenceKey(dexLang), checkValue)
|
||||
.commit()
|
||||
|
@ -593,9 +690,14 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
|
||||
val contentRatingPref = MultiSelectListPreference(screen.context).apply {
|
||||
key = MDConstants.getContentRatingPrefKey(dexLang)
|
||||
title = "Default content rating"
|
||||
summary = "Show content with the selected ratings by default"
|
||||
entries = arrayOf("Safe", "Suggestive", "Erotica", "Pornographic")
|
||||
title = helper.intl.standardContentRating
|
||||
summary = helper.intl.standardContentRatingSummary
|
||||
entries = arrayOf(
|
||||
helper.intl.contentRatingSafe,
|
||||
helper.intl.contentRatingSuggestive,
|
||||
helper.intl.contentRatingErotica,
|
||||
helper.intl.contentRatingPornographic
|
||||
)
|
||||
entryValues = arrayOf(
|
||||
MDConstants.contentRatingPrefValSafe,
|
||||
MDConstants.contentRatingPrefValSuggestive,
|
||||
|
@ -603,8 +705,10 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
MDConstants.contentRatingPrefValPornographic
|
||||
)
|
||||
setDefaultValue(MDConstants.contentRatingPrefDefaults)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val checkValue = newValue as Set<String>
|
||||
|
||||
preferences.edit()
|
||||
.putStringSet(MDConstants.getContentRatingPrefKey(dexLang), checkValue)
|
||||
.commit()
|
||||
|
@ -613,17 +717,23 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
|
||||
val originalLanguagePref = MultiSelectListPreference(screen.context).apply {
|
||||
key = MDConstants.getOriginalLanguagePrefKey(dexLang)
|
||||
title = "Filter original languages"
|
||||
summary = "Only show content that was originally published in the selected languages in both latest and browse"
|
||||
entries = arrayOf("Japanese", "Chinese", "Korean")
|
||||
title = helper.intl.filterOriginalLanguages
|
||||
summary = helper.intl.filterOriginalLanguagesSummary
|
||||
entries = arrayOf(
|
||||
helper.intl.languageDisplayName(MangaDexIntl.JAPANESE),
|
||||
helper.intl.languageDisplayName(MangaDexIntl.CHINESE),
|
||||
helper.intl.languageDisplayName(MangaDexIntl.KOREAN)
|
||||
)
|
||||
entryValues = arrayOf(
|
||||
MDConstants.originalLanguagePrefValJapanese,
|
||||
MDConstants.originalLanguagePrefValChinese,
|
||||
MDConstants.originalLanguagePrefValKorean
|
||||
)
|
||||
setDefaultValue(setOf<String>())
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val checkValue = newValue as Set<String>
|
||||
|
||||
preferences.edit()
|
||||
.putStringSet(MDConstants.getOriginalLanguagePrefKey(dexLang), checkValue)
|
||||
.commit()
|
||||
|
@ -632,15 +742,15 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
|
||||
val blockedGroupsPref = EditTextPreference(screen.context).apply {
|
||||
key = MDConstants.getBlockedGroupsPrefKey(dexLang)
|
||||
title = "Block Groups by UUID"
|
||||
summary = "Chapters from blocked groups will not show up in Latest or Manga feed.\n" +
|
||||
"Enter as a Comma-separated list of group UUIDs"
|
||||
title = helper.intl.blockGroupByUuid
|
||||
summary = helper.intl.blockGroupByUuidSummary
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val groupsBlocked = newValue.toString()
|
||||
.split(",")
|
||||
.map { it.trim() }
|
||||
.filter { helper.containsUuid(it) }
|
||||
.joinToString(separator = ", ")
|
||||
.joinToString(", ")
|
||||
|
||||
preferences.edit()
|
||||
.putString(MDConstants.getBlockedGroupsPrefKey(dexLang), groupsBlocked)
|
||||
|
@ -650,15 +760,15 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
|
||||
val blockedUploaderPref = EditTextPreference(screen.context).apply {
|
||||
key = MDConstants.getBlockedUploaderPrefKey(dexLang)
|
||||
title = "Block Uploader by UUID"
|
||||
summary = "Chapters from blocked users will not show up in Latest or Manga feed.\n" +
|
||||
"Enter as a Comma-separated list of uploader UUIDs"
|
||||
title = helper.intl.blockUploaderByUuid
|
||||
summary = helper.intl.blockUploaderByUuidSummary
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val uploaderBlocked = newValue.toString()
|
||||
.split(",")
|
||||
.map { it.trim() }
|
||||
.filter { helper.containsUuid(it) }
|
||||
.joinToString(separator = ", ")
|
||||
.joinToString(", ")
|
||||
|
||||
preferences.edit()
|
||||
.putString(MDConstants.getBlockedUploaderPrefKey(dexLang), uploaderBlocked)
|
||||
|
@ -676,5 +786,13 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
|
|||
}
|
||||
|
||||
override fun getFilterList(): FilterList =
|
||||
helper.mdFilters.getMDFilterList(preferences, dexLang)
|
||||
helper.mdFilters.getMDFilterList(preferences, dexLang, helper.intl)
|
||||
|
||||
private fun HttpUrl.Builder.addQueryParameter(name: String, value: Set<String>?): HttpUrl.Builder {
|
||||
return apply { value?.forEach { addQueryParameter(name, it) } }
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T = use {
|
||||
helper.json.decodeFromString(body?.string().orEmpty())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,301 +4,376 @@ import android.content.SharedPreferences
|
|||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import okhttp3.HttpUrl
|
||||
import java.util.Locale
|
||||
|
||||
class MangaDexFilters {
|
||||
|
||||
internal fun getMDFilterList(preferences: SharedPreferences, dexLang: String): FilterList {
|
||||
|
||||
internal fun getMDFilterList(preferences: SharedPreferences, dexLang: String, intl: MangaDexIntl): FilterList {
|
||||
return FilterList(
|
||||
OriginalLanguageList(getOriginalLanguage(preferences, dexLang)),
|
||||
ContentRatingList(getContentRating(preferences, dexLang)),
|
||||
DemographicList(getDemographics()),
|
||||
StatusList(getStatus()),
|
||||
SortFilter(sortableList.map { it.first }.toTypedArray()),
|
||||
TagList(getTags()),
|
||||
TagInclusionMode(),
|
||||
TagExclusionMode(),
|
||||
HasAvailableChaptersFilter(),
|
||||
HasAvailableChaptersFilter(intl),
|
||||
OriginalLanguageList(intl, getOriginalLanguage(preferences, dexLang, intl)),
|
||||
ContentRatingList(intl, getContentRating(preferences, dexLang, intl)),
|
||||
DemographicList(intl, getDemographics(intl)),
|
||||
StatusList(intl, getStatus(intl)),
|
||||
SortFilter(intl, getSortables(intl)),
|
||||
TagsFilter(intl, getTagFilters(intl)),
|
||||
TagList(intl.content, getContents(intl)),
|
||||
TagList(intl.format, getFormats(intl)),
|
||||
TagList(intl.genre, getGenres(intl)),
|
||||
TagList(intl.theme, getThemes(intl)),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getContentRating(preferences: SharedPreferences, dexLang: String): List<ContentRating> {
|
||||
private interface UrlQueryFilter {
|
||||
fun addQueryParameter(url: HttpUrl.Builder, dexLang: String)
|
||||
}
|
||||
|
||||
private fun getContentRating(preferences: SharedPreferences, dexLang: String, intl: MangaDexIntl): List<ContentRating> {
|
||||
val contentRatings = preferences.getStringSet(
|
||||
MDConstants.getContentRatingPrefKey(dexLang),
|
||||
MDConstants.contentRatingPrefDefaults
|
||||
)
|
||||
return listOf(
|
||||
ContentRating("Safe").apply {
|
||||
ContentRating(intl.contentRatingSafe, MDConstants.contentRatingPrefValSafe).apply {
|
||||
state = contentRatings
|
||||
?.contains(MDConstants.contentRatingPrefValSafe) ?: true
|
||||
},
|
||||
ContentRating("Suggestive").apply {
|
||||
ContentRating(intl.contentRatingSuggestive, MDConstants.contentRatingPrefValSuggestive).apply {
|
||||
state = contentRatings
|
||||
?.contains(MDConstants.contentRatingPrefValSuggestive) ?: true
|
||||
},
|
||||
ContentRating("Erotica").apply {
|
||||
ContentRating(intl.contentRatingErotica, MDConstants.contentRatingPrefValErotica).apply {
|
||||
state = contentRatings
|
||||
?.contains(MDConstants.contentRatingPrefValErotica) ?: false
|
||||
},
|
||||
ContentRating("Pornographic").apply {
|
||||
ContentRating(intl.contentRatingPornographic, MDConstants.contentRatingPrefValPornographic).apply {
|
||||
state = contentRatings
|
||||
?.contains(MDConstants.contentRatingPrefValPornographic) ?: false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private class Demographic(name: String) : Filter.CheckBox(name)
|
||||
private class DemographicList(demographics: List<Demographic>) :
|
||||
Filter.Group<Demographic>("Publication Demographic", demographics)
|
||||
private class Demographic(name: String, val value: String) : Filter.CheckBox(name)
|
||||
private class DemographicList(intl: MangaDexIntl, demographics: List<Demographic>) :
|
||||
Filter.Group<Demographic>(intl.publicationDemographic, demographics),
|
||||
UrlQueryFilter {
|
||||
|
||||
private fun getDemographics() = listOf(
|
||||
Demographic("None"),
|
||||
Demographic("Shounen"),
|
||||
Demographic("Shoujo"),
|
||||
Demographic("Seinen"),
|
||||
Demographic("Josei")
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
|
||||
state.forEach { demographic ->
|
||||
if (demographic.state) {
|
||||
url.addQueryParameter("publicationDemographic[]", demographic.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDemographics(intl: MangaDexIntl) = listOf(
|
||||
Demographic(intl.publicationDemographicNone, "none"),
|
||||
Demographic(intl.publicationDemographicShounen, "shounen"),
|
||||
Demographic(intl.publicationDemographicShoujo, "shoujo"),
|
||||
Demographic(intl.publicationDemographicSeinen, "seinen"),
|
||||
Demographic(intl.publicationDemographicJosei, "josei")
|
||||
)
|
||||
|
||||
private class Status(name: String) : Filter.CheckBox(name)
|
||||
private class StatusList(status: List<Status>) :
|
||||
Filter.Group<Status>("Status", status)
|
||||
private class Status(name: String, val value: String) : Filter.CheckBox(name)
|
||||
private class StatusList(intl: MangaDexIntl, status: List<Status>) :
|
||||
Filter.Group<Status>(intl.status, status),
|
||||
UrlQueryFilter {
|
||||
|
||||
private fun getStatus() = listOf(
|
||||
Status("Ongoing"),
|
||||
Status("Completed"),
|
||||
Status("Hiatus"),
|
||||
Status("Cancelled"),
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
|
||||
state.forEach { status ->
|
||||
if (status.state) {
|
||||
url.addQueryParameter("status[]", status.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatus(intl: MangaDexIntl) = listOf(
|
||||
Status(intl.statusOngoing, "ongoing"),
|
||||
Status(intl.statusCompleted, "completed"),
|
||||
Status(intl.statusHiatus, "hiatus"),
|
||||
Status(intl.statusCancelled, "cancelled"),
|
||||
)
|
||||
|
||||
private class ContentRating(name: String) : Filter.CheckBox(name)
|
||||
private class ContentRatingList(contentRating: List<ContentRating>) :
|
||||
Filter.Group<ContentRating>("Content Rating", contentRating)
|
||||
private class ContentRating(name: String, val value: String) : Filter.CheckBox(name)
|
||||
private class ContentRatingList(intl: MangaDexIntl, contentRating: List<ContentRating>) :
|
||||
Filter.Group<ContentRating>(intl.contentRating, contentRating),
|
||||
UrlQueryFilter {
|
||||
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
|
||||
state.forEach { rating ->
|
||||
if (rating.state) {
|
||||
url.addQueryParameter("contentRating[]", rating.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class OriginalLanguage(name: String, val isoCode: String) : Filter.CheckBox(name)
|
||||
private class OriginalLanguageList(originalLanguage: List<OriginalLanguage>) :
|
||||
Filter.Group<OriginalLanguage>("Original language", originalLanguage)
|
||||
private class OriginalLanguageList(intl: MangaDexIntl, originalLanguage: List<OriginalLanguage>) :
|
||||
Filter.Group<OriginalLanguage>(intl.originalLanguage, originalLanguage),
|
||||
UrlQueryFilter {
|
||||
|
||||
private fun getOriginalLanguage(preferences: SharedPreferences, dexLang: String): List<OriginalLanguage> {
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
|
||||
state.forEach { lang ->
|
||||
if (lang.state) {
|
||||
// dex has zh and zh-hk for chinese manhua
|
||||
if (lang.isoCode == MDConstants.originalLanguagePrefValChinese) {
|
||||
url.addQueryParameter(
|
||||
"originalLanguage[]",
|
||||
MDConstants.originalLanguagePrefValChineseHk
|
||||
)
|
||||
}
|
||||
|
||||
url.addQueryParameter("originalLanguage[]", lang.isoCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOriginalLanguage(preferences: SharedPreferences, dexLang: String, intl: MangaDexIntl): List<OriginalLanguage> {
|
||||
val originalLanguages = preferences.getStringSet(
|
||||
MDConstants.getOriginalLanguagePrefKey(dexLang),
|
||||
setOf()
|
||||
)
|
||||
)!!
|
||||
|
||||
return listOf(
|
||||
OriginalLanguage("Japanese (Manga)", "ja").apply {
|
||||
state = originalLanguages
|
||||
?.contains(MDConstants.originalLanguagePrefValJapanese) ?: false
|
||||
},
|
||||
OriginalLanguage("Chinese (Manhua)", "zh").apply {
|
||||
state = originalLanguages
|
||||
?.contains(MDConstants.originalLanguagePrefValChinese) ?: false
|
||||
},
|
||||
OriginalLanguage("Korean (Manhwa)", "ko").apply {
|
||||
state = originalLanguages
|
||||
?.contains(MDConstants.originalLanguagePrefValKorean) ?: false
|
||||
},
|
||||
OriginalLanguage(intl.originalLanguageFilterJapanese, MDConstants.originalLanguagePrefValJapanese)
|
||||
.apply { state = MDConstants.originalLanguagePrefValJapanese in originalLanguages },
|
||||
OriginalLanguage(intl.originalLanguageFilterChinese, MDConstants.originalLanguagePrefValChinese)
|
||||
.apply { state = MDConstants.originalLanguagePrefValChinese in originalLanguages },
|
||||
OriginalLanguage(intl.originalLanguageFilterKorean, MDConstants.originalLanguagePrefValKorean)
|
||||
.apply { state = MDConstants.originalLanguagePrefValKorean in originalLanguages },
|
||||
)
|
||||
}
|
||||
|
||||
internal class Tag(val id: String, name: String) : Filter.TriState(name)
|
||||
private class TagList(tags: List<Tag>) : Filter.Group<Tag>("Tags", tags)
|
||||
|
||||
// to get all tags from dex https://api.mangadex.org/manga/tag
|
||||
internal fun getTags() = listOf(
|
||||
Tag("391b0423-d847-456f-aff0-8b0cfc03066b", "Action"),
|
||||
Tag("f4122d1c-3b44-44d0-9936-ff7502c39ad3", "Adaptation"),
|
||||
Tag("87cc87cd-a395-47af-b27a-93258283bbc6", "Adventure"),
|
||||
Tag("e64f6742-c834-471d-8d72-dd51fc02b835", "Aliens"),
|
||||
Tag("3de8c75d-8ee3-48ff-98ee-e20a65c86451", "Animals"),
|
||||
Tag("51d83883-4103-437c-b4b1-731cb73d786c", "Anthology"),
|
||||
Tag("0a39b5a1-b235-4886-a747-1d05d216532d", "Award Winning"),
|
||||
Tag("5920b825-4181-4a17-beeb-9918b0ff7a30", "Boy's Love"),
|
||||
Tag("4d32cc48-9f00-4cca-9b5a-a839f0764984", "Comedy"),
|
||||
Tag("ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", "Cooking"),
|
||||
Tag("5ca48985-9a9d-4bd8-be29-80dc0303db72", "Crime"),
|
||||
Tag("9ab53f92-3eed-4e9b-903a-917c86035ee3", "Crossdressing"),
|
||||
Tag("da2d50ca-3018-4cc0-ac7a-6b7d472a29ea", "Delinquents"),
|
||||
Tag("39730448-9a5f-48a2-85b0-a70db87b1233", "Demons"),
|
||||
Tag("b13b2a48-c720-44a9-9c77-39c9979373fb", "Doujinshi"),
|
||||
Tag("b9af3a63-f058-46de-a9a0-e0c13906197a", "Drama"),
|
||||
Tag("7b2ce280-79ef-4c09-9b58-12b7c23a9b78", "Fan Colored"),
|
||||
Tag("cdc58593-87dd-415e-bbc0-2ec27bf404cc", "Fantasy"),
|
||||
Tag("b11fda93-8f1d-4bef-b2ed-8803d3733170", "4-Koma"),
|
||||
Tag("f5ba408b-0e7a-484d-8d49-4e9125ac96de", "Full Color"),
|
||||
Tag("2bd2e8d0-f146-434a-9b51-fc9ff2c5fe6a", "Genderswap"),
|
||||
Tag("3bb26d85-09d5-4d2e-880c-c34b974339e9", "Ghosts"),
|
||||
Tag("a3c67850-4684-404e-9b7f-c69850ee5da6", "Girl's Love"),
|
||||
Tag("b29d6a3d-1569-4e7a-8caf-7557bc92cd5d", "Gore"),
|
||||
Tag("fad12b5e-68ba-460e-b933-9ae8318f5b65", "Gyaru"),
|
||||
Tag("aafb99c1-7f60-43fa-b75f-fc9502ce29c7", "Harem"),
|
||||
Tag("33771934-028e-4cb3-8744-691e866a923e", "Historical"),
|
||||
Tag("cdad7e68-1419-41dd-bdce-27753074a640", "Horror"),
|
||||
Tag("5bd0e105-4481-44ca-b6e7-7544da56b1a3", "Incest"),
|
||||
Tag("ace04997-f6bd-436e-b261-779182193d3d", "Isekai"),
|
||||
Tag("2d1f5d56-a1e5-4d0d-a961-2193588b08ec", "Loli"),
|
||||
Tag("3e2b8dae-350e-4ab8-a8ce-016e844b9f0d", "Long Strip"),
|
||||
Tag("85daba54-a71c-4554-8a28-9901a8b0afad", "Mafia"),
|
||||
Tag("a1f53773-c69a-4ce5-8cab-fffcd90b1565", "Magic"),
|
||||
Tag("81c836c9-914a-4eca-981a-560dad663e73", "Magical Girls"),
|
||||
Tag("799c202e-7daa-44eb-9cf7-8a3c0441531e", "Martial Arts"),
|
||||
Tag("50880a9d-5440-4732-9afb-8f457127e836", "Mecha"),
|
||||
Tag("c8cbe35b-1b2b-4a3f-9c37-db84c4514856", "Medical"),
|
||||
Tag("ac72833b-c4e9-4878-b9db-6c8a4a99444a", "Military"),
|
||||
Tag("dd1f77c5-dea9-4e2b-97ae-224af09caf99", "Monster Girls"),
|
||||
Tag("36fd93ea-e8b8-445e-b836-358f02b3d33d", "Monsters"),
|
||||
Tag("f42fbf9e-188a-447b-9fdc-f19dc1e4d685", "Music"),
|
||||
Tag("ee968100-4191-4968-93d3-f82d72be7e46", "Mystery"),
|
||||
Tag("489dd859-9b61-4c37-af75-5b18e88daafc", "Ninja"),
|
||||
Tag("92d6d951-ca5e-429c-ac78-451071cbf064", "Office Workers"),
|
||||
Tag("320831a8-4026-470b-94f6-8353740e6f04", "Official Colored"),
|
||||
Tag("0234a31e-a729-4e28-9d6a-3f87c4966b9e", "Oneshot"),
|
||||
Tag("b1e97889-25b4-4258-b28b-cd7f4d28ea9b", "Philosophical"),
|
||||
Tag("df33b754-73a3-4c54-80e6-1a74a8058539", "Police"),
|
||||
Tag("9467335a-1b83-4497-9231-765337a00b96", "Post-Apocalyptic"),
|
||||
Tag("3b60b75c-a2d7-4860-ab56-05f391bb889c", "Psychological"),
|
||||
Tag("0bc90acb-ccc1-44ca-a34a-b9f3a73259d0", "Reincarnation"),
|
||||
Tag("65761a2a-415e-47f3-bef2-a9dababba7a6", "Reverse Harem"),
|
||||
Tag("423e2eae-a7a2-4a8b-ac03-a8351462d71d", "Romance"),
|
||||
Tag("81183756-1453-4c81-aa9e-f6e1b63be016", "Samurai"),
|
||||
Tag("caaa44eb-cd40-4177-b930-79d3ef2afe87", "School Life"),
|
||||
Tag("256c8bd9-4904-4360-bf4f-508a76d67183", "Sci-Fi"),
|
||||
Tag("97893a4c-12af-4dac-b6be-0dffb353568e", "Sexual Violence"),
|
||||
Tag("ddefd648-5140-4e5f-ba18-4eca4071d19b", "Shota"),
|
||||
Tag("e5301a23-ebd9-49dd-a0cb-2add944c7fe9", "Slice of Life"),
|
||||
Tag("69964a64-2f90-4d33-beeb-f3ed2875eb4c", "Sports"),
|
||||
Tag("7064a261-a137-4d3a-8848-2d385de3a99c", "Superhero"),
|
||||
Tag("eabc5b4c-6aff-42f3-b657-3e90cbd00b75", "Supernatural"),
|
||||
Tag("5fff9cde-849c-4d78-aab0-0d52b2ee1d25", "Survival"),
|
||||
Tag("07251805-a27e-4d59-b488-f0bfbec15168", "Thriller"),
|
||||
Tag("292e862b-2d17-4062-90a2-0356caa4ae27", "Time Travel"),
|
||||
Tag("f8f62932-27da-4fe4-8ee1-6779a8c5edba", "Tragedy"),
|
||||
Tag("31932a7e-5b8e-49a6-9f12-2afa39dc544c", "Traditional Games"),
|
||||
Tag("891cf039-b895-47f0-9229-bef4c96eccd4", "User Created"),
|
||||
Tag("d7d1730f-6eb0-4ba6-9437-602cac38664c", "Vampires"),
|
||||
Tag("9438db5a-7e2a-4ac0-b39e-e0d95a34b8a8", "Video Games"),
|
||||
Tag("d14322ac-4d6f-4e9b-afd9-629d5f4d8a41", "Villainess"),
|
||||
Tag("8c86611e-fab7-4986-9dec-d1a2f44acdd5", "Virtual Reality"),
|
||||
Tag("e197df38-d0e7-43b5-9b09-2842d0c326dd", "Web Comic"),
|
||||
Tag("acc803a4-c95a-4c22-86fc-eb6b582d82a2", "Wuxia"),
|
||||
Tag("631ef465-9aba-4afb-b0fc-ea10efe274a8", "Zombies")
|
||||
)
|
||||
private class TagList(collection: String, tags: List<Tag>) :
|
||||
Filter.Group<Tag>(collection, tags),
|
||||
UrlQueryFilter {
|
||||
|
||||
private class TagInclusionMode :
|
||||
Filter.Select<String>("Included tags mode", arrayOf("And", "Or"), 0)
|
||||
|
||||
private class TagExclusionMode :
|
||||
Filter.Select<String>("Excluded tags mode", arrayOf("And", "Or"), 1)
|
||||
|
||||
val sortableList = listOf(
|
||||
Pair("Alphabetic", "title"),
|
||||
Pair("Chapter uploaded at", "latestUploadedChapter"),
|
||||
Pair("Number of follows", "followedCount"),
|
||||
Pair("Manga created at", "createdAt"),
|
||||
Pair("Manga info updated at", "updatedAt"),
|
||||
Pair("Relevant manga", "relevance"),
|
||||
Pair("Year", "year")
|
||||
)
|
||||
|
||||
class SortFilter(sortables: Array<String>) : Filter.Sort("Sort", sortables, Selection(2, false))
|
||||
|
||||
private class HasAvailableChaptersFilter : Filter.CheckBox("Has available chapters")
|
||||
|
||||
internal fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList, dexLang: String): String {
|
||||
url.apply {
|
||||
// add filters
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is OriginalLanguageList -> {
|
||||
filter.state.forEach { lang ->
|
||||
if (lang.state) {
|
||||
// dex has zh and zh-hk for chinese manhua
|
||||
if (lang.isoCode == MDConstants.originalLanguagePrefValChinese) {
|
||||
addQueryParameter(
|
||||
"originalLanguage[]",
|
||||
MDConstants.originalLanguagePrefValChineseHk
|
||||
)
|
||||
}
|
||||
addQueryParameter(
|
||||
"originalLanguage[]",
|
||||
lang.isoCode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ContentRatingList -> {
|
||||
filter.state.forEach { rating ->
|
||||
if (rating.state) {
|
||||
addQueryParameter(
|
||||
"contentRating[]",
|
||||
rating.name.toLowerCase(Locale.US)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is DemographicList -> {
|
||||
filter.state.forEach { demographic ->
|
||||
if (demographic.state) {
|
||||
addQueryParameter(
|
||||
"publicationDemographic[]",
|
||||
demographic.name.toLowerCase(
|
||||
Locale.US
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is StatusList -> {
|
||||
filter.state.forEach { status ->
|
||||
if (status.state) {
|
||||
addQueryParameter(
|
||||
"status[]",
|
||||
status.name.toLowerCase(
|
||||
Locale.US
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is SortFilter -> {
|
||||
if (filter.state != null) {
|
||||
val query = sortableList[filter.state!!.index].second
|
||||
val value = when (filter.state!!.ascending) {
|
||||
true -> "asc"
|
||||
false -> "desc"
|
||||
}
|
||||
addQueryParameter("order[$query]", value)
|
||||
}
|
||||
}
|
||||
is TagList -> {
|
||||
filter.state.forEach { tag ->
|
||||
if (tag.isIncluded()) {
|
||||
addQueryParameter("includedTags[]", tag.id)
|
||||
} else if (tag.isExcluded()) {
|
||||
addQueryParameter("excludedTags[]", tag.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
is TagInclusionMode -> {
|
||||
addQueryParameter(
|
||||
"includedTagsMode",
|
||||
filter.values[filter.state].toUpperCase(Locale.US)
|
||||
)
|
||||
}
|
||||
is TagExclusionMode -> {
|
||||
addQueryParameter(
|
||||
"excludedTagsMode",
|
||||
filter.values[filter.state].toUpperCase(Locale.US)
|
||||
)
|
||||
}
|
||||
is HasAvailableChaptersFilter -> {
|
||||
if (filter.state) {
|
||||
addQueryParameter("hasAvailableChapters", "true")
|
||||
addQueryParameter("availableTranslatedLanguage[]", dexLang)
|
||||
}
|
||||
}
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
|
||||
state.forEach { tag ->
|
||||
if (tag.isIncluded()) {
|
||||
url.addQueryParameter("includedTags[]", tag.id)
|
||||
} else if (tag.isExcluded()) {
|
||||
url.addQueryParameter("excludedTags[]", tag.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getContents(intl: MangaDexIntl): List<Tag> {
|
||||
val tags = listOf(
|
||||
Tag("b29d6a3d-1569-4e7a-8caf-7557bc92cd5d", intl.contentGore),
|
||||
Tag("97893a4c-12af-4dac-b6be-0dffb353568e", intl.contentSexualViolence),
|
||||
)
|
||||
|
||||
return tags.sortIfTranslated(intl)
|
||||
}
|
||||
|
||||
internal fun getFormats(intl: MangaDexIntl): List<Tag> {
|
||||
val tags = listOf(
|
||||
Tag("b11fda93-8f1d-4bef-b2ed-8803d3733170", intl.formatFourKoma),
|
||||
Tag("f4122d1c-3b44-44d0-9936-ff7502c39ad3", intl.formatAdaptation),
|
||||
Tag("51d83883-4103-437c-b4b1-731cb73d786c", intl.formatAnthology),
|
||||
Tag("0a39b5a1-b235-4886-a747-1d05d216532d", intl.formatAwardWinning),
|
||||
Tag("b13b2a48-c720-44a9-9c77-39c9979373fb", intl.formatDoujinshi),
|
||||
Tag("7b2ce280-79ef-4c09-9b58-12b7c23a9b78", intl.formatFanColored),
|
||||
Tag("f5ba408b-0e7a-484d-8d49-4e9125ac96de", intl.formatFullColor),
|
||||
Tag("3e2b8dae-350e-4ab8-a8ce-016e844b9f0d", intl.formatLongStrip),
|
||||
Tag("320831a8-4026-470b-94f6-8353740e6f04", intl.formatOfficialColored),
|
||||
Tag("0234a31e-a729-4e28-9d6a-3f87c4966b9e", intl.formatOneshot),
|
||||
Tag("891cf039-b895-47f0-9229-bef4c96eccd4", intl.formatUserCreated),
|
||||
Tag("e197df38-d0e7-43b5-9b09-2842d0c326dd", intl.formatWebComic),
|
||||
)
|
||||
|
||||
return tags.sortIfTranslated(intl)
|
||||
}
|
||||
|
||||
internal fun getGenres(intl: MangaDexIntl): List<Tag> {
|
||||
val tags = listOf(
|
||||
Tag("391b0423-d847-456f-aff0-8b0cfc03066b", intl.genreAction),
|
||||
Tag("87cc87cd-a395-47af-b27a-93258283bbc6", intl.genreAdventure),
|
||||
Tag("5920b825-4181-4a17-beeb-9918b0ff7a30", intl.genreBoysLove),
|
||||
Tag("4d32cc48-9f00-4cca-9b5a-a839f0764984", intl.genreComedy),
|
||||
Tag("5ca48985-9a9d-4bd8-be29-80dc0303db72", intl.genreCrime),
|
||||
Tag("b9af3a63-f058-46de-a9a0-e0c13906197a", intl.genreDrama),
|
||||
Tag("cdc58593-87dd-415e-bbc0-2ec27bf404cc", intl.genreFantasy),
|
||||
Tag("a3c67850-4684-404e-9b7f-c69850ee5da6", intl.genreGirlsLove),
|
||||
Tag("33771934-028e-4cb3-8744-691e866a923e", intl.genreHistorical),
|
||||
Tag("cdad7e68-1419-41dd-bdce-27753074a640", intl.genreHorror),
|
||||
Tag("ace04997-f6bd-436e-b261-779182193d3d", intl.genreIsekai),
|
||||
Tag("81c836c9-914a-4eca-981a-560dad663e73", intl.genreMagicalGirls),
|
||||
Tag("50880a9d-5440-4732-9afb-8f457127e836", intl.genreMecha),
|
||||
Tag("c8cbe35b-1b2b-4a3f-9c37-db84c4514856", intl.genreMedical),
|
||||
Tag("ee968100-4191-4968-93d3-f82d72be7e46", intl.genreMystery),
|
||||
Tag("b1e97889-25b4-4258-b28b-cd7f4d28ea9b", intl.genrePhilosophical),
|
||||
Tag("423e2eae-a7a2-4a8b-ac03-a8351462d71d", intl.genreRomance),
|
||||
Tag("256c8bd9-4904-4360-bf4f-508a76d67183", intl.genreSciFi),
|
||||
Tag("e5301a23-ebd9-49dd-a0cb-2add944c7fe9", intl.genreSliceOfLife),
|
||||
Tag("69964a64-2f90-4d33-beeb-f3ed2875eb4c", intl.genreSports),
|
||||
Tag("7064a261-a137-4d3a-8848-2d385de3a99c", intl.genreSuperhero),
|
||||
Tag("07251805-a27e-4d59-b488-f0bfbec15168", intl.genreThriller),
|
||||
Tag("f8f62932-27da-4fe4-8ee1-6779a8c5edba", intl.genreTragedy),
|
||||
Tag("acc803a4-c95a-4c22-86fc-eb6b582d82a2", intl.genreWuxia),
|
||||
)
|
||||
|
||||
return tags.sortIfTranslated(intl)
|
||||
}
|
||||
|
||||
// to get all tags from dex https://api.mangadex.org/manga/tag
|
||||
internal fun getThemes(intl: MangaDexIntl): List<Tag> {
|
||||
val tags = listOf(
|
||||
Tag("e64f6742-c834-471d-8d72-dd51fc02b835", intl.themeAliens),
|
||||
Tag("3de8c75d-8ee3-48ff-98ee-e20a65c86451", intl.themeAnimals),
|
||||
Tag("ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", intl.themeCooking),
|
||||
Tag("9ab53f92-3eed-4e9b-903a-917c86035ee3", intl.themeCrossdressing),
|
||||
Tag("da2d50ca-3018-4cc0-ac7a-6b7d472a29ea", intl.themeDelinquents),
|
||||
Tag("39730448-9a5f-48a2-85b0-a70db87b1233", intl.themeDemons),
|
||||
Tag("2bd2e8d0-f146-434a-9b51-fc9ff2c5fe6a", intl.themeGenderSwap),
|
||||
Tag("3bb26d85-09d5-4d2e-880c-c34b974339e9", intl.themeGhosts),
|
||||
Tag("fad12b5e-68ba-460e-b933-9ae8318f5b65", intl.themeGyaru),
|
||||
Tag("aafb99c1-7f60-43fa-b75f-fc9502ce29c7", intl.themeHarem),
|
||||
Tag("5bd0e105-4481-44ca-b6e7-7544da56b1a3", intl.themeIncest),
|
||||
Tag("2d1f5d56-a1e5-4d0d-a961-2193588b08ec", intl.themeLoli),
|
||||
Tag("85daba54-a71c-4554-8a28-9901a8b0afad", intl.themeMafia),
|
||||
Tag("a1f53773-c69a-4ce5-8cab-fffcd90b1565", intl.themeMagic),
|
||||
Tag("799c202e-7daa-44eb-9cf7-8a3c0441531e", intl.themeMartialArts),
|
||||
Tag("ac72833b-c4e9-4878-b9db-6c8a4a99444a", intl.themeMilitary),
|
||||
Tag("dd1f77c5-dea9-4e2b-97ae-224af09caf99", intl.themeMonsterGirls),
|
||||
Tag("36fd93ea-e8b8-445e-b836-358f02b3d33d", intl.themeMonsters),
|
||||
Tag("f42fbf9e-188a-447b-9fdc-f19dc1e4d685", intl.themeMusic),
|
||||
Tag("489dd859-9b61-4c37-af75-5b18e88daafc", intl.themeNinja),
|
||||
Tag("92d6d951-ca5e-429c-ac78-451071cbf064", intl.themeOfficeWorkers),
|
||||
Tag("df33b754-73a3-4c54-80e6-1a74a8058539", intl.themePolice),
|
||||
Tag("9467335a-1b83-4497-9231-765337a00b96", intl.themePostApocalyptic),
|
||||
Tag("3b60b75c-a2d7-4860-ab56-05f391bb889c", intl.themePsychological),
|
||||
Tag("0bc90acb-ccc1-44ca-a34a-b9f3a73259d0", intl.themeReincarnation),
|
||||
Tag("65761a2a-415e-47f3-bef2-a9dababba7a6", intl.themeReverseHarem),
|
||||
Tag("81183756-1453-4c81-aa9e-f6e1b63be016", intl.themeSamurai),
|
||||
Tag("caaa44eb-cd40-4177-b930-79d3ef2afe87", intl.themeSchoolLife),
|
||||
Tag("ddefd648-5140-4e5f-ba18-4eca4071d19b", intl.themeShota),
|
||||
Tag("eabc5b4c-6aff-42f3-b657-3e90cbd00b75", intl.themeSupernatural),
|
||||
Tag("5fff9cde-849c-4d78-aab0-0d52b2ee1d25", intl.themeSurvival),
|
||||
Tag("292e862b-2d17-4062-90a2-0356caa4ae27", intl.themeTimeTravel),
|
||||
Tag("31932a7e-5b8e-49a6-9f12-2afa39dc544c", intl.themeTraditionalGames),
|
||||
Tag("d7d1730f-6eb0-4ba6-9437-602cac38664c", intl.themeVampires),
|
||||
Tag("9438db5a-7e2a-4ac0-b39e-e0d95a34b8a8", intl.themeVideoGames),
|
||||
Tag("d14322ac-4d6f-4e9b-afd9-629d5f4d8a41", intl.themeVillainess),
|
||||
Tag("8c86611e-fab7-4986-9dec-d1a2f44acdd5", intl.themeVirtualReality),
|
||||
Tag("631ef465-9aba-4afb-b0fc-ea10efe274a8", intl.themeZombies)
|
||||
)
|
||||
|
||||
return tags.sortIfTranslated(intl)
|
||||
}
|
||||
|
||||
internal fun getTags(intl: MangaDexIntl): List<Tag> {
|
||||
return getContents(intl) + getFormats(intl) + getGenres(intl) + getThemes(intl)
|
||||
}
|
||||
|
||||
private data class TagMode(val title: String, val value: String) {
|
||||
override fun toString(): String = title
|
||||
}
|
||||
|
||||
private fun getTagModes(intl: MangaDexIntl) = arrayOf(
|
||||
TagMode(intl.modeAnd, "AND"),
|
||||
TagMode(intl.modeOr, "OR")
|
||||
)
|
||||
|
||||
private class TagInclusionMode(intl: MangaDexIntl, modes: Array<TagMode>) :
|
||||
Filter.Select<TagMode>(intl.includedTagsMode, modes, 0),
|
||||
UrlQueryFilter {
|
||||
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
|
||||
url.addQueryParameter("includedTagsMode", values[state].value)
|
||||
}
|
||||
}
|
||||
|
||||
private class TagExclusionMode(intl: MangaDexIntl, modes: Array<TagMode>) :
|
||||
Filter.Select<TagMode>(intl.excludedTagsMode, modes, 1),
|
||||
UrlQueryFilter {
|
||||
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
|
||||
url.addQueryParameter("excludedTagsMode", values[state].value)
|
||||
}
|
||||
}
|
||||
|
||||
data class Sortable(val title: String, val value: String) {
|
||||
override fun toString(): String = title
|
||||
}
|
||||
|
||||
private fun getSortables(intl: MangaDexIntl) = arrayOf(
|
||||
Sortable(intl.sortAlphabetic, "title"),
|
||||
Sortable(intl.sortChapterUploadedAt, "latestUploadedChapter"),
|
||||
Sortable(intl.sortNumberOfFollows, "followedCount"),
|
||||
Sortable(intl.sortContentCreatedAt, "createdAt"),
|
||||
Sortable(intl.sortContentInfoUpdatedAt, "updatedAt"),
|
||||
Sortable(intl.sortRelevance, "relevance"),
|
||||
Sortable(intl.sortYear, "year")
|
||||
)
|
||||
|
||||
class SortFilter(intl: MangaDexIntl, private val sortables: Array<Sortable>) :
|
||||
Filter.Sort(
|
||||
intl.sort,
|
||||
sortables.map(Sortable::title).toTypedArray(),
|
||||
Selection(2, false)
|
||||
),
|
||||
UrlQueryFilter {
|
||||
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
|
||||
if (state != null) {
|
||||
val query = sortables[state!!.index].value
|
||||
val value = when (state!!.ascending) {
|
||||
true -> "asc"
|
||||
false -> "desc"
|
||||
}
|
||||
|
||||
url.addQueryParameter("order[$query]", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class HasAvailableChaptersFilter(intl: MangaDexIntl) :
|
||||
Filter.CheckBox(intl.hasAvailableChapters),
|
||||
UrlQueryFilter {
|
||||
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
|
||||
if (state) {
|
||||
url.addQueryParameter("hasAvailableChapters", "true")
|
||||
url.addQueryParameter("availableTranslatedLanguage[]", dexLang)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TagsFilter(intl: MangaDexIntl, innerFilters: FilterList) :
|
||||
Filter.Group<Filter<*>>(intl.tags, innerFilters),
|
||||
UrlQueryFilter {
|
||||
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
|
||||
state.filterIsInstance<UrlQueryFilter>()
|
||||
.forEach { filter -> filter.addQueryParameter(url, dexLang) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTagFilters(intl: MangaDexIntl): FilterList = FilterList(
|
||||
TagInclusionMode(intl, getTagModes(intl)),
|
||||
TagExclusionMode(intl, getTagModes(intl)),
|
||||
)
|
||||
|
||||
internal fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList, dexLang: String): String {
|
||||
filters.filterIsInstance<UrlQueryFilter>()
|
||||
.forEach { filter -> filter.addQueryParameter(url, dexLang) }
|
||||
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
private fun List<Tag>.sortIfTranslated(intl: MangaDexIntl): List<Tag> = apply {
|
||||
if (intl.availableLang == MangaDexIntl.ENGLISH) {
|
||||
return this
|
||||
}
|
||||
|
||||
return sortedWith(compareBy(intl.collator, Tag::name))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,14 +15,16 @@ import kotlinx.serialization.json.Json
|
|||
import kotlinx.serialization.json.jsonArray
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.parser.Parser
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MangaDexHelper() {
|
||||
class MangaDexHelper(private val lang: String) {
|
||||
|
||||
val mdFilters = MangaDexFilters()
|
||||
|
||||
|
@ -34,16 +36,26 @@ class MangaDexHelper() {
|
|||
prettyPrint = true
|
||||
}
|
||||
|
||||
val intl = MangaDexIntl(lang)
|
||||
|
||||
/**
|
||||
* Gets the UUID from the url
|
||||
*/
|
||||
fun getUUIDFromUrl(url: String) = url.substringAfterLast("/")
|
||||
|
||||
/**
|
||||
* get chapters for manga (aka manga/$id/feed endpoint)
|
||||
* Get chapters for manga (aka manga/$id/feed endpoint)
|
||||
*/
|
||||
fun getChapterEndpoint(mangaId: String, offset: Int, langCode: String) =
|
||||
"${MDConstants.apiMangaUrl}/$mangaId/feed?includes[]=${MDConstants.scanlator}&includes[]=${MDConstants.uploader}&limit=500&offset=$offset&translatedLanguage[]=$langCode&order[volume]=desc&order[chapter]=desc"
|
||||
"${MDConstants.apiMangaUrl}/$mangaId/feed".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("includes[]", MDConstants.scanlator)
|
||||
.addQueryParameter("includes[]", MDConstants.uploader)
|
||||
.addQueryParameter("limit", "500")
|
||||
.addQueryParameter("offset", offset.toString())
|
||||
.addQueryParameter("translatedLanguage[]", langCode)
|
||||
.addQueryParameter("order[volume]", "desc")
|
||||
.addQueryParameter("order[chapter]", "desc")
|
||||
.toString()
|
||||
|
||||
/**
|
||||
* Check if the manga url is a valid uuid
|
||||
|
@ -61,15 +73,15 @@ class MangaDexHelper() {
|
|||
fun getLatestChapterOffset(page: Int): String = (MDConstants.latestChapterLimit * (page - 1)).toString()
|
||||
|
||||
/**
|
||||
* Remove markdown links as well as parse any html characters in description or
|
||||
* chapter name to actual characters for example ♥ will show ♥
|
||||
* Remove any HTML characters in description or chapter name to actual
|
||||
* characters. For example ♥ will show ♥
|
||||
*/
|
||||
fun cleanString(string: String): String {
|
||||
val unescapedString = Parser.unescapeEntities(string, false)
|
||||
|
||||
return unescapedString
|
||||
return Parser.unescapeEntities(string, false)
|
||||
.substringBefore("---")
|
||||
.replace(markdownLinksRegex, "$1")
|
||||
.replace(markdownItalicBoldRegex, "$1")
|
||||
.replace(markdownItalicRegex, "$1")
|
||||
.trim()
|
||||
}
|
||||
|
||||
|
@ -109,6 +121,8 @@ class MangaDexHelper() {
|
|||
.build()
|
||||
|
||||
val markdownLinksRegex = "\\[([^]]+)\\]\\(([^)]+)\\)".toRegex()
|
||||
val markdownItalicBoldRegex = "\\*+\\s*([^\\*]*)\\s*\\*+".toRegex()
|
||||
val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex()
|
||||
|
||||
val titleSpecialCharactersRegex = "[^a-z0-9]+".toRegex()
|
||||
|
||||
|
@ -213,7 +227,12 @@ class MangaDexHelper() {
|
|||
/**
|
||||
* Create an SManga from json element with all details
|
||||
*/
|
||||
fun createManga(mangaDataDto: MangaDataDto, chapters: List<String>, lang: String, coverSuffix: String?): SManga {
|
||||
fun createManga(
|
||||
mangaDataDto: MangaDataDto,
|
||||
chapters: List<String>,
|
||||
lang: String,
|
||||
coverSuffix: String?
|
||||
): SManga {
|
||||
try {
|
||||
val attr = mangaDataDto.attributes
|
||||
|
||||
|
@ -224,7 +243,7 @@ class MangaDexHelper() {
|
|||
if (tempContentRating == null || tempContentRating.equals("safe", true)) {
|
||||
null
|
||||
} else {
|
||||
"Content rating: " + tempContentRating.capitalize(Locale.US)
|
||||
intl.contentRatingGenre(tempContentRating)
|
||||
}
|
||||
|
||||
val dexLocale = Locale.forLanguageTag(lang)
|
||||
|
@ -237,44 +256,53 @@ class MangaDexHelper() {
|
|||
.capitalize(dexLocale)
|
||||
)
|
||||
|
||||
val authors = mangaDataDto.relationships.filter { relationshipDto ->
|
||||
relationshipDto.type.equals(MDConstants.author, true)
|
||||
}.mapNotNull { it.attributes!!.name }.distinct()
|
||||
val authors = mangaDataDto.relationships
|
||||
.filter { relationshipDto ->
|
||||
relationshipDto.type.equals(MDConstants.author, true)
|
||||
}
|
||||
.mapNotNull { it.attributes!!.name }.distinct()
|
||||
|
||||
val artists = mangaDataDto.relationships.filter { relationshipDto ->
|
||||
relationshipDto.type.equals(MDConstants.artist, true)
|
||||
}.mapNotNull { it.attributes!!.name }.distinct()
|
||||
val artists = mangaDataDto.relationships
|
||||
.filter { relationshipDto ->
|
||||
relationshipDto.type.equals(MDConstants.artist, true)
|
||||
}
|
||||
.mapNotNull { it.attributes!!.name }.distinct()
|
||||
|
||||
val coverFileName = mangaDataDto.relationships.firstOrNull { relationshipDto ->
|
||||
relationshipDto.type.equals(MDConstants.coverArt, true)
|
||||
}?.attributes?.fileName
|
||||
val coverFileName = mangaDataDto.relationships
|
||||
.firstOrNull { relationshipDto ->
|
||||
relationshipDto.type.equals(MDConstants.coverArt, true)
|
||||
}
|
||||
?.attributes
|
||||
?.fileName
|
||||
|
||||
// get tag list
|
||||
val tags = mdFilters.getTags()
|
||||
val tags = mdFilters.getTags(intl)
|
||||
|
||||
// map ids to tag names
|
||||
val genreList = (
|
||||
attr.tags
|
||||
.map { it.id }
|
||||
.map { dexId ->
|
||||
tags.firstOrNull { it.id == dexId }
|
||||
}
|
||||
.map { it?.name } +
|
||||
nonGenres
|
||||
)
|
||||
.filter { it.isNullOrBlank().not() }
|
||||
val genresMap = attr.tags
|
||||
.groupBy({ it.attributes.group }) { tagDto ->
|
||||
tags.firstOrNull { it.id == tagDto.id }?.name
|
||||
}
|
||||
.map { (group, tags) ->
|
||||
group to tags.filterNotNull().sortedWith(intl.collator)
|
||||
}
|
||||
.toMap()
|
||||
|
||||
val genreList = MDConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } +
|
||||
nonGenres.filterNotNull()
|
||||
|
||||
val desc = attr.description.asMdMap()
|
||||
|
||||
return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang).apply {
|
||||
description = cleanString(desc[lang] ?: desc["en"] ?: "")
|
||||
author = authors.joinToString(", ")
|
||||
artist = artists.joinToString(", ")
|
||||
status = getPublicationStatus(attr, chapters)
|
||||
genre = genreList.joinToString(", ")
|
||||
genre = genreList
|
||||
.filter(String::isNotEmpty)
|
||||
.joinToString(", ")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("MangaDex", "error parsing manga", e)
|
||||
throw(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -295,14 +323,21 @@ class MangaDexHelper() {
|
|||
.joinToString(" & ")
|
||||
.ifEmpty {
|
||||
// fall back to uploader name if no group
|
||||
val users = chapterDataDto.relationships.filter { relationshipDto ->
|
||||
relationshipDto.type.equals(
|
||||
MDConstants.uploader,
|
||||
true
|
||||
)
|
||||
}.mapNotNull { it.attributes!!.username }
|
||||
users.joinToString(" & ", if (users.isNotEmpty()) "Uploaded by " else "")
|
||||
}.ifEmpty { "No Group" } // "No Group" as final resort
|
||||
val users = chapterDataDto.relationships
|
||||
.filter { relationshipDto ->
|
||||
relationshipDto.type.equals(
|
||||
MDConstants.uploader,
|
||||
true
|
||||
)
|
||||
}
|
||||
.mapNotNull { it.attributes!!.username }
|
||||
|
||||
users.joinToString(
|
||||
" & ",
|
||||
if (users.isNotEmpty()) intl.uploadedBy(users.toString()) else ""
|
||||
)
|
||||
}
|
||||
.ifEmpty { intl.noGroup } // "No Group" as final resort
|
||||
|
||||
val chapterName = mutableListOf<String>()
|
||||
// Build chapter name
|
||||
|
@ -347,7 +382,7 @@ class MangaDexHelper() {
|
|||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("MangaDex", "error parsing chapter", e)
|
||||
throw(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,661 @@
|
|||
package eu.kanade.tachiyomi.extension.all.mangadex
|
||||
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
|
||||
class MangaDexIntl(val lang: String) {
|
||||
|
||||
val availableLang: String = if (lang in AVAILABLE_LANGS) lang else ENGLISH
|
||||
|
||||
val locale: Locale = Locale.forLanguageTag(availableLang)
|
||||
|
||||
val collator: Collator = Collator.getInstance(locale)
|
||||
|
||||
val invalidGroupId: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "ID do grupo inválido"
|
||||
else -> "Not a valid group ID"
|
||||
}
|
||||
|
||||
val invalidAuthorId: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "ID do autor inválido"
|
||||
else -> "Not a valid author ID"
|
||||
}
|
||||
|
||||
val noSeriesInList: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Sem séries na lista"
|
||||
else -> "No series in the list"
|
||||
}
|
||||
|
||||
val migrateWarning: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE ->
|
||||
"Migre esta entrada do $MANGADEX_NAME para o $MANGADEX_NAME para atualizar"
|
||||
else -> "Migrate this entry from $MANGADEX_NAME to $MANGADEX_NAME to update it"
|
||||
}
|
||||
|
||||
val coverQuality: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Qualidade da capa"
|
||||
else -> "Cover quality"
|
||||
}
|
||||
|
||||
val coverQualityOriginal: String = "Original"
|
||||
|
||||
val coverQualityMedium: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Média"
|
||||
else -> "Medium"
|
||||
}
|
||||
|
||||
val coverQualityLow: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Baixa"
|
||||
else -> "Low"
|
||||
}
|
||||
|
||||
val dataSaver: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Economia de dados"
|
||||
else -> "Data saver"
|
||||
}
|
||||
|
||||
val dataSaverSummary: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Utiliza imagens menores e mais compactadas"
|
||||
else -> "Enables smaller, more compressed images"
|
||||
}
|
||||
|
||||
val standardHttpsPort: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Utilizar somente a porta 443 do HTTPS"
|
||||
else -> "Use HTTPS port 443 only"
|
||||
}
|
||||
|
||||
val standardHttpsPortSummary: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE ->
|
||||
"Ative para fazer requisições em somente servidores de imagem que usem a porta 443. " +
|
||||
"Isso permite com que usuários com regras mais restritas de firewall possam acessar " +
|
||||
"as imagens do MangaDex."
|
||||
else ->
|
||||
"Enable to only request image servers that use port 443. This allows users with " +
|
||||
"stricter firewall restrictions to access MangaDex images"
|
||||
}
|
||||
|
||||
val contentRating: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Classificação de conteúdo"
|
||||
else -> "Content rating"
|
||||
}
|
||||
|
||||
val standardContentRating: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Classificação de conteúdo padrão"
|
||||
else -> "Default content rating"
|
||||
}
|
||||
|
||||
val standardContentRatingSummary: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE ->
|
||||
"Mostra os conteúdos com as classificações selecionadas por padrão"
|
||||
else -> "Show content with the selected ratings by default"
|
||||
}
|
||||
|
||||
val contentRatingSafe: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Seguro"
|
||||
else -> "Safe"
|
||||
}
|
||||
|
||||
val contentRatingSuggestive: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Sugestivo"
|
||||
else -> "Suggestive"
|
||||
}
|
||||
|
||||
val contentRatingErotica: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Erótico"
|
||||
else -> "Erotica"
|
||||
}
|
||||
|
||||
val contentRatingPornographic: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Pornográfico"
|
||||
else -> "Pornographic"
|
||||
}
|
||||
|
||||
private val contentRatingMap: Map<String, String> = mapOf(
|
||||
"safe" to contentRatingSafe,
|
||||
"suggestive" to contentRatingSuggestive,
|
||||
"erotica" to contentRatingErotica,
|
||||
"pornographic" to contentRatingPornographic
|
||||
)
|
||||
|
||||
fun contentRatingGenre(contentRatingKey: String): String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Classificação: ${contentRatingMap[contentRatingKey]}"
|
||||
else -> "$contentRating: ${contentRatingMap[contentRatingKey]}"
|
||||
}
|
||||
|
||||
val originalLanguage: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Idioma original"
|
||||
else -> "Original language"
|
||||
}
|
||||
|
||||
val originalLanguageFilterJapanese: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "${languageDisplayName(JAPANESE)} (Mangá)"
|
||||
else -> "${languageDisplayName(JAPANESE)} (Manga)"
|
||||
}
|
||||
|
||||
val originalLanguageFilterChinese: String = "${languageDisplayName(CHINESE)} (Manhua)"
|
||||
|
||||
val originalLanguageFilterKorean: String = "${languageDisplayName(KOREAN)} (Manhwa)"
|
||||
|
||||
val filterOriginalLanguages: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Filtrar os idiomas originais"
|
||||
else -> "Filter original languages"
|
||||
}
|
||||
|
||||
val filterOriginalLanguagesSummary: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE ->
|
||||
"Mostra somente conteúdos que foram publicados originalmente nos idiomas " +
|
||||
"selecionados nas seções de recentes e navegar"
|
||||
else ->
|
||||
"Only show content that was originally published in the selected languages in " +
|
||||
"both latest and browse"
|
||||
}
|
||||
|
||||
val blockGroupByUuid: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Bloquear grupos por UUID"
|
||||
else -> "Block groups by UUID"
|
||||
}
|
||||
|
||||
val blockGroupByUuidSummary: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE ->
|
||||
"Capítulos de grupos bloqueados não irão aparecer no feed de Recentes ou Mangás. " +
|
||||
"Digite uma lista de UUIDs dos grupos separados por vírgulas"
|
||||
else ->
|
||||
"Chapters from blocked groups will not show up in Latest or Manga feed. " +
|
||||
"Enter as a Comma-separated list of group UUIDs"
|
||||
}
|
||||
|
||||
val blockUploaderByUuid: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Bloquear uploaders por UUID"
|
||||
else -> "Block uploader by UUID"
|
||||
}
|
||||
|
||||
val blockUploaderByUuidSummary: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE ->
|
||||
"Capítulos de usuários bloqueados não irão aparecer no feed de Recentes ou Mangás. " +
|
||||
"Digite uma lista de UUIDs dos usuários separados por vírgulas"
|
||||
else ->
|
||||
"Chapters from blocked uploaders will not show up in Latest or Manga feed. " +
|
||||
"Enter as a Comma-separated list of uploader UUIDs"
|
||||
}
|
||||
|
||||
val publicationDemographic: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Demografia da publicação"
|
||||
else -> "Publication demographic"
|
||||
}
|
||||
|
||||
val publicationDemographicNone: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Nenhuma"
|
||||
else -> "None"
|
||||
}
|
||||
|
||||
val publicationDemographicShounen: String = "Shounen"
|
||||
|
||||
val publicationDemographicShoujo: String = "Shoujo"
|
||||
|
||||
val publicationDemographicSeinen: String = "Seinen"
|
||||
|
||||
val publicationDemographicJosei: String = "Josei"
|
||||
|
||||
val status: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Estado"
|
||||
else -> "Status"
|
||||
}
|
||||
|
||||
val statusOngoing: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Em andamento"
|
||||
else -> "Ongoing"
|
||||
}
|
||||
|
||||
val statusCompleted: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Completo"
|
||||
else -> "Completed"
|
||||
}
|
||||
|
||||
val statusHiatus: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Hiato"
|
||||
else -> "Hiatus"
|
||||
}
|
||||
|
||||
val statusCancelled: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Cancelado"
|
||||
else -> "Cancelled"
|
||||
}
|
||||
|
||||
val content: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Conteúdo"
|
||||
else -> "Content"
|
||||
}
|
||||
|
||||
val contentGore: String = "Gore"
|
||||
|
||||
val contentSexualViolence: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Violência sexual"
|
||||
else -> "Sexual Violence"
|
||||
}
|
||||
|
||||
val format: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Formato"
|
||||
else -> "Format"
|
||||
}
|
||||
|
||||
val formatAdaptation: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Adaptação"
|
||||
else -> "Adaptation"
|
||||
}
|
||||
|
||||
val formatAnthology: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Antologia"
|
||||
else -> "Anthology"
|
||||
}
|
||||
|
||||
val formatAwardWinning: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Premiado"
|
||||
else -> "Award Winning"
|
||||
}
|
||||
|
||||
val formatDoujinshi: String = "Doujinshi"
|
||||
|
||||
val formatFanColored: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Colorizado por fãs"
|
||||
else -> "Fan Colored"
|
||||
}
|
||||
|
||||
val formatFourKoma: String = "4-Koma"
|
||||
|
||||
val formatFullColor: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Colorido"
|
||||
else -> "Full Color"
|
||||
}
|
||||
|
||||
val formatLongStrip: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Vertical"
|
||||
else -> "Long Strip"
|
||||
}
|
||||
|
||||
val formatOfficialColored: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Colorizado oficialmente"
|
||||
else -> "Official Colored"
|
||||
}
|
||||
|
||||
val formatOneshot: String = "Oneshot"
|
||||
|
||||
val formatUserCreated: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Criado por usuários"
|
||||
else -> "User Created"
|
||||
}
|
||||
|
||||
val formatWebComic: String = "Web Comic"
|
||||
|
||||
val genre: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Gênero"
|
||||
else -> "Genre"
|
||||
}
|
||||
|
||||
val genreAction: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Ação"
|
||||
else -> "Action"
|
||||
}
|
||||
|
||||
val genreAdventure: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Aventura"
|
||||
else -> "Adventure"
|
||||
}
|
||||
|
||||
val genreBoysLove: String = "Boy's Love"
|
||||
|
||||
val genreComedy: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Comédia"
|
||||
else -> "Comedy"
|
||||
}
|
||||
|
||||
val genreCrime: String = "Crime"
|
||||
|
||||
val genreDrama: String = "Drama"
|
||||
|
||||
val genreFantasy: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Fantasia"
|
||||
else -> "Fantasy"
|
||||
}
|
||||
|
||||
val genreGirlsLove: String = "Girl's Love"
|
||||
|
||||
val genreHistorical: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Histórico"
|
||||
else -> "Historical"
|
||||
}
|
||||
|
||||
val genreHorror: String = "Horror"
|
||||
|
||||
val genreIsekai: String = "Isekai"
|
||||
|
||||
val genreMagicalGirls: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Garotas mágicas"
|
||||
else -> "Magical Girls"
|
||||
}
|
||||
|
||||
val genreMecha: String = "Mecha"
|
||||
|
||||
val genreMedical: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Médico"
|
||||
else -> "Medical"
|
||||
}
|
||||
|
||||
val genreMystery: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Mistério"
|
||||
else -> "Mystery"
|
||||
}
|
||||
|
||||
val genrePhilosophical: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Filosófico"
|
||||
else -> "Philosophical"
|
||||
}
|
||||
|
||||
val genreRomance: String = "Romance"
|
||||
|
||||
val genreSciFi: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Ficção científica"
|
||||
else -> "Sci-Fi"
|
||||
}
|
||||
|
||||
val genreSliceOfLife: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Cotidiano"
|
||||
else -> "Slice of Life"
|
||||
}
|
||||
|
||||
val genreSports: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Esportes"
|
||||
else -> "Sports"
|
||||
}
|
||||
|
||||
val genreSuperhero: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Super-heroi"
|
||||
else -> "Superhero"
|
||||
}
|
||||
|
||||
val genreThriller: String = "Thriller"
|
||||
|
||||
val genreTragedy: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Tragédia"
|
||||
else -> "Tragedy"
|
||||
}
|
||||
|
||||
val genreWuxia: String = "Wuxia"
|
||||
|
||||
val theme: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Tema"
|
||||
else -> "Theme"
|
||||
}
|
||||
|
||||
val themeAliens: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Alienígenas"
|
||||
else -> "Aliens"
|
||||
}
|
||||
|
||||
val themeAnimals: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Animais"
|
||||
else -> "Animals"
|
||||
}
|
||||
|
||||
val themeCooking: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Culinária"
|
||||
else -> "Cooking"
|
||||
}
|
||||
|
||||
val themeCrossdressing: String = "Crossdressing"
|
||||
|
||||
val themeDelinquents: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Delinquentes"
|
||||
else -> "Delinquents"
|
||||
}
|
||||
|
||||
val themeDemons: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Demônios"
|
||||
else -> "Demons"
|
||||
}
|
||||
|
||||
val themeGenderSwap: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Troca de gêneros"
|
||||
else -> "Genderswap"
|
||||
}
|
||||
|
||||
val themeGhosts: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Fantasmas"
|
||||
else -> "Ghosts"
|
||||
}
|
||||
|
||||
val themeGyaru: String = "Gyaru"
|
||||
|
||||
val themeHarem: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Harém"
|
||||
else -> "Harem"
|
||||
}
|
||||
|
||||
val themeIncest: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Incesto"
|
||||
else -> "Incest"
|
||||
}
|
||||
|
||||
val themeLoli: String = "Loli"
|
||||
|
||||
val themeMafia: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Máfia"
|
||||
else -> "Mafia"
|
||||
}
|
||||
|
||||
val themeMagic: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Magia"
|
||||
else -> "Magic"
|
||||
}
|
||||
|
||||
val themeMartialArts: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Artes marciais"
|
||||
else -> "Martial Arts"
|
||||
}
|
||||
|
||||
val themeMilitary: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Militar"
|
||||
else -> "Military"
|
||||
}
|
||||
|
||||
val themeMonsterGirls: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Garotas monstro"
|
||||
else -> "Monster Girls"
|
||||
}
|
||||
|
||||
val themeMonsters: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Monstros"
|
||||
else -> "Monsters"
|
||||
}
|
||||
|
||||
val themeMusic: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Musical"
|
||||
else -> "Music"
|
||||
}
|
||||
|
||||
val themeNinja: String = "Ninja"
|
||||
|
||||
val themeOfficeWorkers: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Funcionários de escritório"
|
||||
else -> "Office Workers"
|
||||
}
|
||||
|
||||
val themePolice: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Policial"
|
||||
else -> "Police"
|
||||
}
|
||||
|
||||
val themePostApocalyptic: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Pós-apocalíptico"
|
||||
else -> "Post-Apocalypytic"
|
||||
}
|
||||
|
||||
val themePsychological: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Psicológico"
|
||||
else -> "Psychological"
|
||||
}
|
||||
|
||||
val themeReincarnation: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Reencarnação"
|
||||
else -> "Reincarnation"
|
||||
}
|
||||
|
||||
val themeReverseHarem: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Harém reverso"
|
||||
else -> "Reverse Harem"
|
||||
}
|
||||
|
||||
val themeSamurai: String = "Samurai"
|
||||
|
||||
val themeSchoolLife: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Vida escolar"
|
||||
else -> "School Life"
|
||||
}
|
||||
|
||||
val themeShota: String = "Shota"
|
||||
|
||||
val themeSupernatural: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Sobrenatural"
|
||||
else -> "Supernatural"
|
||||
}
|
||||
|
||||
val themeSurvival: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Sobrevivência"
|
||||
else -> "Survival"
|
||||
}
|
||||
|
||||
val themeTimeTravel: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Viagem no tempo"
|
||||
else -> "Time Travel"
|
||||
}
|
||||
|
||||
val themeTraditionalGames: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Jogos tradicionais"
|
||||
else -> "Traditional Games"
|
||||
}
|
||||
|
||||
val themeVampires: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Vampiros"
|
||||
else -> "Vampires"
|
||||
}
|
||||
|
||||
val themeVideoGames: String = "Video Games"
|
||||
|
||||
val themeVillainess: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Vilania"
|
||||
else -> "Villainess"
|
||||
}
|
||||
|
||||
val themeVirtualReality: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Realidade virtual"
|
||||
else -> "Virtual Reality"
|
||||
}
|
||||
|
||||
val themeZombies: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Zumbis"
|
||||
else -> "Zombies"
|
||||
}
|
||||
|
||||
val tags: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Modo das tags"
|
||||
else -> "Tags mode"
|
||||
}
|
||||
|
||||
val includedTagsMode: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Modo de inclusão de tags"
|
||||
else -> "Included tags mode"
|
||||
}
|
||||
|
||||
val excludedTagsMode: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Modo de exclusão de tags"
|
||||
else -> "Excluded tags mode"
|
||||
}
|
||||
|
||||
val modeAnd: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "E"
|
||||
else -> "And"
|
||||
}
|
||||
|
||||
val modeOr: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Ou"
|
||||
else -> "Or"
|
||||
}
|
||||
|
||||
val sort: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Ordenar"
|
||||
else -> "Sort"
|
||||
}
|
||||
|
||||
val sortAlphabetic: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Alfabeticamente"
|
||||
else -> "Alphabetic"
|
||||
}
|
||||
|
||||
val sortChapterUploadedAt: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Upload do capítulo"
|
||||
else -> "Chapter uploaded at"
|
||||
}
|
||||
|
||||
val sortNumberOfFollows: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Número de seguidores"
|
||||
else -> "Number of follows"
|
||||
}
|
||||
|
||||
val sortContentCreatedAt: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Criação do conteúdo"
|
||||
else -> "Content created at"
|
||||
}
|
||||
|
||||
val sortContentInfoUpdatedAt: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Atualização das informações"
|
||||
else -> "Content info updated at"
|
||||
}
|
||||
|
||||
val sortRelevance: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Relevância"
|
||||
else -> "Relevance"
|
||||
}
|
||||
|
||||
val sortYear: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Ano de lançamento"
|
||||
else -> "Year"
|
||||
}
|
||||
|
||||
val hasAvailableChapters: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Há capítulos disponíveis"
|
||||
else -> "Has available chapters"
|
||||
}
|
||||
|
||||
fun languageDisplayName(localeCode: String): String =
|
||||
Locale.forLanguageTag(localeCode)
|
||||
.getDisplayName(locale)
|
||||
.capitalize(locale)
|
||||
|
||||
fun unableToProcessChapterRequest(code: Int): String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE ->
|
||||
"Não foi possível processar a requisição do capítulo. Código HTTP: $code"
|
||||
else -> "Unable to process Chapter request. HTTP code: $code"
|
||||
}
|
||||
|
||||
fun uploadedBy(user: String): String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Enviado por $user"
|
||||
else -> "Uploaded by $user"
|
||||
}
|
||||
|
||||
val noGroup: String = when (availableLang) {
|
||||
BRAZILIAN_PORTUGUESE, PORTUGUESE -> "Sem grupo"
|
||||
else -> "No Group"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BRAZILIAN_PORTUGUESE = "pt-BR"
|
||||
const val CHINESE = "zh"
|
||||
const val ENGLISH = "en"
|
||||
const val JAPANESE = "ja"
|
||||
const val KOREAN = "ko"
|
||||
const val PORTUGUESE = "pt"
|
||||
|
||||
val AVAILABLE_LANGS = arrayOf(BRAZILIAN_PORTUGUESE, ENGLISH, PORTUGUESE)
|
||||
|
||||
const val MANGADEX_NAME = "MangaDex"
|
||||
}
|
||||
}
|
|
@ -59,6 +59,12 @@ data class MangaAttributesDto(
|
|||
@Serializable
|
||||
data class TagDto(
|
||||
val id: String,
|
||||
val attributes: TagAttributesDto
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TagAttributesDto(
|
||||
val group: String
|
||||
)
|
||||
|
||||
fun JsonElement.asMdMap(): Map<String, String> {
|
||||
|
|
Loading…
Reference in New Issue