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:
Alessandro Jean 2022-05-21 19:47:24 -03:00 committed by GitHub
parent 1a8556a280
commit cb9a898787
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1366 additions and 463 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'MangaDex' extName = 'MangaDex'
pkgNameSuffix = 'all.mangadex' pkgNameSuffix = 'all.mangadex'
extClass = '.MangaDexFactory' extClass = '.MangaDexFactory'
extVersionCode = 158 extVersionCode = 159
isNsfw = true isNsfw = true
} }

View File

@ -46,7 +46,8 @@ object MDConstants {
return "${coverQualityPref}_$dexLang" 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") fun getCoverQualityPreferenceEntryValues() = arrayOf("", ".512.jpg", ".256.jpg")
@ -76,10 +77,11 @@ object MDConstants {
} }
private const val originalLanguagePref = "originalLanguage" private const val originalLanguagePref = "originalLanguage"
const val originalLanguagePrefValJapanese = "ja" const val originalLanguagePrefValJapanese = MangaDexIntl.JAPANESE
const val originalLanguagePrefValChinese = "zh" const val originalLanguagePrefValChinese = MangaDexIntl.CHINESE
const val originalLanguagePrefValChineseHk = "zh-hk" const val originalLanguagePrefValChineseHk = "zh-hk"
const val originalLanguagePrefValKorean = "ko" const val originalLanguagePrefValKorean = MangaDexIntl.KOREAN
val originalLanguagePrefDefaults = emptySet<String>()
fun getOriginalLanguagePrefKey(dexLang: String): String { fun getOriginalLanguagePrefKey(dexLang: String): String {
return "${originalLanguagePref}_$dexLang" return "${originalLanguagePref}_$dexLang"
@ -100,4 +102,10 @@ object MDConstants {
fun getBlockedUploaderPrefKey(dexLang: String): String { fun getBlockedUploaderPrefKey(dexLang: String): String {
return "${blockedUploaderPref}_$dexLang" 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)
} }

View File

@ -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.ListDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaDto 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.MangaListDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.RelationshipDto
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
@ -28,6 +29,7 @@ import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request import okhttp3.Request
@ -37,10 +39,11 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Date 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, ConfigurableSource,
HttpSource() { HttpSource() {
override val name = "MangaDex"
override val name = MangaDexIntl.MANGADEX_NAME
override val baseUrl = "https://mangadex.org" override val baseUrl = "https://mangadex.org"
override val supportsLatest = true 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) 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("Referer", "$baseUrl/")
.add("User-Agent", "Tachiyomi " + System.getProperty("http.agent")) .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("limit", MDConstants.mangaLimit.toString())
addQueryParameter("offset", helper.getMangaListOffset(page)) addQueryParameter("offset", helper.getMangaListOffset(page))
addQueryParameter("includes[]", MDConstants.coverArt) addQueryParameter("includes[]", MDConstants.coverArt)
addQueryParameter(
"contentRating[]",
preferences.getStringSet( preferences.getStringSet(
MDConstants.getContentRatingPrefKey(dexLang), MDConstants.getContentRatingPrefKey(dexLang),
MDConstants.contentRatingPrefDefaults MDConstants.contentRatingPrefDefaults
)?.forEach { addQueryParameter("contentRating[]", it) } )
preferences.getStringSet( )
val originalLanguages = preferences.getStringSet(
MDConstants.getOriginalLanguagePrefKey(dexLang), MDConstants.getOriginalLanguagePrefKey(dexLang),
setOf() MDConstants.originalLanguagePrefDefaults
)?.forEach { )
addQueryParameter("originalLanguage[]", it)
// dex has zh and zh-hk for chinese manhua addQueryParameter("originalLanguage[]", originalLanguages)
if (it == MDConstants.originalLanguagePrefValChinese) {
addQueryParameter("originalLanguage[]", MDConstants.originalLanguagePrefValChineseHk) // Dex has zh and zh-hk for Chinese manhua
if (MDConstants.originalLanguagePrefValChinese in originalLanguages!!) {
addQueryParameter(
"originalLanguage[]",
MDConstants.originalLanguagePrefValChineseHk
)
} }
} }
}.build().toUrl().toString()
return GET( return GET(
url = url, url = url.build().toString(),
headers = headers, headers = headers,
cache = CacheControl.FORCE_NETWORK cache = CacheControl.FORCE_NETWORK
) )
} }
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
if (response.isSuccessful.not()) {
throw Exception("HTTP ${response.code}")
}
if (response.code == 204) { if (response.code == 204) {
return MangasPage(emptyList(), false) 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 hasMoreResults = mangaListDto.limit + mangaListDto.offset < mangaListDto.total
val coverSuffix = preferences.getString(MDConstants.getCoverQualityPreferenceKey(dexLang), "") 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 // LATEST section API can't sort by date yet so not implemented
override fun latestUpdatesParse(response: Response): MangasPage { 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 hasMoreResults = chapterListDto.limit + chapterListDto.offset < chapterListDto.total
val mangaIds = chapterListDto.data.map { it.relationships }.flatten() val mangaIds = chapterListDto.data
.filter { it.type == MDConstants.manga }.map { it.id }.distinct() .flatMap { it.relationships }
.filter { it.type == MDConstants.manga }
.map { it.id }
.distinct()
.toSet()
val mangaUrl = MDConstants.apiMangaUrl.toHttpUrlOrNull()!!.newBuilder().apply { val mangaUrl = MDConstants.apiMangaUrl.toHttpUrlOrNull()!!.newBuilder().apply {
addQueryParameter("includes[]", MDConstants.coverArt) addQueryParameter("includes[]", MDConstants.coverArt)
addQueryParameter("limit", mangaIds.size.toString()) addQueryParameter("limit", mangaIds.size.toString())
addQueryParameter(
"contentRating[]",
preferences.getStringSet( preferences.getStringSet(
MDConstants.getContentRatingPrefKey(dexLang), MDConstants.getContentRatingPrefKey(dexLang),
MDConstants.contentRatingPrefDefaults MDConstants.contentRatingPrefDefaults
)?.forEach { addQueryParameter("contentRating[]", it) } )
)
mangaIds.forEach { id -> addQueryParameter("ids[]", mangaIds)
addQueryParameter("ids[]", id)
} }
}.build().toString()
val mangaResponse = client.newCall(GET(mangaUrl, headers, CacheControl.FORCE_NETWORK)).execute() val mangaRequest = GET(mangaUrl.build().toString(), headers, CacheControl.FORCE_NETWORK)
val mangaListDto = helper.json.decodeFromString<MangaListDto>(mangaResponse.body!!.string()) val mangaResponse = client.newCall(mangaRequest).execute()
val mangaListDto = mangaResponse.parseAs<MangaListDto>()
val mangaDtoMap = mangaListDto.data.associateBy({ it.id }, { it }) 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) return MangasPage(mangaList, hasMoreResults)
} }
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
val url = MDConstants.apiChapterUrl.toHttpUrlOrNull()!!.newBuilder().apply { val url = MDConstants.apiChapterUrl.toHttpUrlOrNull()!!.newBuilder().apply {
addQueryParameter("offset", helper.getLatestChapterOffset(page)) addQueryParameter("offset", helper.getLatestChapterOffset(page))
@ -161,32 +178,52 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
addQueryParameter("translatedLanguage[]", dexLang) addQueryParameter("translatedLanguage[]", dexLang)
addQueryParameter("order[publishAt]", "desc") addQueryParameter("order[publishAt]", "desc")
addQueryParameter("includeFutureUpdates", "0") addQueryParameter("includeFutureUpdates", "0")
preferences.getStringSet(
val originalLanguages = preferences.getStringSet(
MDConstants.getOriginalLanguagePrefKey(dexLang), MDConstants.getOriginalLanguagePrefKey(dexLang),
setOf() MDConstants.originalLanguagePrefDefaults
)?.forEach { )
addQueryParameter("originalLanguage[]", it)
// dex has zh and zh-hk for chinese manhua addQueryParameter("originalLanguage[]", originalLanguages)
if (it == MDConstants.originalLanguagePrefValChinese) {
addQueryParameter("originalLanguage[]", MDConstants.originalLanguagePrefValChineseHk) // Dex has zh and zh-hk for Chinese manhua
} if (MDConstants.originalLanguagePrefValChinese in originalLanguages!!) {
addQueryParameter(
"originalLanguage[]",
MDConstants.originalLanguagePrefValChineseHk
)
} }
addQueryParameter(
"contentRating[]",
preferences.getStringSet( preferences.getStringSet(
MDConstants.getContentRatingPrefKey(dexLang), MDConstants.getContentRatingPrefKey(dexLang),
MDConstants.contentRatingPrefDefaults MDConstants.contentRatingPrefDefaults
)?.forEach { addQueryParameter("contentRating[]", it) } )
MDConstants.defaultBlockedGroups.forEach { )
addQueryParameter("excludedGroups[]", it)
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)
} }
preferences.getString(
MDConstants.getBlockedGroupsPrefKey(dexLang), "" return GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK)
)?.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)
} }
// SEARCH section // 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 -> return getMangaIdFromChapterId(query.removePrefix(MDConstants.prefixChSearch)).flatMap { manga_id ->
super.fetchSearchManga(page, MDConstants.prefixIdSearch + manga_id, filters) super.fetchSearchManga(page, MDConstants.prefixIdSearch + manga_id, filters)
} }
query.startsWith(MDConstants.prefixUsrSearch) -> query.startsWith(MDConstants.prefixUsrSearch) ->
return client.newCall(searchMangaUploaderRequest(page, query.removePrefix(MDConstants.prefixUsrSearch))) return client.newCall(searchMangaUploaderRequest(page, query.removePrefix(MDConstants.prefixUsrSearch)))
.asObservableSuccess() .asObservableSuccess()
.map { latestUpdatesParse(it) } .map { latestUpdatesParse(it) }
query.startsWith(MDConstants.prefixListSearch) -> query.startsWith(MDConstants.prefixListSearch) ->
return client.newCall(GET(MDConstants.apiListUrl + "/" + query.removePrefix(MDConstants.prefixListSearch), headers, CacheControl.FORCE_NETWORK)) return client.newCall(GET(MDConstants.apiListUrl + "/" + query.removePrefix(MDConstants.prefixListSearch), headers, CacheControl.FORCE_NETWORK))
.asObservableSuccess() .asObservableSuccess()
.map { searchMangaListRequest(it, page) } .map { searchMangaListRequest(it, page) }
else -> else ->
return super.fetchSearchManga(page, query, filters) return super.fetchSearchManga(page, query, filters)
} }
@ -215,20 +255,18 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
if (response.isSuccessful.not()) { 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 response.parseAs<ChapterDto>().data.relationships
.find { .find { it.type == MDConstants.manga }!!.id
it.type == MDConstants.manga
}!!.id
} }
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val tempUrl = MDConstants.apiMangaUrl.toHttpUrl().newBuilder().apply { val tempUrl = MDConstants.apiMangaUrl.toHttpUrl().newBuilder().apply {
addQueryParameter("limit", MDConstants.mangaLimit.toString()) addQueryParameter("limit", MDConstants.mangaLimit.toString())
addQueryParameter("offset", (helper.getMangaListOffset(page))) addQueryParameter("offset", helper.getMangaListOffset(page))
addQueryParameter("includes[]", MDConstants.coverArt) addQueryParameter("includes[]", MDConstants.coverArt)
} }
@ -241,24 +279,26 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
addQueryParameter("contentRating[]", "suggestive") addQueryParameter("contentRating[]", "suggestive")
addQueryParameter("contentRating[]", "erotica") addQueryParameter("contentRating[]", "erotica")
addQueryParameter("contentRating[]", "pornographic") 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) -> { query.startsWith(MDConstants.prefixGrpSearch) -> {
val groupID = query.removePrefix(MDConstants.prefixGrpSearch) val groupID = query.removePrefix(MDConstants.prefixGrpSearch)
if (!helper.containsUuid(groupID)) { if (!helper.containsUuid(groupID)) {
throw Exception("Not a valid group ID") throw Exception(helper.intl.invalidGroupId)
} }
tempUrl.apply { tempUrl.apply {
addQueryParameter("group", groupID) addQueryParameter("group", groupID)
} }
} }
query.startsWith(MDConstants.prefixAuthSearch) -> { query.startsWith(MDConstants.prefixAuthSearch) -> {
val authorID = query.removePrefix(MDConstants.prefixAuthSearch) val authorID = query.removePrefix(MDConstants.prefixAuthSearch)
if (!helper.containsUuid(authorID)) { if (!helper.containsUuid(authorID)) {
throw Exception("Not a valid author ID") throw Exception(helper.intl.invalidAuthorId)
} }
tempUrl.apply { tempUrl.apply {
@ -266,6 +306,7 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
addQueryParameter("artists[]", authorID) addQueryParameter("artists[]", authorID)
} }
} }
else -> { else -> {
tempUrl.apply { tempUrl.apply {
val actualQuery = query.replace(MDConstants.whitespaceRegex, " ") 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) override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
private fun searchMangaListRequest(response: Response, page: Int): MangasPage { private fun searchMangaListRequest(response: Response, page: Int): MangasPage {
if (response.isSuccessful.not()) { val listDto = response.parseAs<ListDto>()
throw Exception("HTTP ${response.code}")
}
val listDto = helper.json.decodeFromString<ListDto>(response.body!!.string())
val listDtoFiltered = listDto.data.relationships.filter { it.type != "Manga" } val listDtoFiltered = listDto.data.relationships.filter { it.type != "Manga" }
val amount = listDtoFiltered.count() val amount = listDtoFiltered.count()
if (amount < 1) { if (amount < 1) {
throw Exception("No Manga in List") throw Exception(helper.intl.noSeriesInList)
} }
val minIndex = (page - 1) * MDConstants.mangaLimit val minIndex = (page - 1) * MDConstants.mangaLimit
val url = MDConstants.apiMangaUrl.toHttpUrl().newBuilder().apply { 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("offset", "0")
addQueryParameter("includes[]", MDConstants.coverArt) 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 ids = listDtoFiltered
val mangaList = searchMangaListParse(request.execute()) .filterIndexed { i, _ -> i >= minIndex && i < (minIndex + MDConstants.mangaLimit) }
return MangasPage(mangaList, amount.toFloat() / MDConstants.mangaLimit - (page.toFloat() - 1) > 1) .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> { private fun searchMangaListParse(response: Response): List<SManga> {
if (response.isSuccessful.not()) { 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), "") 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("order[publishAt]", "desc")
addQueryParameter("includeFutureUpdates", "0") addQueryParameter("includeFutureUpdates", "0")
addQueryParameter("uploader", uploader) addQueryParameter("uploader", uploader)
preferences.getStringSet(
val originalLanguages = preferences.getStringSet(
MDConstants.getOriginalLanguagePrefKey(dexLang), MDConstants.getOriginalLanguagePrefKey(dexLang),
setOf() MDConstants.originalLanguagePrefDefaults
)?.forEach { )
addQueryParameter("originalLanguage[]", it)
// dex has zh and zh-hk for chinese manhua addQueryParameter("originalLanguage[]", originalLanguages)
if (it == MDConstants.originalLanguagePrefValChinese) {
addQueryParameter("originalLanguage[]", MDConstants.originalLanguagePrefValChineseHk) // Dex has zh and zh-hk for Chinese manhua
} if (MDConstants.originalLanguagePrefValChinese in originalLanguages!!) {
addQueryParameter(
"originalLanguage[]",
MDConstants.originalLanguagePrefValChineseHk
)
} }
addQueryParameter(
"contentRating[]",
preferences.getStringSet( preferences.getStringSet(
MDConstants.getContentRatingPrefKey(dexLang), MDConstants.getContentRatingPrefKey(dexLang),
MDConstants.contentRatingPrefDefaults MDConstants.contentRatingPrefDefaults
)?.forEach { addQueryParameter("contentRating[]", it) } )
MDConstants.defaultBlockedGroups.forEach { )
addQueryParameter("excludedGroups[]", it)
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)
} }
preferences.getString(
MDConstants.getBlockedGroupsPrefKey(dexLang), "" return GET(url.build().toString(), headers, CacheControl.FORCE_NETWORK)
)?.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)
} }
// Manga Details section // Manga Details section
@ -391,20 +456,28 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
*/ */
private fun apiMangaDetailsRequest(manga: SManga): Request { private fun apiMangaDetailsRequest(manga: SManga): Request {
if (!helper.containsUuid(manga.url.trim())) { 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 { val url = (MDConstants.apiUrl + manga.url).toHttpUrl().newBuilder().apply {
addQueryParameter("includes[]", MDConstants.coverArt) addQueryParameter("includes[]", MDConstants.coverArt)
addQueryParameter("includes[]", MDConstants.author) addQueryParameter("includes[]", MDConstants.author)
addQueryParameter("includes[]", MDConstants.artist) 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 { 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), "") 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 url = "${MDConstants.apiMangaUrl}/${manga.data.id}/aggregate?translatedLanguage[]=$langCode"
val response = client.newCall(GET(url, headers)).execute() val response = client.newCall(GET(url, headers)).execute()
val chapters: AggregateDto val chapters: AggregateDto
try { try {
chapters = helper.json.decodeFromString(response.body!!.string()) chapters = response.parseAs()
} catch (e: SerializationException) { } catch (e: SerializationException) {
return emptyList() return emptyList()
} }
if (chapters.volumes.isNullOrEmpty()) 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 // Chapter list section
@ -433,8 +511,9 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
*/ */
override fun chapterListRequest(manga: SManga): Request { override fun chapterListRequest(manga: SManga): Request {
if (!helper.containsUuid(manga.url)) { 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) return actualChapterListRequest(helper.getUUIDFromUrl(manga.url), 0)
} }
@ -447,30 +526,43 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
addQueryParameter("contentRating[]", "suggestive") addQueryParameter("contentRating[]", "suggestive")
addQueryParameter("contentRating[]", "erotica") addQueryParameter("contentRating[]", "erotica")
addQueryParameter("contentRating[]", "pornographic") addQueryParameter("contentRating[]", "pornographic")
preferences.getString(
MDConstants.getBlockedGroupsPrefKey(dexLang), "" val excludedGroups = preferences
)?.split(",")?.sorted()?.forEach { if (it.isNotEmpty()) addQueryParameter("excludedGroups[]", it.trim()) } .getString(MDConstants.getBlockedGroupsPrefKey(dexLang), "")
preferences.getString( ?.split(",")
MDConstants.getBlockedUploaderPrefKey(dexLang), ?.map(String::trim)
"" ?.filter(String::isNotEmpty)
)?.split(",")?.sorted()?.forEach { if (it.isNotEmpty()) addQueryParameter("excludedUploaders[]", it.trim()) } ?.sorted()
}.build().toString() ?.toSet()
return GET(url, headers = headers, cache = CacheControl.FORCE_NETWORK)
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> { override fun chapterListParse(response: Response): List<SChapter> {
if (response.isSuccessful.not()) {
throw Exception("HTTP ${response.code}")
}
if (response.code == 204) { if (response.code == 204) {
return emptyList() return emptyList()
} }
try { try {
val chapterListResponse = helper.json.decodeFromString<ChapterListDto>(response.body!!.string()) val chapterListResponse = response.parseAs<ChapterListDto>()
val chapterListResults = chapterListResponse.data.toMutableList() val chapterListResults = chapterListResponse.data.toMutableList()
val mangaId = val mangaId = response.request.url.toString()
response.request.url.toString().substringBefore("/feed") .substringBefore("/feed")
.substringAfter("${MDConstants.apiMangaUrl}/") .substringAfter("${MDConstants.apiMangaUrl}/")
val limit = chapterListResponse.limit val limit = chapterListResponse.limit
@ -479,31 +571,32 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
var hasMoreResults = (limit + offset) < chapterListResponse.total 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) { while (hasMoreResults) {
offset += limit offset += limit
val newResponse = val newRequest = actualChapterListRequest(mangaId, offset)
client.newCall(actualChapterListRequest(mangaId, offset)).execute() val newResponse = client.newCall(newRequest).execute()
val newChapterList = helper.json.decodeFromString<ChapterListDto>(newResponse.body!!.string()) val newChapterList = newResponse.parseAs<ChapterListDto>()
chapterListResults.addAll(newChapterList.data) chapterListResults.addAll(newChapterList.data)
hasMoreResults = (limit + offset) < newChapterList.total hasMoreResults = (limit + offset) < newChapterList.total
} }
val now = Date().time val now = Date().time
return chapterListResults.mapNotNull { helper.createChapter(it) } return chapterListResults
.filter { .mapNotNull { helper.createChapter(it) }
it.date_upload <= now .filter { it.date_upload <= now }
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e("MangaDex", "error parsing chapter list", e) Log.e("MangaDex", "error parsing chapter list", e)
throw(e) throw e
} }
} }
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
if (!helper.containsUuid(chapter.url)) { 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 chapterId = chapter.url.substringAfter("/chapter/")
val usingStandardHTTPS = val usingStandardHTTPS =
preferences.getBoolean(MDConstants.getStandardHttpsPreferenceKey(dexLang), false) 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> { override fun pageListParse(response: Response): List<Page> {
val atHomeRequestUrl = response.request.url val atHomeRequestUrl = response.request.url
val atHomeDto = helper.json.decodeFromString<AtHomeDto>(response.body!!.string()) val atHomeDto = response.parseAs<AtHomeDto>()
val host = atHomeDto.baseUrl val host = atHomeDto.baseUrl
val usingDataSaver = val usingDataSaver =
preferences.getBoolean(MDConstants.getDataSaverPreferenceKey(dexLang), false) 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) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val coverQualityPref = ListPreference(screen.context).apply { val coverQualityPref = ListPreference(screen.context).apply {
key = MDConstants.getCoverQualityPreferenceKey(dexLang) key = MDConstants.getCoverQualityPreferenceKey(dexLang)
title = "Manga Cover Quality" title = helper.intl.coverQuality
entries = MDConstants.getCoverQualityPreferenceEntries() entries = MDConstants.getCoverQualityPreferenceEntries(helper.intl)
entryValues = MDConstants.getCoverQualityPreferenceEntryValues() entryValues = MDConstants.getCoverQualityPreferenceEntryValues()
setDefaultValue(MDConstants.getCoverQualityPreferenceDefaultValue()) setDefaultValue(MDConstants.getCoverQualityPreferenceDefaultValue())
summary = "%s" summary = "%s"
@ -558,18 +651,22 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
val selected = newValue as String val selected = newValue as String
val index = findIndexOfValue(selected) val index = findIndexOfValue(selected)
val entry = entryValues[index] as String 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 { val dataSaverPref = SwitchPreferenceCompat(screen.context).apply {
key = MDConstants.getDataSaverPreferenceKey(dexLang) key = MDConstants.getDataSaverPreferenceKey(dexLang)
title = "Data saver" title = helper.intl.dataSaver
summary = "Enables smaller, more compressed images" summary = helper.intl.dataSaverSummary
setDefaultValue(false) setDefaultValue(false)
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean val checkValue = newValue as Boolean
preferences.edit() preferences.edit()
.putBoolean(MDConstants.getDataSaverPreferenceKey(dexLang), checkValue) .putBoolean(MDConstants.getDataSaverPreferenceKey(dexLang), checkValue)
.commit() .commit()
@ -578,13 +675,13 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
val standardHttpsPortPref = SwitchPreferenceCompat(screen.context).apply { val standardHttpsPortPref = SwitchPreferenceCompat(screen.context).apply {
key = MDConstants.getStandardHttpsPreferenceKey(dexLang) key = MDConstants.getStandardHttpsPreferenceKey(dexLang)
title = "Use HTTPS port 443 only" title = helper.intl.standardHttpsPort
summary = summary = helper.intl.standardHttpsPortSummary
"Enable to only request image servers that use port 443. This allows users with stricter firewall restrictions to access MangaDex images"
setDefaultValue(false) setDefaultValue(false)
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean val checkValue = newValue as Boolean
preferences.edit() preferences.edit()
.putBoolean(MDConstants.getStandardHttpsPreferenceKey(dexLang), checkValue) .putBoolean(MDConstants.getStandardHttpsPreferenceKey(dexLang), checkValue)
.commit() .commit()
@ -593,9 +690,14 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
val contentRatingPref = MultiSelectListPreference(screen.context).apply { val contentRatingPref = MultiSelectListPreference(screen.context).apply {
key = MDConstants.getContentRatingPrefKey(dexLang) key = MDConstants.getContentRatingPrefKey(dexLang)
title = "Default content rating" title = helper.intl.standardContentRating
summary = "Show content with the selected ratings by default" summary = helper.intl.standardContentRatingSummary
entries = arrayOf("Safe", "Suggestive", "Erotica", "Pornographic") entries = arrayOf(
helper.intl.contentRatingSafe,
helper.intl.contentRatingSuggestive,
helper.intl.contentRatingErotica,
helper.intl.contentRatingPornographic
)
entryValues = arrayOf( entryValues = arrayOf(
MDConstants.contentRatingPrefValSafe, MDConstants.contentRatingPrefValSafe,
MDConstants.contentRatingPrefValSuggestive, MDConstants.contentRatingPrefValSuggestive,
@ -603,8 +705,10 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
MDConstants.contentRatingPrefValPornographic MDConstants.contentRatingPrefValPornographic
) )
setDefaultValue(MDConstants.contentRatingPrefDefaults) setDefaultValue(MDConstants.contentRatingPrefDefaults)
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Set<String> val checkValue = newValue as Set<String>
preferences.edit() preferences.edit()
.putStringSet(MDConstants.getContentRatingPrefKey(dexLang), checkValue) .putStringSet(MDConstants.getContentRatingPrefKey(dexLang), checkValue)
.commit() .commit()
@ -613,17 +717,23 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
val originalLanguagePref = MultiSelectListPreference(screen.context).apply { val originalLanguagePref = MultiSelectListPreference(screen.context).apply {
key = MDConstants.getOriginalLanguagePrefKey(dexLang) key = MDConstants.getOriginalLanguagePrefKey(dexLang)
title = "Filter original languages" title = helper.intl.filterOriginalLanguages
summary = "Only show content that was originally published in the selected languages in both latest and browse" summary = helper.intl.filterOriginalLanguagesSummary
entries = arrayOf("Japanese", "Chinese", "Korean") entries = arrayOf(
helper.intl.languageDisplayName(MangaDexIntl.JAPANESE),
helper.intl.languageDisplayName(MangaDexIntl.CHINESE),
helper.intl.languageDisplayName(MangaDexIntl.KOREAN)
)
entryValues = arrayOf( entryValues = arrayOf(
MDConstants.originalLanguagePrefValJapanese, MDConstants.originalLanguagePrefValJapanese,
MDConstants.originalLanguagePrefValChinese, MDConstants.originalLanguagePrefValChinese,
MDConstants.originalLanguagePrefValKorean MDConstants.originalLanguagePrefValKorean
) )
setDefaultValue(setOf<String>()) setDefaultValue(setOf<String>())
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Set<String> val checkValue = newValue as Set<String>
preferences.edit() preferences.edit()
.putStringSet(MDConstants.getOriginalLanguagePrefKey(dexLang), checkValue) .putStringSet(MDConstants.getOriginalLanguagePrefKey(dexLang), checkValue)
.commit() .commit()
@ -632,15 +742,15 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
val blockedGroupsPref = EditTextPreference(screen.context).apply { val blockedGroupsPref = EditTextPreference(screen.context).apply {
key = MDConstants.getBlockedGroupsPrefKey(dexLang) key = MDConstants.getBlockedGroupsPrefKey(dexLang)
title = "Block Groups by UUID" title = helper.intl.blockGroupByUuid
summary = "Chapters from blocked groups will not show up in Latest or Manga feed.\n" + summary = helper.intl.blockGroupByUuidSummary
"Enter as a Comma-separated list of group UUIDs"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val groupsBlocked = newValue.toString() val groupsBlocked = newValue.toString()
.split(",") .split(",")
.map { it.trim() } .map { it.trim() }
.filter { helper.containsUuid(it) } .filter { helper.containsUuid(it) }
.joinToString(separator = ", ") .joinToString(", ")
preferences.edit() preferences.edit()
.putString(MDConstants.getBlockedGroupsPrefKey(dexLang), groupsBlocked) .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 { val blockedUploaderPref = EditTextPreference(screen.context).apply {
key = MDConstants.getBlockedUploaderPrefKey(dexLang) key = MDConstants.getBlockedUploaderPrefKey(dexLang)
title = "Block Uploader by UUID" title = helper.intl.blockUploaderByUuid
summary = "Chapters from blocked users will not show up in Latest or Manga feed.\n" + summary = helper.intl.blockUploaderByUuidSummary
"Enter as a Comma-separated list of uploader UUIDs"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val uploaderBlocked = newValue.toString() val uploaderBlocked = newValue.toString()
.split(",") .split(",")
.map { it.trim() } .map { it.trim() }
.filter { helper.containsUuid(it) } .filter { helper.containsUuid(it) }
.joinToString(separator = ", ") .joinToString(", ")
preferences.edit() preferences.edit()
.putString(MDConstants.getBlockedUploaderPrefKey(dexLang), uploaderBlocked) .putString(MDConstants.getBlockedUploaderPrefKey(dexLang), uploaderBlocked)
@ -676,5 +786,13 @@ abstract class MangaDex(override val lang: String, val dexLang: String) :
} }
override fun getFilterList(): FilterList = 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())
}
} }

View File

@ -4,301 +4,376 @@ import android.content.SharedPreferences
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import okhttp3.HttpUrl import okhttp3.HttpUrl
import java.util.Locale
class MangaDexFilters { class MangaDexFilters {
internal fun getMDFilterList(preferences: SharedPreferences, dexLang: String): FilterList { internal fun getMDFilterList(preferences: SharedPreferences, dexLang: String, intl: MangaDexIntl): FilterList {
return FilterList( return FilterList(
OriginalLanguageList(getOriginalLanguage(preferences, dexLang)), HasAvailableChaptersFilter(intl),
ContentRatingList(getContentRating(preferences, dexLang)), OriginalLanguageList(intl, getOriginalLanguage(preferences, dexLang, intl)),
DemographicList(getDemographics()), ContentRatingList(intl, getContentRating(preferences, dexLang, intl)),
StatusList(getStatus()), DemographicList(intl, getDemographics(intl)),
SortFilter(sortableList.map { it.first }.toTypedArray()), StatusList(intl, getStatus(intl)),
TagList(getTags()), SortFilter(intl, getSortables(intl)),
TagInclusionMode(), TagsFilter(intl, getTagFilters(intl)),
TagExclusionMode(), TagList(intl.content, getContents(intl)),
HasAvailableChaptersFilter(), 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( val contentRatings = preferences.getStringSet(
MDConstants.getContentRatingPrefKey(dexLang), MDConstants.getContentRatingPrefKey(dexLang),
MDConstants.contentRatingPrefDefaults MDConstants.contentRatingPrefDefaults
) )
return listOf( return listOf(
ContentRating("Safe").apply { ContentRating(intl.contentRatingSafe, MDConstants.contentRatingPrefValSafe).apply {
state = contentRatings state = contentRatings
?.contains(MDConstants.contentRatingPrefValSafe) ?: true ?.contains(MDConstants.contentRatingPrefValSafe) ?: true
}, },
ContentRating("Suggestive").apply { ContentRating(intl.contentRatingSuggestive, MDConstants.contentRatingPrefValSuggestive).apply {
state = contentRatings state = contentRatings
?.contains(MDConstants.contentRatingPrefValSuggestive) ?: true ?.contains(MDConstants.contentRatingPrefValSuggestive) ?: true
}, },
ContentRating("Erotica").apply { ContentRating(intl.contentRatingErotica, MDConstants.contentRatingPrefValErotica).apply {
state = contentRatings state = contentRatings
?.contains(MDConstants.contentRatingPrefValErotica) ?: false ?.contains(MDConstants.contentRatingPrefValErotica) ?: false
}, },
ContentRating("Pornographic").apply { ContentRating(intl.contentRatingPornographic, MDConstants.contentRatingPrefValPornographic).apply {
state = contentRatings state = contentRatings
?.contains(MDConstants.contentRatingPrefValPornographic) ?: false ?.contains(MDConstants.contentRatingPrefValPornographic) ?: false
}, },
) )
} }
private class Demographic(name: String) : Filter.CheckBox(name) private class Demographic(name: String, val value: String) : Filter.CheckBox(name)
private class DemographicList(demographics: List<Demographic>) : private class DemographicList(intl: MangaDexIntl, demographics: List<Demographic>) :
Filter.Group<Demographic>("Publication Demographic", demographics) Filter.Group<Demographic>(intl.publicationDemographic, demographics),
UrlQueryFilter {
private fun getDemographics() = listOf( override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
Demographic("None"), state.forEach { demographic ->
Demographic("Shounen"), if (demographic.state) {
Demographic("Shoujo"), url.addQueryParameter("publicationDemographic[]", demographic.value)
Demographic("Seinen"), }
Demographic("Josei") }
) }
private class Status(name: String) : Filter.CheckBox(name)
private class StatusList(status: List<Status>) :
Filter.Group<Status>("Status", status)
private fun getStatus() = listOf(
Status("Ongoing"),
Status("Completed"),
Status("Hiatus"),
Status("Cancelled"),
)
private class ContentRating(name: String) : Filter.CheckBox(name)
private class ContentRatingList(contentRating: List<ContentRating>) :
Filter.Group<ContentRating>("Content Rating", contentRating)
private class OriginalLanguage(name: String, val isoCode: String) : Filter.CheckBox(name)
private class OriginalLanguageList(originalLanguage: List<OriginalLanguage>) :
Filter.Group<OriginalLanguage>("Original language", originalLanguage)
private fun getOriginalLanguage(preferences: SharedPreferences, dexLang: String): 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
},
)
} }
internal class Tag(val id: String, name: String) : Filter.TriState(name) private fun getDemographics(intl: MangaDexIntl) = listOf(
private class TagList(tags: List<Tag>) : Filter.Group<Tag>("Tags", tags) Demographic(intl.publicationDemographicNone, "none"),
Demographic(intl.publicationDemographicShounen, "shounen"),
// to get all tags from dex https://api.mangadex.org/manga/tag Demographic(intl.publicationDemographicShoujo, "shoujo"),
internal fun getTags() = listOf( Demographic(intl.publicationDemographicSeinen, "seinen"),
Tag("391b0423-d847-456f-aff0-8b0cfc03066b", "Action"), Demographic(intl.publicationDemographicJosei, "josei")
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 TagInclusionMode : private class Status(name: String, val value: String) : Filter.CheckBox(name)
Filter.Select<String>("Included tags mode", arrayOf("And", "Or"), 0) private class StatusList(intl: MangaDexIntl, status: List<Status>) :
Filter.Group<Status>(intl.status, status),
UrlQueryFilter {
private class TagExclusionMode : override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
Filter.Select<String>("Excluded tags mode", arrayOf("And", "Or"), 1) state.forEach { status ->
if (status.state) {
url.addQueryParameter("status[]", status.value)
}
}
}
}
val sortableList = listOf( private fun getStatus(intl: MangaDexIntl) = listOf(
Pair("Alphabetic", "title"), Status(intl.statusOngoing, "ongoing"),
Pair("Chapter uploaded at", "latestUploadedChapter"), Status(intl.statusCompleted, "completed"),
Pair("Number of follows", "followedCount"), Status(intl.statusHiatus, "hiatus"),
Pair("Manga created at", "createdAt"), Status(intl.statusCancelled, "cancelled"),
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 ContentRating(name: String, val value: String) : Filter.CheckBox(name)
private class ContentRatingList(intl: MangaDexIntl, contentRating: List<ContentRating>) :
Filter.Group<ContentRating>(intl.contentRating, contentRating),
UrlQueryFilter {
private class HasAvailableChaptersFilter : Filter.CheckBox("Has available chapters") override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
state.forEach { rating ->
if (rating.state) {
url.addQueryParameter("contentRating[]", rating.value)
}
}
}
}
internal fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList, dexLang: String): String { private class OriginalLanguage(name: String, val isoCode: String) : Filter.CheckBox(name)
url.apply { private class OriginalLanguageList(intl: MangaDexIntl, originalLanguage: List<OriginalLanguage>) :
// add filters Filter.Group<OriginalLanguage>(intl.originalLanguage, originalLanguage),
filters.forEach { filter -> UrlQueryFilter {
when (filter) {
is OriginalLanguageList -> { override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
filter.state.forEach { lang -> state.forEach { lang ->
if (lang.state) { if (lang.state) {
// dex has zh and zh-hk for chinese manhua // dex has zh and zh-hk for chinese manhua
if (lang.isoCode == MDConstants.originalLanguagePrefValChinese) { if (lang.isoCode == MDConstants.originalLanguagePrefValChinese) {
addQueryParameter( url.addQueryParameter(
"originalLanguage[]", "originalLanguage[]",
MDConstants.originalLanguagePrefValChineseHk MDConstants.originalLanguagePrefValChineseHk
) )
} }
addQueryParameter(
"originalLanguage[]", url.addQueryParameter("originalLanguage[]", lang.isoCode)
lang.isoCode }
}
}
}
private fun getOriginalLanguage(preferences: SharedPreferences, dexLang: String, intl: MangaDexIntl): List<OriginalLanguage> {
val originalLanguages = preferences.getStringSet(
MDConstants.getOriginalLanguagePrefKey(dexLang),
setOf()
)!!
return listOf(
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(collection: String, tags: List<Tag>) :
Filter.Group<Tag>(collection, tags),
UrlQueryFilter {
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)
} }
} }
is ContentRatingList -> { }
filter.state.forEach { rating -> }
if (rating.state) {
addQueryParameter( internal fun getContents(intl: MangaDexIntl): List<Tag> {
"contentRating[]", val tags = listOf(
rating.name.toLowerCase(Locale.US) 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> {
is DemographicList -> { val tags = listOf(
filter.state.forEach { demographic -> Tag("b11fda93-8f1d-4bef-b2ed-8803d3733170", intl.formatFourKoma),
if (demographic.state) { Tag("f4122d1c-3b44-44d0-9936-ff7502c39ad3", intl.formatAdaptation),
addQueryParameter( Tag("51d83883-4103-437c-b4b1-731cb73d786c", intl.formatAnthology),
"publicationDemographic[]", Tag("0a39b5a1-b235-4886-a747-1d05d216532d", intl.formatAwardWinning),
demographic.name.toLowerCase( Tag("b13b2a48-c720-44a9-9c77-39c9979373fb", intl.formatDoujinshi),
Locale.US 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
is StatusList -> { internal fun getThemes(intl: MangaDexIntl): List<Tag> {
filter.state.forEach { status -> val tags = listOf(
if (status.state) { Tag("e64f6742-c834-471d-8d72-dd51fc02b835", intl.themeAliens),
addQueryParameter( Tag("3de8c75d-8ee3-48ff-98ee-e20a65c86451", intl.themeAnimals),
"status[]", Tag("ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", intl.themeCooking),
status.name.toLowerCase( Tag("9ab53f92-3eed-4e9b-903a-917c86035ee3", intl.themeCrossdressing),
Locale.US 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)
} }
is SortFilter -> { }
if (filter.state != null) {
val query = sortableList[filter.state!!.index].second data class Sortable(val title: String, val value: String) {
val value = when (filter.state!!.ascending) { 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" true -> "asc"
false -> "desc" false -> "desc"
} }
addQueryParameter("order[$query]", value)
} url.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( private class HasAvailableChaptersFilter(intl: MangaDexIntl) :
"includedTagsMode", Filter.CheckBox(intl.hasAvailableChapters),
filter.values[filter.state].toUpperCase(Locale.US) 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)),
) )
}
is TagExclusionMode -> { internal fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList, dexLang: String): String {
addQueryParameter( filters.filterIsInstance<UrlQueryFilter>()
"excludedTagsMode", .forEach { filter -> filter.addQueryParameter(url, dexLang) }
filter.values[filter.state].toUpperCase(Locale.US)
)
}
is HasAvailableChaptersFilter -> {
if (filter.state) {
addQueryParameter("hasAvailableChapters", "true")
addQueryParameter("availableTranslatedLanguage[]", dexLang)
}
}
}
}
}
return url.toString() 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))
}
} }

View File

@ -15,14 +15,16 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.jsoup.parser.Parser import org.jsoup.parser.Parser
import uy.kohesive.injekt.api.get
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class MangaDexHelper() { class MangaDexHelper(private val lang: String) {
val mdFilters = MangaDexFilters() val mdFilters = MangaDexFilters()
@ -34,16 +36,26 @@ class MangaDexHelper() {
prettyPrint = true prettyPrint = true
} }
val intl = MangaDexIntl(lang)
/** /**
* Gets the UUID from the url * Gets the UUID from the url
*/ */
fun getUUIDFromUrl(url: String) = url.substringAfterLast("/") 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) = 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 * 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() fun getLatestChapterOffset(page: Int): String = (MDConstants.latestChapterLimit * (page - 1)).toString()
/** /**
* Remove markdown links as well as parse any html characters in description or * Remove any HTML characters in description or chapter name to actual
* chapter name to actual characters for example &hearts; will show * characters. For example &hearts; will show
*/ */
fun cleanString(string: String): String { fun cleanString(string: String): String {
val unescapedString = Parser.unescapeEntities(string, false) return Parser.unescapeEntities(string, false)
return unescapedString
.substringBefore("---") .substringBefore("---")
.replace(markdownLinksRegex, "$1") .replace(markdownLinksRegex, "$1")
.replace(markdownItalicBoldRegex, "$1")
.replace(markdownItalicRegex, "$1")
.trim() .trim()
} }
@ -109,6 +121,8 @@ class MangaDexHelper() {
.build() .build()
val markdownLinksRegex = "\\[([^]]+)\\]\\(([^)]+)\\)".toRegex() val markdownLinksRegex = "\\[([^]]+)\\]\\(([^)]+)\\)".toRegex()
val markdownItalicBoldRegex = "\\*+\\s*([^\\*]*)\\s*\\*+".toRegex()
val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex()
val titleSpecialCharactersRegex = "[^a-z0-9]+".toRegex() val titleSpecialCharactersRegex = "[^a-z0-9]+".toRegex()
@ -213,7 +227,12 @@ class MangaDexHelper() {
/** /**
* Create an SManga from json element with all details * 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 { try {
val attr = mangaDataDto.attributes val attr = mangaDataDto.attributes
@ -224,7 +243,7 @@ class MangaDexHelper() {
if (tempContentRating == null || tempContentRating.equals("safe", true)) { if (tempContentRating == null || tempContentRating.equals("safe", true)) {
null null
} else { } else {
"Content rating: " + tempContentRating.capitalize(Locale.US) intl.contentRatingGenre(tempContentRating)
} }
val dexLocale = Locale.forLanguageTag(lang) val dexLocale = Locale.forLanguageTag(lang)
@ -237,44 +256,53 @@ class MangaDexHelper() {
.capitalize(dexLocale) .capitalize(dexLocale)
) )
val authors = mangaDataDto.relationships.filter { relationshipDto -> val authors = mangaDataDto.relationships
.filter { relationshipDto ->
relationshipDto.type.equals(MDConstants.author, true) 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 coverFileName = mangaDataDto.relationships.firstOrNull { relationshipDto ->
relationshipDto.type.equals(MDConstants.coverArt, true)
}?.attributes?.fileName
// get tag list
val tags = mdFilters.getTags()
// map ids to tag names
val genreList = (
attr.tags
.map { it.id }
.map { dexId ->
tags.firstOrNull { it.id == dexId }
} }
.map { it?.name } + .mapNotNull { it.attributes!!.name }.distinct()
nonGenres
) val artists = mangaDataDto.relationships
.filter { it.isNullOrBlank().not() } .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 tags = mdFilters.getTags(intl)
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() val desc = attr.description.asMdMap()
return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang).apply { return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang).apply {
description = cleanString(desc[lang] ?: desc["en"] ?: "") description = cleanString(desc[lang] ?: desc["en"] ?: "")
author = authors.joinToString(", ") author = authors.joinToString(", ")
artist = artists.joinToString(", ") artist = artists.joinToString(", ")
status = getPublicationStatus(attr, chapters) status = getPublicationStatus(attr, chapters)
genre = genreList.joinToString(", ") genre = genreList
.filter(String::isNotEmpty)
.joinToString(", ")
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("MangaDex", "error parsing manga", e) Log.e("MangaDex", "error parsing manga", e)
throw(e) throw e
} }
} }
@ -295,14 +323,21 @@ class MangaDexHelper() {
.joinToString(" & ") .joinToString(" & ")
.ifEmpty { .ifEmpty {
// fall back to uploader name if no group // fall back to uploader name if no group
val users = chapterDataDto.relationships.filter { relationshipDto -> val users = chapterDataDto.relationships
.filter { relationshipDto ->
relationshipDto.type.equals( relationshipDto.type.equals(
MDConstants.uploader, MDConstants.uploader,
true true
) )
}.mapNotNull { it.attributes!!.username } }
users.joinToString(" & ", if (users.isNotEmpty()) "Uploaded by " else "") .mapNotNull { it.attributes!!.username }
}.ifEmpty { "No Group" } // "No Group" as final resort
users.joinToString(
" & ",
if (users.isNotEmpty()) intl.uploadedBy(users.toString()) else ""
)
}
.ifEmpty { intl.noGroup } // "No Group" as final resort
val chapterName = mutableListOf<String>() val chapterName = mutableListOf<String>()
// Build chapter name // Build chapter name
@ -347,7 +382,7 @@ class MangaDexHelper() {
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("MangaDex", "error parsing chapter", e) Log.e("MangaDex", "error parsing chapter", e)
throw(e) throw e
} }
} }

View File

@ -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"
}
}

View File

@ -59,6 +59,12 @@ data class MangaAttributesDto(
@Serializable @Serializable
data class TagDto( data class TagDto(
val id: String, val id: String,
val attributes: TagAttributesDto
)
@Serializable
data class TagAttributesDto(
val group: String
) )
fun JsonElement.asMdMap(): Map<String, String> { fun JsonElement.asMdMap(): Map<String, String> {