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'
pkgNameSuffix = 'all.mangadex'
extClass = '.MangaDexFactory'
extVersionCode = 158
extVersionCode = 159
isNsfw = true
}

View File

@ -46,7 +46,8 @@ object MDConstants {
return "${coverQualityPref}_$dexLang"
}
fun getCoverQualityPreferenceEntries() = arrayOf("Original", "Medium", "Low")
fun getCoverQualityPreferenceEntries(intl: MangaDexIntl) =
arrayOf(intl.coverQualityOriginal, intl.coverQualityMedium, intl.coverQualityLow)
fun getCoverQualityPreferenceEntryValues() = arrayOf("", ".512.jpg", ".256.jpg")
@ -76,10 +77,11 @@ object MDConstants {
}
private const val originalLanguagePref = "originalLanguage"
const val originalLanguagePrefValJapanese = "ja"
const val originalLanguagePrefValChinese = "zh"
const val originalLanguagePrefValJapanese = MangaDexIntl.JAPANESE
const val originalLanguagePrefValChinese = MangaDexIntl.CHINESE
const val originalLanguagePrefValChineseHk = "zh-hk"
const val originalLanguagePrefValKorean = "ko"
const val originalLanguagePrefValKorean = MangaDexIntl.KOREAN
val originalLanguagePrefDefaults = emptySet<String>()
fun getOriginalLanguagePrefKey(dexLang: String): String {
return "${originalLanguagePref}_$dexLang"
@ -100,4 +102,10 @@ object MDConstants {
fun getBlockedUploaderPrefKey(dexLang: String): String {
return "${blockedUploaderPref}_$dexLang"
}
const val tagGroupContent = "content"
const val tagGroupFormat = "format"
const val tagGroupGenre = "genre"
const val tagGroupTheme = "theme"
val tagGroupsOrder = arrayOf(tagGroupContent, tagGroupFormat, tagGroupGenre, tagGroupTheme)
}

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

View File

@ -4,301 +4,376 @@ import android.content.SharedPreferences
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import okhttp3.HttpUrl
import java.util.Locale
class MangaDexFilters {
internal fun getMDFilterList(preferences: SharedPreferences, dexLang: String): FilterList {
internal fun getMDFilterList(preferences: SharedPreferences, dexLang: String, intl: MangaDexIntl): FilterList {
return FilterList(
OriginalLanguageList(getOriginalLanguage(preferences, dexLang)),
ContentRatingList(getContentRating(preferences, dexLang)),
DemographicList(getDemographics()),
StatusList(getStatus()),
SortFilter(sortableList.map { it.first }.toTypedArray()),
TagList(getTags()),
TagInclusionMode(),
TagExclusionMode(),
HasAvailableChaptersFilter(),
HasAvailableChaptersFilter(intl),
OriginalLanguageList(intl, getOriginalLanguage(preferences, dexLang, intl)),
ContentRatingList(intl, getContentRating(preferences, dexLang, intl)),
DemographicList(intl, getDemographics(intl)),
StatusList(intl, getStatus(intl)),
SortFilter(intl, getSortables(intl)),
TagsFilter(intl, getTagFilters(intl)),
TagList(intl.content, getContents(intl)),
TagList(intl.format, getFormats(intl)),
TagList(intl.genre, getGenres(intl)),
TagList(intl.theme, getThemes(intl)),
)
}
private fun getContentRating(preferences: SharedPreferences, dexLang: String): List<ContentRating> {
private interface UrlQueryFilter {
fun addQueryParameter(url: HttpUrl.Builder, dexLang: String)
}
private fun getContentRating(preferences: SharedPreferences, dexLang: String, intl: MangaDexIntl): List<ContentRating> {
val contentRatings = preferences.getStringSet(
MDConstants.getContentRatingPrefKey(dexLang),
MDConstants.contentRatingPrefDefaults
)
return listOf(
ContentRating("Safe").apply {
ContentRating(intl.contentRatingSafe, MDConstants.contentRatingPrefValSafe).apply {
state = contentRatings
?.contains(MDConstants.contentRatingPrefValSafe) ?: true
},
ContentRating("Suggestive").apply {
ContentRating(intl.contentRatingSuggestive, MDConstants.contentRatingPrefValSuggestive).apply {
state = contentRatings
?.contains(MDConstants.contentRatingPrefValSuggestive) ?: true
},
ContentRating("Erotica").apply {
ContentRating(intl.contentRatingErotica, MDConstants.contentRatingPrefValErotica).apply {
state = contentRatings
?.contains(MDConstants.contentRatingPrefValErotica) ?: false
},
ContentRating("Pornographic").apply {
ContentRating(intl.contentRatingPornographic, MDConstants.contentRatingPrefValPornographic).apply {
state = contentRatings
?.contains(MDConstants.contentRatingPrefValPornographic) ?: false
},
)
}
private class Demographic(name: String) : Filter.CheckBox(name)
private class DemographicList(demographics: List<Demographic>) :
Filter.Group<Demographic>("Publication Demographic", demographics)
private class Demographic(name: String, val value: String) : Filter.CheckBox(name)
private class DemographicList(intl: MangaDexIntl, demographics: List<Demographic>) :
Filter.Group<Demographic>(intl.publicationDemographic, demographics),
UrlQueryFilter {
private fun getDemographics() = listOf(
Demographic("None"),
Demographic("Shounen"),
Demographic("Shoujo"),
Demographic("Seinen"),
Demographic("Josei")
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
state.forEach { demographic ->
if (demographic.state) {
url.addQueryParameter("publicationDemographic[]", demographic.value)
}
}
}
}
private fun getDemographics(intl: MangaDexIntl) = listOf(
Demographic(intl.publicationDemographicNone, "none"),
Demographic(intl.publicationDemographicShounen, "shounen"),
Demographic(intl.publicationDemographicShoujo, "shoujo"),
Demographic(intl.publicationDemographicSeinen, "seinen"),
Demographic(intl.publicationDemographicJosei, "josei")
)
private class Status(name: String) : Filter.CheckBox(name)
private class StatusList(status: List<Status>) :
Filter.Group<Status>("Status", status)
private class Status(name: String, val value: String) : Filter.CheckBox(name)
private class StatusList(intl: MangaDexIntl, status: List<Status>) :
Filter.Group<Status>(intl.status, status),
UrlQueryFilter {
private fun getStatus() = listOf(
Status("Ongoing"),
Status("Completed"),
Status("Hiatus"),
Status("Cancelled"),
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
state.forEach { status ->
if (status.state) {
url.addQueryParameter("status[]", status.value)
}
}
}
}
private fun getStatus(intl: MangaDexIntl) = listOf(
Status(intl.statusOngoing, "ongoing"),
Status(intl.statusCompleted, "completed"),
Status(intl.statusHiatus, "hiatus"),
Status(intl.statusCancelled, "cancelled"),
)
private class ContentRating(name: String) : Filter.CheckBox(name)
private class ContentRatingList(contentRating: List<ContentRating>) :
Filter.Group<ContentRating>("Content Rating", contentRating)
private class ContentRating(name: String, val value: String) : Filter.CheckBox(name)
private class ContentRatingList(intl: MangaDexIntl, contentRating: List<ContentRating>) :
Filter.Group<ContentRating>(intl.contentRating, contentRating),
UrlQueryFilter {
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
state.forEach { rating ->
if (rating.state) {
url.addQueryParameter("contentRating[]", rating.value)
}
}
}
}
private class OriginalLanguage(name: String, val isoCode: String) : Filter.CheckBox(name)
private class OriginalLanguageList(originalLanguage: List<OriginalLanguage>) :
Filter.Group<OriginalLanguage>("Original language", originalLanguage)
private class OriginalLanguageList(intl: MangaDexIntl, originalLanguage: List<OriginalLanguage>) :
Filter.Group<OriginalLanguage>(intl.originalLanguage, originalLanguage),
UrlQueryFilter {
private fun getOriginalLanguage(preferences: SharedPreferences, dexLang: String): List<OriginalLanguage> {
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
state.forEach { lang ->
if (lang.state) {
// dex has zh and zh-hk for chinese manhua
if (lang.isoCode == MDConstants.originalLanguagePrefValChinese) {
url.addQueryParameter(
"originalLanguage[]",
MDConstants.originalLanguagePrefValChineseHk
)
}
url.addQueryParameter("originalLanguage[]", lang.isoCode)
}
}
}
}
private fun getOriginalLanguage(preferences: SharedPreferences, dexLang: String, intl: MangaDexIntl): List<OriginalLanguage> {
val originalLanguages = preferences.getStringSet(
MDConstants.getOriginalLanguagePrefKey(dexLang),
setOf()
)
)!!
return listOf(
OriginalLanguage("Japanese (Manga)", "ja").apply {
state = originalLanguages
?.contains(MDConstants.originalLanguagePrefValJapanese) ?: false
},
OriginalLanguage("Chinese (Manhua)", "zh").apply {
state = originalLanguages
?.contains(MDConstants.originalLanguagePrefValChinese) ?: false
},
OriginalLanguage("Korean (Manhwa)", "ko").apply {
state = originalLanguages
?.contains(MDConstants.originalLanguagePrefValKorean) ?: false
},
OriginalLanguage(intl.originalLanguageFilterJapanese, MDConstants.originalLanguagePrefValJapanese)
.apply { state = MDConstants.originalLanguagePrefValJapanese in originalLanguages },
OriginalLanguage(intl.originalLanguageFilterChinese, MDConstants.originalLanguagePrefValChinese)
.apply { state = MDConstants.originalLanguagePrefValChinese in originalLanguages },
OriginalLanguage(intl.originalLanguageFilterKorean, MDConstants.originalLanguagePrefValKorean)
.apply { state = MDConstants.originalLanguagePrefValKorean in originalLanguages },
)
}
internal class Tag(val id: String, name: String) : Filter.TriState(name)
private class TagList(tags: List<Tag>) : Filter.Group<Tag>("Tags", tags)
// to get all tags from dex https://api.mangadex.org/manga/tag
internal fun getTags() = listOf(
Tag("391b0423-d847-456f-aff0-8b0cfc03066b", "Action"),
Tag("f4122d1c-3b44-44d0-9936-ff7502c39ad3", "Adaptation"),
Tag("87cc87cd-a395-47af-b27a-93258283bbc6", "Adventure"),
Tag("e64f6742-c834-471d-8d72-dd51fc02b835", "Aliens"),
Tag("3de8c75d-8ee3-48ff-98ee-e20a65c86451", "Animals"),
Tag("51d83883-4103-437c-b4b1-731cb73d786c", "Anthology"),
Tag("0a39b5a1-b235-4886-a747-1d05d216532d", "Award Winning"),
Tag("5920b825-4181-4a17-beeb-9918b0ff7a30", "Boy's Love"),
Tag("4d32cc48-9f00-4cca-9b5a-a839f0764984", "Comedy"),
Tag("ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", "Cooking"),
Tag("5ca48985-9a9d-4bd8-be29-80dc0303db72", "Crime"),
Tag("9ab53f92-3eed-4e9b-903a-917c86035ee3", "Crossdressing"),
Tag("da2d50ca-3018-4cc0-ac7a-6b7d472a29ea", "Delinquents"),
Tag("39730448-9a5f-48a2-85b0-a70db87b1233", "Demons"),
Tag("b13b2a48-c720-44a9-9c77-39c9979373fb", "Doujinshi"),
Tag("b9af3a63-f058-46de-a9a0-e0c13906197a", "Drama"),
Tag("7b2ce280-79ef-4c09-9b58-12b7c23a9b78", "Fan Colored"),
Tag("cdc58593-87dd-415e-bbc0-2ec27bf404cc", "Fantasy"),
Tag("b11fda93-8f1d-4bef-b2ed-8803d3733170", "4-Koma"),
Tag("f5ba408b-0e7a-484d-8d49-4e9125ac96de", "Full Color"),
Tag("2bd2e8d0-f146-434a-9b51-fc9ff2c5fe6a", "Genderswap"),
Tag("3bb26d85-09d5-4d2e-880c-c34b974339e9", "Ghosts"),
Tag("a3c67850-4684-404e-9b7f-c69850ee5da6", "Girl's Love"),
Tag("b29d6a3d-1569-4e7a-8caf-7557bc92cd5d", "Gore"),
Tag("fad12b5e-68ba-460e-b933-9ae8318f5b65", "Gyaru"),
Tag("aafb99c1-7f60-43fa-b75f-fc9502ce29c7", "Harem"),
Tag("33771934-028e-4cb3-8744-691e866a923e", "Historical"),
Tag("cdad7e68-1419-41dd-bdce-27753074a640", "Horror"),
Tag("5bd0e105-4481-44ca-b6e7-7544da56b1a3", "Incest"),
Tag("ace04997-f6bd-436e-b261-779182193d3d", "Isekai"),
Tag("2d1f5d56-a1e5-4d0d-a961-2193588b08ec", "Loli"),
Tag("3e2b8dae-350e-4ab8-a8ce-016e844b9f0d", "Long Strip"),
Tag("85daba54-a71c-4554-8a28-9901a8b0afad", "Mafia"),
Tag("a1f53773-c69a-4ce5-8cab-fffcd90b1565", "Magic"),
Tag("81c836c9-914a-4eca-981a-560dad663e73", "Magical Girls"),
Tag("799c202e-7daa-44eb-9cf7-8a3c0441531e", "Martial Arts"),
Tag("50880a9d-5440-4732-9afb-8f457127e836", "Mecha"),
Tag("c8cbe35b-1b2b-4a3f-9c37-db84c4514856", "Medical"),
Tag("ac72833b-c4e9-4878-b9db-6c8a4a99444a", "Military"),
Tag("dd1f77c5-dea9-4e2b-97ae-224af09caf99", "Monster Girls"),
Tag("36fd93ea-e8b8-445e-b836-358f02b3d33d", "Monsters"),
Tag("f42fbf9e-188a-447b-9fdc-f19dc1e4d685", "Music"),
Tag("ee968100-4191-4968-93d3-f82d72be7e46", "Mystery"),
Tag("489dd859-9b61-4c37-af75-5b18e88daafc", "Ninja"),
Tag("92d6d951-ca5e-429c-ac78-451071cbf064", "Office Workers"),
Tag("320831a8-4026-470b-94f6-8353740e6f04", "Official Colored"),
Tag("0234a31e-a729-4e28-9d6a-3f87c4966b9e", "Oneshot"),
Tag("b1e97889-25b4-4258-b28b-cd7f4d28ea9b", "Philosophical"),
Tag("df33b754-73a3-4c54-80e6-1a74a8058539", "Police"),
Tag("9467335a-1b83-4497-9231-765337a00b96", "Post-Apocalyptic"),
Tag("3b60b75c-a2d7-4860-ab56-05f391bb889c", "Psychological"),
Tag("0bc90acb-ccc1-44ca-a34a-b9f3a73259d0", "Reincarnation"),
Tag("65761a2a-415e-47f3-bef2-a9dababba7a6", "Reverse Harem"),
Tag("423e2eae-a7a2-4a8b-ac03-a8351462d71d", "Romance"),
Tag("81183756-1453-4c81-aa9e-f6e1b63be016", "Samurai"),
Tag("caaa44eb-cd40-4177-b930-79d3ef2afe87", "School Life"),
Tag("256c8bd9-4904-4360-bf4f-508a76d67183", "Sci-Fi"),
Tag("97893a4c-12af-4dac-b6be-0dffb353568e", "Sexual Violence"),
Tag("ddefd648-5140-4e5f-ba18-4eca4071d19b", "Shota"),
Tag("e5301a23-ebd9-49dd-a0cb-2add944c7fe9", "Slice of Life"),
Tag("69964a64-2f90-4d33-beeb-f3ed2875eb4c", "Sports"),
Tag("7064a261-a137-4d3a-8848-2d385de3a99c", "Superhero"),
Tag("eabc5b4c-6aff-42f3-b657-3e90cbd00b75", "Supernatural"),
Tag("5fff9cde-849c-4d78-aab0-0d52b2ee1d25", "Survival"),
Tag("07251805-a27e-4d59-b488-f0bfbec15168", "Thriller"),
Tag("292e862b-2d17-4062-90a2-0356caa4ae27", "Time Travel"),
Tag("f8f62932-27da-4fe4-8ee1-6779a8c5edba", "Tragedy"),
Tag("31932a7e-5b8e-49a6-9f12-2afa39dc544c", "Traditional Games"),
Tag("891cf039-b895-47f0-9229-bef4c96eccd4", "User Created"),
Tag("d7d1730f-6eb0-4ba6-9437-602cac38664c", "Vampires"),
Tag("9438db5a-7e2a-4ac0-b39e-e0d95a34b8a8", "Video Games"),
Tag("d14322ac-4d6f-4e9b-afd9-629d5f4d8a41", "Villainess"),
Tag("8c86611e-fab7-4986-9dec-d1a2f44acdd5", "Virtual Reality"),
Tag("e197df38-d0e7-43b5-9b09-2842d0c326dd", "Web Comic"),
Tag("acc803a4-c95a-4c22-86fc-eb6b582d82a2", "Wuxia"),
Tag("631ef465-9aba-4afb-b0fc-ea10efe274a8", "Zombies")
)
private class TagList(collection: String, tags: List<Tag>) :
Filter.Group<Tag>(collection, tags),
UrlQueryFilter {
private class TagInclusionMode :
Filter.Select<String>("Included tags mode", arrayOf("And", "Or"), 0)
private class TagExclusionMode :
Filter.Select<String>("Excluded tags mode", arrayOf("And", "Or"), 1)
val sortableList = listOf(
Pair("Alphabetic", "title"),
Pair("Chapter uploaded at", "latestUploadedChapter"),
Pair("Number of follows", "followedCount"),
Pair("Manga created at", "createdAt"),
Pair("Manga info updated at", "updatedAt"),
Pair("Relevant manga", "relevance"),
Pair("Year", "year")
)
class SortFilter(sortables: Array<String>) : Filter.Sort("Sort", sortables, Selection(2, false))
private class HasAvailableChaptersFilter : Filter.CheckBox("Has available chapters")
internal fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList, dexLang: String): String {
url.apply {
// add filters
filters.forEach { filter ->
when (filter) {
is OriginalLanguageList -> {
filter.state.forEach { lang ->
if (lang.state) {
// dex has zh and zh-hk for chinese manhua
if (lang.isoCode == MDConstants.originalLanguagePrefValChinese) {
addQueryParameter(
"originalLanguage[]",
MDConstants.originalLanguagePrefValChineseHk
)
}
addQueryParameter(
"originalLanguage[]",
lang.isoCode
)
}
}
}
is ContentRatingList -> {
filter.state.forEach { rating ->
if (rating.state) {
addQueryParameter(
"contentRating[]",
rating.name.toLowerCase(Locale.US)
)
}
}
}
is DemographicList -> {
filter.state.forEach { demographic ->
if (demographic.state) {
addQueryParameter(
"publicationDemographic[]",
demographic.name.toLowerCase(
Locale.US
)
)
}
}
}
is StatusList -> {
filter.state.forEach { status ->
if (status.state) {
addQueryParameter(
"status[]",
status.name.toLowerCase(
Locale.US
)
)
}
}
}
is SortFilter -> {
if (filter.state != null) {
val query = sortableList[filter.state!!.index].second
val value = when (filter.state!!.ascending) {
true -> "asc"
false -> "desc"
}
addQueryParameter("order[$query]", value)
}
}
is TagList -> {
filter.state.forEach { tag ->
if (tag.isIncluded()) {
addQueryParameter("includedTags[]", tag.id)
} else if (tag.isExcluded()) {
addQueryParameter("excludedTags[]", tag.id)
}
}
}
is TagInclusionMode -> {
addQueryParameter(
"includedTagsMode",
filter.values[filter.state].toUpperCase(Locale.US)
)
}
is TagExclusionMode -> {
addQueryParameter(
"excludedTagsMode",
filter.values[filter.state].toUpperCase(Locale.US)
)
}
is HasAvailableChaptersFilter -> {
if (filter.state) {
addQueryParameter("hasAvailableChapters", "true")
addQueryParameter("availableTranslatedLanguage[]", dexLang)
}
}
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
state.forEach { tag ->
if (tag.isIncluded()) {
url.addQueryParameter("includedTags[]", tag.id)
} else if (tag.isExcluded()) {
url.addQueryParameter("excludedTags[]", tag.id)
}
}
}
}
internal fun getContents(intl: MangaDexIntl): List<Tag> {
val tags = listOf(
Tag("b29d6a3d-1569-4e7a-8caf-7557bc92cd5d", intl.contentGore),
Tag("97893a4c-12af-4dac-b6be-0dffb353568e", intl.contentSexualViolence),
)
return tags.sortIfTranslated(intl)
}
internal fun getFormats(intl: MangaDexIntl): List<Tag> {
val tags = listOf(
Tag("b11fda93-8f1d-4bef-b2ed-8803d3733170", intl.formatFourKoma),
Tag("f4122d1c-3b44-44d0-9936-ff7502c39ad3", intl.formatAdaptation),
Tag("51d83883-4103-437c-b4b1-731cb73d786c", intl.formatAnthology),
Tag("0a39b5a1-b235-4886-a747-1d05d216532d", intl.formatAwardWinning),
Tag("b13b2a48-c720-44a9-9c77-39c9979373fb", intl.formatDoujinshi),
Tag("7b2ce280-79ef-4c09-9b58-12b7c23a9b78", intl.formatFanColored),
Tag("f5ba408b-0e7a-484d-8d49-4e9125ac96de", intl.formatFullColor),
Tag("3e2b8dae-350e-4ab8-a8ce-016e844b9f0d", intl.formatLongStrip),
Tag("320831a8-4026-470b-94f6-8353740e6f04", intl.formatOfficialColored),
Tag("0234a31e-a729-4e28-9d6a-3f87c4966b9e", intl.formatOneshot),
Tag("891cf039-b895-47f0-9229-bef4c96eccd4", intl.formatUserCreated),
Tag("e197df38-d0e7-43b5-9b09-2842d0c326dd", intl.formatWebComic),
)
return tags.sortIfTranslated(intl)
}
internal fun getGenres(intl: MangaDexIntl): List<Tag> {
val tags = listOf(
Tag("391b0423-d847-456f-aff0-8b0cfc03066b", intl.genreAction),
Tag("87cc87cd-a395-47af-b27a-93258283bbc6", intl.genreAdventure),
Tag("5920b825-4181-4a17-beeb-9918b0ff7a30", intl.genreBoysLove),
Tag("4d32cc48-9f00-4cca-9b5a-a839f0764984", intl.genreComedy),
Tag("5ca48985-9a9d-4bd8-be29-80dc0303db72", intl.genreCrime),
Tag("b9af3a63-f058-46de-a9a0-e0c13906197a", intl.genreDrama),
Tag("cdc58593-87dd-415e-bbc0-2ec27bf404cc", intl.genreFantasy),
Tag("a3c67850-4684-404e-9b7f-c69850ee5da6", intl.genreGirlsLove),
Tag("33771934-028e-4cb3-8744-691e866a923e", intl.genreHistorical),
Tag("cdad7e68-1419-41dd-bdce-27753074a640", intl.genreHorror),
Tag("ace04997-f6bd-436e-b261-779182193d3d", intl.genreIsekai),
Tag("81c836c9-914a-4eca-981a-560dad663e73", intl.genreMagicalGirls),
Tag("50880a9d-5440-4732-9afb-8f457127e836", intl.genreMecha),
Tag("c8cbe35b-1b2b-4a3f-9c37-db84c4514856", intl.genreMedical),
Tag("ee968100-4191-4968-93d3-f82d72be7e46", intl.genreMystery),
Tag("b1e97889-25b4-4258-b28b-cd7f4d28ea9b", intl.genrePhilosophical),
Tag("423e2eae-a7a2-4a8b-ac03-a8351462d71d", intl.genreRomance),
Tag("256c8bd9-4904-4360-bf4f-508a76d67183", intl.genreSciFi),
Tag("e5301a23-ebd9-49dd-a0cb-2add944c7fe9", intl.genreSliceOfLife),
Tag("69964a64-2f90-4d33-beeb-f3ed2875eb4c", intl.genreSports),
Tag("7064a261-a137-4d3a-8848-2d385de3a99c", intl.genreSuperhero),
Tag("07251805-a27e-4d59-b488-f0bfbec15168", intl.genreThriller),
Tag("f8f62932-27da-4fe4-8ee1-6779a8c5edba", intl.genreTragedy),
Tag("acc803a4-c95a-4c22-86fc-eb6b582d82a2", intl.genreWuxia),
)
return tags.sortIfTranslated(intl)
}
// to get all tags from dex https://api.mangadex.org/manga/tag
internal fun getThemes(intl: MangaDexIntl): List<Tag> {
val tags = listOf(
Tag("e64f6742-c834-471d-8d72-dd51fc02b835", intl.themeAliens),
Tag("3de8c75d-8ee3-48ff-98ee-e20a65c86451", intl.themeAnimals),
Tag("ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", intl.themeCooking),
Tag("9ab53f92-3eed-4e9b-903a-917c86035ee3", intl.themeCrossdressing),
Tag("da2d50ca-3018-4cc0-ac7a-6b7d472a29ea", intl.themeDelinquents),
Tag("39730448-9a5f-48a2-85b0-a70db87b1233", intl.themeDemons),
Tag("2bd2e8d0-f146-434a-9b51-fc9ff2c5fe6a", intl.themeGenderSwap),
Tag("3bb26d85-09d5-4d2e-880c-c34b974339e9", intl.themeGhosts),
Tag("fad12b5e-68ba-460e-b933-9ae8318f5b65", intl.themeGyaru),
Tag("aafb99c1-7f60-43fa-b75f-fc9502ce29c7", intl.themeHarem),
Tag("5bd0e105-4481-44ca-b6e7-7544da56b1a3", intl.themeIncest),
Tag("2d1f5d56-a1e5-4d0d-a961-2193588b08ec", intl.themeLoli),
Tag("85daba54-a71c-4554-8a28-9901a8b0afad", intl.themeMafia),
Tag("a1f53773-c69a-4ce5-8cab-fffcd90b1565", intl.themeMagic),
Tag("799c202e-7daa-44eb-9cf7-8a3c0441531e", intl.themeMartialArts),
Tag("ac72833b-c4e9-4878-b9db-6c8a4a99444a", intl.themeMilitary),
Tag("dd1f77c5-dea9-4e2b-97ae-224af09caf99", intl.themeMonsterGirls),
Tag("36fd93ea-e8b8-445e-b836-358f02b3d33d", intl.themeMonsters),
Tag("f42fbf9e-188a-447b-9fdc-f19dc1e4d685", intl.themeMusic),
Tag("489dd859-9b61-4c37-af75-5b18e88daafc", intl.themeNinja),
Tag("92d6d951-ca5e-429c-ac78-451071cbf064", intl.themeOfficeWorkers),
Tag("df33b754-73a3-4c54-80e6-1a74a8058539", intl.themePolice),
Tag("9467335a-1b83-4497-9231-765337a00b96", intl.themePostApocalyptic),
Tag("3b60b75c-a2d7-4860-ab56-05f391bb889c", intl.themePsychological),
Tag("0bc90acb-ccc1-44ca-a34a-b9f3a73259d0", intl.themeReincarnation),
Tag("65761a2a-415e-47f3-bef2-a9dababba7a6", intl.themeReverseHarem),
Tag("81183756-1453-4c81-aa9e-f6e1b63be016", intl.themeSamurai),
Tag("caaa44eb-cd40-4177-b930-79d3ef2afe87", intl.themeSchoolLife),
Tag("ddefd648-5140-4e5f-ba18-4eca4071d19b", intl.themeShota),
Tag("eabc5b4c-6aff-42f3-b657-3e90cbd00b75", intl.themeSupernatural),
Tag("5fff9cde-849c-4d78-aab0-0d52b2ee1d25", intl.themeSurvival),
Tag("292e862b-2d17-4062-90a2-0356caa4ae27", intl.themeTimeTravel),
Tag("31932a7e-5b8e-49a6-9f12-2afa39dc544c", intl.themeTraditionalGames),
Tag("d7d1730f-6eb0-4ba6-9437-602cac38664c", intl.themeVampires),
Tag("9438db5a-7e2a-4ac0-b39e-e0d95a34b8a8", intl.themeVideoGames),
Tag("d14322ac-4d6f-4e9b-afd9-629d5f4d8a41", intl.themeVillainess),
Tag("8c86611e-fab7-4986-9dec-d1a2f44acdd5", intl.themeVirtualReality),
Tag("631ef465-9aba-4afb-b0fc-ea10efe274a8", intl.themeZombies)
)
return tags.sortIfTranslated(intl)
}
internal fun getTags(intl: MangaDexIntl): List<Tag> {
return getContents(intl) + getFormats(intl) + getGenres(intl) + getThemes(intl)
}
private data class TagMode(val title: String, val value: String) {
override fun toString(): String = title
}
private fun getTagModes(intl: MangaDexIntl) = arrayOf(
TagMode(intl.modeAnd, "AND"),
TagMode(intl.modeOr, "OR")
)
private class TagInclusionMode(intl: MangaDexIntl, modes: Array<TagMode>) :
Filter.Select<TagMode>(intl.includedTagsMode, modes, 0),
UrlQueryFilter {
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
url.addQueryParameter("includedTagsMode", values[state].value)
}
}
private class TagExclusionMode(intl: MangaDexIntl, modes: Array<TagMode>) :
Filter.Select<TagMode>(intl.excludedTagsMode, modes, 1),
UrlQueryFilter {
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
url.addQueryParameter("excludedTagsMode", values[state].value)
}
}
data class Sortable(val title: String, val value: String) {
override fun toString(): String = title
}
private fun getSortables(intl: MangaDexIntl) = arrayOf(
Sortable(intl.sortAlphabetic, "title"),
Sortable(intl.sortChapterUploadedAt, "latestUploadedChapter"),
Sortable(intl.sortNumberOfFollows, "followedCount"),
Sortable(intl.sortContentCreatedAt, "createdAt"),
Sortable(intl.sortContentInfoUpdatedAt, "updatedAt"),
Sortable(intl.sortRelevance, "relevance"),
Sortable(intl.sortYear, "year")
)
class SortFilter(intl: MangaDexIntl, private val sortables: Array<Sortable>) :
Filter.Sort(
intl.sort,
sortables.map(Sortable::title).toTypedArray(),
Selection(2, false)
),
UrlQueryFilter {
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
if (state != null) {
val query = sortables[state!!.index].value
val value = when (state!!.ascending) {
true -> "asc"
false -> "desc"
}
url.addQueryParameter("order[$query]", value)
}
}
}
private class HasAvailableChaptersFilter(intl: MangaDexIntl) :
Filter.CheckBox(intl.hasAvailableChapters),
UrlQueryFilter {
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
if (state) {
url.addQueryParameter("hasAvailableChapters", "true")
url.addQueryParameter("availableTranslatedLanguage[]", dexLang)
}
}
}
private class TagsFilter(intl: MangaDexIntl, innerFilters: FilterList) :
Filter.Group<Filter<*>>(intl.tags, innerFilters),
UrlQueryFilter {
override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) {
state.filterIsInstance<UrlQueryFilter>()
.forEach { filter -> filter.addQueryParameter(url, dexLang) }
}
}
private fun getTagFilters(intl: MangaDexIntl): FilterList = FilterList(
TagInclusionMode(intl, getTagModes(intl)),
TagExclusionMode(intl, getTagModes(intl)),
)
internal fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList, dexLang: String): String {
filters.filterIsInstance<UrlQueryFilter>()
.forEach { filter -> filter.addQueryParameter(url, dexLang) }
return url.toString()
}
private fun List<Tag>.sortIfTranslated(intl: MangaDexIntl): List<Tag> = apply {
if (intl.availableLang == MangaDexIntl.ENGLISH) {
return this
}
return sortedWith(compareBy(intl.collator, Tag::name))
}
}

View File

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

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
data class TagDto(
val id: String,
val attributes: TagAttributesDto
)
@Serializable
data class TagAttributesDto(
val group: String
)
fun JsonElement.asMdMap(): Map<String, String> {