diff --git a/REMOVED_SOURCES.md b/REMOVED_SOURCES.md index 54ef58300..d56c5dfff 100644 --- a/REMOVED_SOURCES.md +++ b/REMOVED_SOURCES.md @@ -66,3 +66,13 @@ Here is a list of known sources that were removed. - SetsuScans https://github.com/tachiyomiorg/tachiyomi-extensions/issues/11040 - ShinobiScans https://github.com/tachiyomiorg/tachiyomi-extensions/issues/14457 - XXX Yaoi https://github.com/tachiyomiorg/tachiyomi-extensions/issues/9535 + +### Requested removal by copyright holders + +By request of [Kakao Entertainment](https://www.kakaoent.com/): + +- 1st Kiss-Manga +- Bato.to +- Mangadex +- NewToki / ManaToki +- S2Manga diff --git a/multisrc/overrides/madara/s2manga/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/madara/s2manga/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 78aed8f9d..000000000 Binary files a/multisrc/overrides/madara/s2manga/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/multisrc/overrides/madara/s2manga/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/madara/s2manga/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 57b0e935b..000000000 Binary files a/multisrc/overrides/madara/s2manga/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/multisrc/overrides/madara/s2manga/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/madara/s2manga/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 60ac860b9..000000000 Binary files a/multisrc/overrides/madara/s2manga/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/multisrc/overrides/madara/s2manga/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/madara/s2manga/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 6201c85ab..000000000 Binary files a/multisrc/overrides/madara/s2manga/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/multisrc/overrides/madara/s2manga/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/madara/s2manga/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index ed057db6b..000000000 Binary files a/multisrc/overrides/madara/s2manga/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/multisrc/overrides/madara/s2manga/res/web_hi_res_512.png b/multisrc/overrides/madara/s2manga/res/web_hi_res_512.png deleted file mode 100644 index c39e43b09..000000000 Binary files a/multisrc/overrides/madara/s2manga/res/web_hi_res_512.png and /dev/null differ diff --git a/multisrc/overrides/madara/s2manga/src/S2Manga.kt b/multisrc/overrides/madara/s2manga/src/S2Manga.kt deleted file mode 100644 index 017949318..000000000 --- a/multisrc/overrides/madara/s2manga/src/S2Manga.kt +++ /dev/null @@ -1,11 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.s2manga - -import eu.kanade.tachiyomi.multisrc.madara.Madara - -class S2Manga : Madara("S2Manga", "https://www.s2manga.com", "en") { - - override fun headersBuilder() = super.headersBuilder() - .add("Referer", "$baseUrl/") - - override val pageListParseSelector = "div.page-break img[src*=\"https\"]" -} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt index 5e347bac4..8e9be4445 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt @@ -422,7 +422,6 @@ class MadaraGenerator : ThemeSourceGenerator { SingleLang("ROG Mangás", "https://rogmangas.com", "pt-BR", pkgName = "mangasoverall", className = "RogMangas", overrideVersionCode = 1), SingleLang("Romantik Manga", "https://romantikmanga.com", "tr"), SingleLang("Rüya Manga", "https://www.ruyamanga.com", "tr", className = "RuyaManga", overrideVersionCode = 1), - SingleLang("S2Manga", "https://www.s2manga.com", "en", overrideVersionCode = 2), SingleLang("Sagrado Império da Britannia", "https://imperiodabritannia.com", "pt-BR", className = "ImperioDaBritannia"), SingleLang("SamuraiScan", "https://samuraiscan.com", "es", overrideVersionCode = 3), SingleLang("Sawamics", "https://sawamics.com", "en"), diff --git a/settings.gradle.kts b/settings.gradle.kts index 3e0631f9e..e1dbaa7e2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,7 +19,7 @@ if (System.getenv("CI") == null || System.getenv("CI_MODULE_GEN") == "true") { */ loadAllIndividualExtensions() loadAllGeneratedMultisrcExtensions() - // loadIndividualExtension("all", "mangadex") + // loadIndividualExtension("all", "komga") // loadGeneratedMultisrcExtension("en", "guya") } else { // Running in CI (GitHub Actions) diff --git a/src/all/batoto/AndroidManifest.xml b/src/all/batoto/AndroidManifest.xml deleted file mode 100644 index c4ca310dd..000000000 --- a/src/all/batoto/AndroidManifest.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/all/batoto/CHANGELOG.md b/src/all/batoto/CHANGELOG.md deleted file mode 100644 index e240bf4e9..000000000 --- a/src/all/batoto/CHANGELOG.md +++ /dev/null @@ -1,201 +0,0 @@ -## 1.3.30 - -### Refactor - -* Replace CryptoJS with Native Kotlin Functions -* Remove QuickJS dependency - -## 1.3.29 - -### Refactor - -* Cleanup pageListParse function -* Replace Duktape with QuickJS - -## 1.3.28 - -### Features - -* Add mirror `batocc.com` -* Add mirror `batotwo.com` -* Add mirror `mangatoto.net` -* Add mirror `mangatoto.org` -* Add mirror `mycordant.co.uk` -* Add mirror `dto.to` -* Add mirror `hto.to` -* Add mirror `mto.to` -* Add mirror `wto.to` -* Remove mirror `mycdhands.com` - -## 1.3.27 - -### Features - -* Change default popular sort by `Most Views Totally` - -## 1.3.26 - -### Fix - -* Update author and artist parsing - -## 1.3.25 - -### Fix - -* Status parsing -* Artist name parsing - -## 1.3.24 - -### Fix - -* Bump versions for individual extension with URL handler activities - -## 1.2.23 - -### Fix - -* Update pageListParse logic to handle website changes - -## 1.2.22 - -### Features - -* Add `CHANGELOG.md` & `README.md` - -## 1.2.21 - -### Fix - -* Update lang codes - -## 1.2.20 - -### Features - -* Rework of search - -## 1.2.19 - -### Features - -* Support for alternative chapter list -* Personal lists filter - -## 1.2.18 - -### Features - -* Utils lists filter -* Letter matching filter - -## 1.2.17 - -### Features - -* Add mirror `mycdhands.com` - -## 1.2.16 - -### Features - -* Mirror support -* URL intent updates - -## 1.2.15 - -### Fix - -* Manga description - -## 1.2.14 - -### Features - -* Escape entities - -## 1.2.13 - -### Refactor - -* Replace Gson with kotlinx.serialization - -## 1.2.12 - -### Fix - -* Infinity search - -## 1.2.11 - -### Fix - -* No search result - -## 1.2.10 - -### Features - -* Support for URL intent -* Updated filters - -## 1.2.9 - -### Fix - -* Chapter parsing - -## 1.2.8 - -### Features - -* More chapter filtering - -## 1.2.7 - -### Fix - -* Language filtering in latest -* Parsing of seconds - -## 1.2.6 - -### Features - -* Scanlator support - -### Fix - -* Date parsing - -## 1.2.5 - -### Features - -* Update supported Language list - -## 1.2.4 - -### Features - -* Support for excluding genres - -## 1.2.3 - -### Fix - -* Typo in some genres - -## 1.2.2 - -### Features - -* Reworked filter option - -## 1.2.1 - -### Features - -* Conversion from Emerald to Bato.to -* First version diff --git a/src/all/batoto/README.md b/src/all/batoto/README.md deleted file mode 100644 index 8f7c39cc5..000000000 --- a/src/all/batoto/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Bato.to - -Table of Content -- [FAQ](#FAQ) - - [Why are there Manga of diffrent languge than the selected one in Personal & Utils lists?](#why-are-there-manga-of-diffrent-languge-than-the-selected-one-in-personal--utils-lists) - - [Bato.to is not loading anything?](#batoto-is-not-loading-anything) - -[Uncomment this if needed; and replace ( and ) with ( and )]: <> (- [Guides](#Guides)) - -Don't find the question you are look for go check out our general FAQs and Guides over at [Extension FAQ](https://tachiyomi.org/help/faq/#extensions) or [Getting Started](https://tachiyomi.org/help/guides/getting-started/#installation) - -## FAQ - -### Why are there Manga of diffrent languge than the selected one in Personal & Utils lists? -Personol & Utils lists have no way to difritiate between langueges. - -### Bato.to is not loading anything? -Bato.to get blocked by some ISPs, try using a diffrent mirror of Bato.to from the settings. - -[Uncomment this if needed]: <> (## Guides) diff --git a/src/all/batoto/build.gradle b/src/all/batoto/build.gradle deleted file mode 100644 index 3589fc1cd..000000000 --- a/src/all/batoto/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlinx-serialization' - -ext { - extName = 'Bato.to' - pkgNameSuffix = 'all.batoto' - extClass = '.BatoToFactory' - extVersionCode = 32 - isNsfw = true -} - -apply from: "$rootDir/common.gradle" - -dependencies { - implementation(project(':lib-cryptoaes')) -} diff --git a/src/all/batoto/res/mipmap-hdpi/ic_launcher.png b/src/all/batoto/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 0c371b3be..000000000 Binary files a/src/all/batoto/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src/all/batoto/res/mipmap-mdpi/ic_launcher.png b/src/all/batoto/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 718a3b8b1..000000000 Binary files a/src/all/batoto/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src/all/batoto/res/mipmap-xhdpi/ic_launcher.png b/src/all/batoto/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index e705165fc..000000000 Binary files a/src/all/batoto/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src/all/batoto/res/mipmap-xxhdpi/ic_launcher.png b/src/all/batoto/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 7f9d6a195..000000000 Binary files a/src/all/batoto/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/all/batoto/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/batoto/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index d7d91a0db..000000000 Binary files a/src/all/batoto/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/all/batoto/res/web_hi_res_512.png b/src/all/batoto/res/web_hi_res_512.png deleted file mode 100644 index 973b65efa..000000000 Binary files a/src/all/batoto/res/web_hi_res_512.png and /dev/null differ diff --git a/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoTo.kt b/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoTo.kt deleted file mode 100644 index 8d04c0e6b..000000000 --- a/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoTo.kt +++ /dev/null @@ -1,974 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.batoto - -import android.app.Application -import android.content.SharedPreferences -import androidx.preference.CheckBoxPreference -import androidx.preference.ListPreference -import androidx.preference.PreferenceScreen -import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES -import eu.kanade.tachiyomi.lib.cryptoaes.Deobfuscator -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.POST -import eu.kanade.tachiyomi.network.asObservableSuccess -import eu.kanade.tachiyomi.source.ConfigurableSource -import eu.kanade.tachiyomi.source.model.Filter -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.MangasPage -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.ParsedHttpSource -import eu.kanade.tachiyomi.util.asJsoup -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import okhttp3.FormBody -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import org.jsoup.Jsoup -import org.jsoup.nodes.Document -import org.jsoup.nodes.Element -import org.jsoup.parser.Parser -import rx.Observable -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Locale -import java.util.concurrent.TimeUnit - -open class BatoTo( - final override val lang: String, - private val siteLang: String, -) : ConfigurableSource, ParsedHttpSource() { - - private val preferences: SharedPreferences by lazy { - Injekt.get().getSharedPreferences("source_$id", 0x0000) - } - - override val name: String = "Bato.to" - override val baseUrl: String = getMirrorPref()!! - override val id: Long = when (lang) { - "zh-Hans" -> 2818874445640189582 - "zh-Hant" -> 38886079663327225 - "ro-MD" -> 8871355786189601023 - else -> super.id - } - - override fun setupPreferenceScreen(screen: PreferenceScreen) { - val mirrorPref = ListPreference(screen.context).apply { - key = "${MIRROR_PREF_KEY}_$lang" - title = MIRROR_PREF_TITLE - entries = MIRROR_PREF_ENTRIES - entryValues = MIRROR_PREF_ENTRY_VALUES - setDefaultValue(MIRROR_PREF_DEFAULT_VALUE) - summary = "%s" - - setOnPreferenceChangeListener { _, newValue -> - val selected = newValue as String - val index = findIndexOfValue(selected) - val entry = entryValues[index] as String - preferences.edit().putString("${MIRROR_PREF_KEY}_$lang", entry).commit() - } - } - val altChapterListPref = CheckBoxPreference(screen.context).apply { - key = "${ALT_CHAPTER_LIST_PREF_KEY}_$lang" - title = ALT_CHAPTER_LIST_PREF_TITLE - summary = ALT_CHAPTER_LIST_PREF_SUMMARY - setDefaultValue(ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE) - - setOnPreferenceChangeListener { _, newValue -> - val checkValue = newValue as Boolean - preferences.edit().putBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", checkValue).commit() - } - } - screen.addPreference(mirrorPref) - screen.addPreference(altChapterListPref) - } - - private fun getMirrorPref(): String? = preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE) - private fun getAltChapterListPref(): Boolean = preferences.getBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE) - - override val supportsLatest = true - private val json: Json by injectLazy() - override val client: OkHttpClient = network.cloudflareClient.newBuilder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .build() - - override fun latestUpdatesRequest(page: Int): Request { - return GET("$baseUrl/browse?langs=$siteLang&sort=update&page=$page") - } - - override fun latestUpdatesSelector(): String { - return when (siteLang) { - "" -> "div#series-list div.col" - "en" -> "div#series-list div.col.no-flag" - else -> "div#series-list div.col:has([data-lang=\"$siteLang\"])" - } - } - - override fun latestUpdatesFromElement(element: Element): SManga { - val manga = SManga.create() - val item = element.select("a.item-cover") - val imgurl = item.select("img").attr("abs:src") - manga.setUrlWithoutDomain(item.attr("href")) - manga.title = element.select("a.item-title").text().removeEntities() - manga.thumbnail_url = imgurl - return manga - } - - override fun latestUpdatesNextPageSelector() = "div#mainer nav.d-none .pagination .page-item:last-of-type:not(.disabled)" - - override fun popularMangaRequest(page: Int): Request { - return GET("$baseUrl/browse?langs=$siteLang&sort=views_a&page=$page") - } - - override fun popularMangaSelector() = latestUpdatesSelector() - - override fun popularMangaFromElement(element: Element) = latestUpdatesFromElement(element) - - override fun popularMangaNextPageSelector() = latestUpdatesNextPageSelector() - - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { - return when { - query.startsWith("ID:") -> { - val id = query.substringAfter("ID:") - client.newCall(GET("$baseUrl/series/$id", headers)).asObservableSuccess() - .map { response -> - queryIDParse(response) - } - } - query.isNotBlank() -> { - val url = "$baseUrl/search".toHttpUrl().newBuilder() - .addQueryParameter("word", query) - .addQueryParameter("page", page.toString()) - filters.forEach { filter -> - when (filter) { - is LetterFilter -> { - if (filter.state == 1) { - url.addQueryParameter("mode", "letter") - } - } - else -> { /* Do Nothing */ } - } - } - client.newCall(GET(url.build().toString(), headers)).asObservableSuccess() - .map { response -> - queryParse(response) - } - } - else -> { - val url = "$baseUrl/browse".toHttpUrlOrNull()!!.newBuilder() - var min = "" - var max = "" - filters.forEach { filter -> - when (filter) { - is UtilsFilter -> { - if (filter.state != 0) { - val filterUrl = "$baseUrl/_utils/comic-list?type=${filter.selected}" - return client.newCall(GET(filterUrl, headers)).asObservableSuccess() - .map { response -> - queryUtilsParse(response) - } - } - } - is HistoryFilter -> { - if (filter.state != 0) { - val filterUrl = "$baseUrl/ajax.my.${filter.selected}.paging" - return client.newCall(POST(filterUrl, headers, formBuilder().build())).asObservableSuccess() - .map { response -> - queryHistoryParse(response) - } - } - } - is LangGroupFilter -> { - if (filter.selected.isEmpty()) { - url.addQueryParameter("langs", siteLang) - } else { - val selection = "${filter.selected.joinToString(",")},$siteLang" - url.addQueryParameter("langs", selection) - } - } - is GenreGroupFilter -> { - with(filter) { - url.addQueryParameter( - "genres", - included.joinToString(",") + "|" + excluded.joinToString(","), - ) - } - } - is StatusFilter -> url.addQueryParameter("release", filter.selected) - is SortFilter -> { - if (filter.state != null) { - val sort = getSortFilter()[filter.state!!.index].value - val value = when (filter.state!!.ascending) { - true -> "az" - false -> "za" - } - url.addQueryParameter("sort", "$sort.$value") - } - } - is OriginGroupFilter -> { - if (filter.selected.isNotEmpty()) { - url.addQueryParameter("origs", filter.selected.joinToString(",")) - } - } - is MinChapterTextFilter -> min = filter.state - is MaxChapterTextFilter -> max = filter.state - else -> { /* Do Nothing */ } - } - } - url.addQueryParameter("page", page.toString()) - - if (max.isNotEmpty() or min.isNotEmpty()) { - url.addQueryParameter("chapters", "$min-$max") - } - - client.newCall(GET(url.build().toString(), headers)).asObservableSuccess() - .map { response -> - queryParse(response) - } - } - } - } - - private fun queryIDParse(response: Response): MangasPage { - val document = response.asJsoup() - val infoElement = document.select("div#mainer div.container-fluid") - val manga = SManga.create() - manga.title = infoElement.select("h3").text().removeEntities() - manga.thumbnail_url = document.select("div.attr-cover img") - .attr("abs:src") - manga.url = infoElement.select("h3 a").attr("abs:href") - return MangasPage(listOf(manga), false) - } - - private fun queryParse(response: Response): MangasPage { - val document = response.asJsoup() - val mangas = document.select(latestUpdatesSelector()) - .map { element -> latestUpdatesFromElement(element) } - val nextPage = document.select(latestUpdatesNextPageSelector()).first() != null - return MangasPage(mangas, nextPage) - } - - private fun queryUtilsParse(response: Response): MangasPage { - val document = response.asJsoup() - val mangas = document.select("tbody > tr") - .map { element -> searchUtilsFromElement(element) } - return MangasPage(mangas, false) - } - - private fun queryHistoryParse(response: Response): MangasPage { - val json = json.decodeFromString(response.body.string()) - val html = json.jsonObject["html"]!!.jsonPrimitive.content - - val document = Jsoup.parse(html, response.request.url.toString()) - val mangas = document.select(".my-history-item") - .map { element -> searchHistoryFromElement(element) } - return MangasPage(mangas, false) - } - - private fun searchUtilsFromElement(element: Element): SManga { - val manga = SManga.create() - manga.setUrlWithoutDomain(element.select("td a").attr("href")) - manga.title = element.select("td a").text() - manga.thumbnail_url = element.select("img").attr("abs:src") - return manga - } - - private fun searchHistoryFromElement(element: Element): SManga { - val manga = SManga.create() - manga.setUrlWithoutDomain(element.select(".position-relative a").attr("href")) - manga.title = element.select(".position-relative a").text() - manga.thumbnail_url = element.select("img").attr("abs:src") - return manga - } - - open fun formBuilder() = FormBody.Builder().apply { - add("_where", "browse") - add("first", "0") - add("limit", "0") - add("prevPos", "null") - } - - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException("Not used") - override fun searchMangaSelector() = throw UnsupportedOperationException("Not used") - override fun searchMangaFromElement(element: Element) = throw UnsupportedOperationException("Not used") - override fun searchMangaNextPageSelector() = throw UnsupportedOperationException("Not used") - - override fun mangaDetailsRequest(manga: SManga): Request { - if (manga.url.startsWith("http")) { - return GET(manga.url, headers) - } - return super.mangaDetailsRequest(manga) - } - - override fun mangaDetailsParse(document: Document): SManga { - val infoElement = document.select("div#mainer div.container-fluid") - val manga = SManga.create() - val workStatus = infoElement.select("div.attr-item:contains(original work) span").text() - val uploadStatus = infoElement.select("div.attr-item:contains(upload status) span").text() - manga.title = infoElement.select("h3").text().removeEntities() - manga.author = infoElement.select("div.attr-item:contains(author) span").text() - manga.artist = infoElement.select("div.attr-item:contains(artist) span").text() - manga.status = parseStatus(workStatus, uploadStatus) - manga.genre = infoElement.select(".attr-item b:contains(genres) + span ").joinToString { it.text() } - manga.description = infoElement.select("div.limit-html").text() + "\n" + infoElement.select(".episode-list > .alert-warning").text().trim() - manga.thumbnail_url = document.select("div.attr-cover img") - .attr("abs:src") - return manga - } - - private fun parseStatus(workStatus: String?, uploadStatus: String?) = when { - workStatus == null -> SManga.UNKNOWN - workStatus.contains("Ongoing") -> SManga.ONGOING - workStatus.contains("Cancelled") -> SManga.CANCELLED - workStatus.contains("Hiatus") -> SManga.ON_HIATUS - workStatus.contains("Completed") -> when { - uploadStatus?.contains("Ongoing") == true -> SManga.PUBLISHING_FINISHED - else -> SManga.COMPLETED - } - else -> SManga.UNKNOWN - } - - override fun fetchChapterList(manga: SManga): Observable> { - val url = client.newCall( - GET( - when { - manga.url.startsWith("http") -> manga.url - else -> "$baseUrl${manga.url}" - }, - ), - ).execute().asJsoup() - if (getAltChapterListPref() || checkChapterLists(url)) { - val id = manga.url.substringBeforeLast("/").substringAfterLast("/").trim() - return client.newCall(GET("$baseUrl/rss/series/$id.xml")) - .asObservableSuccess() - .map { altChapterParse(it, manga.title) } - } - return super.fetchChapterList(manga) - } - - private fun altChapterParse(response: Response, title: String): List { - return Jsoup.parse(response.body.string(), response.request.url.toString(), Parser.xmlParser()) - .select("channel > item").map { item -> - SChapter.create().apply { - url = item.selectFirst("guid")!!.text() - name = item.selectFirst("title")!!.text().substringAfter(title).trim() - date_upload = SimpleDateFormat("E, dd MMM yyyy H:m:s Z", Locale.US).parse(item.selectFirst("pubDate")!!.text())?.time ?: 0L - } - } - } - - private fun checkChapterLists(document: Document): Boolean { - return document.select(".episode-list > .alert-warning").text().contains("This comic has been marked as deleted and the chapter list is not available.") - } - - override fun chapterListRequest(manga: SManga): Request { - if (manga.url.startsWith("http")) { - return GET(manga.url, headers) - } - return super.chapterListRequest(manga) - } - - override fun chapterListSelector() = "div.main div.p-2" - - override fun chapterFromElement(element: Element): SChapter { - val chapter = SChapter.create() - val urlElement = element.select("a.chapt") - val group = element.select("div.extra > a:not(.ps-3)").text() - val time = element.select("div.extra > i.ps-3").text() - chapter.setUrlWithoutDomain(urlElement.attr("href")) - chapter.name = urlElement.text() - if (group != "") { - chapter.scanlator = group - } - if (time != "") { - chapter.date_upload = parseChapterDate(time) - } - return chapter - } - - private fun parseChapterDate(date: String): Long { - val value = date.split(' ')[0].toInt() - - return when { - "secs" in date -> Calendar.getInstance().apply { - add(Calendar.SECOND, value * -1) - }.timeInMillis - "mins" in date -> Calendar.getInstance().apply { - add(Calendar.MINUTE, value * -1) - }.timeInMillis - "hours" in date -> Calendar.getInstance().apply { - add(Calendar.HOUR_OF_DAY, value * -1) - }.timeInMillis - "days" in date -> Calendar.getInstance().apply { - add(Calendar.DATE, value * -1) - }.timeInMillis - "weeks" in date -> Calendar.getInstance().apply { - add(Calendar.DATE, value * 7 * -1) - }.timeInMillis - "months" in date -> Calendar.getInstance().apply { - add(Calendar.MONTH, value * -1) - }.timeInMillis - "years" in date -> Calendar.getInstance().apply { - add(Calendar.YEAR, value * -1) - }.timeInMillis - "sec" in date -> Calendar.getInstance().apply { - add(Calendar.SECOND, value * -1) - }.timeInMillis - "min" in date -> Calendar.getInstance().apply { - add(Calendar.MINUTE, value * -1) - }.timeInMillis - "hour" in date -> Calendar.getInstance().apply { - add(Calendar.HOUR_OF_DAY, value * -1) - }.timeInMillis - "day" in date -> Calendar.getInstance().apply { - add(Calendar.DATE, value * -1) - }.timeInMillis - "week" in date -> Calendar.getInstance().apply { - add(Calendar.DATE, value * 7 * -1) - }.timeInMillis - "month" in date -> Calendar.getInstance().apply { - add(Calendar.MONTH, value * -1) - }.timeInMillis - "year" in date -> Calendar.getInstance().apply { - add(Calendar.YEAR, value * -1) - }.timeInMillis - else -> { - return 0 - } - } - } - - override fun pageListRequest(chapter: SChapter): Request { - if (chapter.url.startsWith("http")) { - return GET(chapter.url, headers) - } - return super.pageListRequest(chapter) - } - - override fun pageListParse(document: Document): List { - val script = document.selectFirst("script:containsData(imgHttpLis):containsData(batoWord):containsData(batoPass)")?.html() - ?: throw RuntimeException("Couldn't find script with image data.") - - val imgHttpLisString = script.substringAfter("const imgHttpLis =").substringBefore(";").trim() - val imgHttpLis = json.parseToJsonElement(imgHttpLisString).jsonArray.map { it.jsonPrimitive.content } - val batoWord = script.substringAfter("const batoWord =").substringBefore(";").trim() - val batoPass = script.substringAfter("const batoPass =").substringBefore(";").trim() - - val evaluatedPass: String = Deobfuscator.deobfuscateJsPassword(batoPass) - val imgAccListString = CryptoAES.decrypt(batoWord.removeSurrounding("\""), evaluatedPass) - val imgAccList = json.parseToJsonElement(imgAccListString).jsonArray.map { it.jsonPrimitive.content } - - return imgHttpLis.zip(imgAccList).mapIndexed { i, (imgUrl, imgAcc) -> - Page(i, imageUrl = "$imgUrl?$imgAcc") - } - } - - override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used") - - private fun String.removeEntities(): String = Parser.unescapeEntities(this, true) - - override fun getFilterList() = FilterList( - LetterFilter(getLetterFilter(), 0), - Filter.Separator(), - Filter.Header("NOTE: Ignored if using text search!"), - Filter.Separator(), - SortFilter(getSortFilter().map { it.name }.toTypedArray()), - StatusFilter(getStatusFilter(), 0), - GenreGroupFilter(getGenreFilter()), - OriginGroupFilter(getOrginFilter()), - LangGroupFilter(getLangFilter()), - MinChapterTextFilter(), - MaxChapterTextFilter(), - Filter.Separator(), - Filter.Header("NOTE: Filters below are incompatible with any other filters!"), - Filter.Header("NOTE: Login Required!"), - Filter.Separator(), - UtilsFilter(getUtilsFilter(), 0), - HistoryFilter(getHistoryFilter(), 0), - ) - class SelectFilterOption(val name: String, val value: String) - class CheckboxFilterOption(val value: String, name: String, default: Boolean = false) : Filter.CheckBox(name, default) - class TriStateFilterOption(val value: String, name: String, default: Int = 0) : Filter.TriState(name, default) - - abstract class SelectFilter(name: String, private val options: List, default: Int = 0) : Filter.Select(name, options.map { it.name }.toTypedArray(), default) { - val selected: String - get() = options[state].value - } - - abstract class CheckboxGroupFilter(name: String, options: List) : Filter.Group(name, options) { - val selected: List - get() = state.filter { it.state }.map { it.value } - } - - abstract class TriStateGroupFilter(name: String, options: List) : Filter.Group(name, options) { - val included: List - get() = state.filter { it.isIncluded() }.map { it.value } - - val excluded: List - get() = state.filter { it.isExcluded() }.map { it.value } - } - - abstract class TextFilter(name: String) : Filter.Text(name) - - class SortFilter(sortables: Array) : Filter.Sort("Sort", sortables, Selection(5, false)) - class StatusFilter(options: List, default: Int) : SelectFilter("Status", options, default) - class OriginGroupFilter(options: List) : CheckboxGroupFilter("Origin", options) - class GenreGroupFilter(options: List) : TriStateGroupFilter("Genre", options) - class MinChapterTextFilter : TextFilter("Min. Chapters") - class MaxChapterTextFilter : TextFilter("Max. Chapters") - class LangGroupFilter(options: List) : CheckboxGroupFilter("Languages", options) - class LetterFilter(options: List, default: Int) : SelectFilter("Letter matching mode (Slow)", options, default) - class UtilsFilter(options: List, default: Int) : SelectFilter("Utils comic list", options, default) - class HistoryFilter(options: List, default: Int) : SelectFilter("Personal list", options, default) - - private fun getLetterFilter() = listOf( - SelectFilterOption("Disabled", "disabled"), - SelectFilterOption("Enabled", "enabled"), - ) - - private fun getSortFilter() = listOf( - SelectFilterOption("Z-A", "title"), - SelectFilterOption("Last Updated", "update"), - SelectFilterOption("Newest Added", "create"), - SelectFilterOption("Most Views Totally", "views_a"), - SelectFilterOption("Most Views 365 days", "views_y"), - SelectFilterOption("Most Views 30 days", "views_m"), - SelectFilterOption("Most Views 7 days", "views_w"), - SelectFilterOption("Most Views 24 hours", "views_d"), - SelectFilterOption("Most Views 60 minutes", "views_h"), - ) - - private fun getHistoryFilter() = listOf( - SelectFilterOption("None", ""), - SelectFilterOption("My History", "history"), - SelectFilterOption("My Updates", "updates"), - ) - - private fun getUtilsFilter() = listOf( - SelectFilterOption("None", ""), - SelectFilterOption("Comics: I Created", "i-created"), - SelectFilterOption("Comics: I Modified", "i-modified"), - SelectFilterOption("Comics: I Uploaded", "i-uploaded"), - SelectFilterOption("Comics: Authorized to me", "i-authorized"), - SelectFilterOption("Comics: Draft Status", "status-draft"), - SelectFilterOption("Comics: Hidden Status", "status-hidden"), - SelectFilterOption("Ongoing and Not updated in 30-60 days", "not-updated-30-60"), - SelectFilterOption("Ongoing and Not updated in 60-90 days", "not-updated-60-90"), - SelectFilterOption("Ongoing and Not updated in 90-180 days", "not-updated-90-180"), - SelectFilterOption("Ongoing and Not updated in 180-360 days", "not-updated-180-360"), - SelectFilterOption("Ongoing and Not updated in 360-1000 days", "not-updated-360-1000"), - SelectFilterOption("Ongoing and Not updated more than 1000 days", "not-updated-1000"), - ) - - private fun getStatusFilter() = listOf( - SelectFilterOption("All", ""), - SelectFilterOption("Pending", "pending"), - SelectFilterOption("Ongoing", "ongoing"), - SelectFilterOption("Completed", "completed"), - SelectFilterOption("Hiatus", "hiatus"), - SelectFilterOption("Cancelled", "cancelled"), - ) - - private fun getOrginFilter() = listOf( - // Values exported from publish.bato.to - CheckboxFilterOption("zh", "Chinese"), - CheckboxFilterOption("en", "English"), - CheckboxFilterOption("ja", "Japanese"), - CheckboxFilterOption("ko", "Korean"), - CheckboxFilterOption("af", "Afrikaans"), - CheckboxFilterOption("sq", "Albanian"), - CheckboxFilterOption("am", "Amharic"), - CheckboxFilterOption("ar", "Arabic"), - CheckboxFilterOption("hy", "Armenian"), - CheckboxFilterOption("az", "Azerbaijani"), - CheckboxFilterOption("be", "Belarusian"), - CheckboxFilterOption("bn", "Bengali"), - CheckboxFilterOption("bs", "Bosnian"), - CheckboxFilterOption("bg", "Bulgarian"), - CheckboxFilterOption("my", "Burmese"), - CheckboxFilterOption("km", "Cambodian"), - CheckboxFilterOption("ca", "Catalan"), - CheckboxFilterOption("ceb", "Cebuano"), - CheckboxFilterOption("zh_hk", "Chinese (Cantonese)"), - CheckboxFilterOption("zh_tw", "Chinese (Traditional)"), - CheckboxFilterOption("hr", "Croatian"), - CheckboxFilterOption("cs", "Czech"), - CheckboxFilterOption("da", "Danish"), - CheckboxFilterOption("nl", "Dutch"), - CheckboxFilterOption("en_us", "English (United States)"), - CheckboxFilterOption("eo", "Esperanto"), - CheckboxFilterOption("et", "Estonian"), - CheckboxFilterOption("fo", "Faroese"), - CheckboxFilterOption("fil", "Filipino"), - CheckboxFilterOption("fi", "Finnish"), - CheckboxFilterOption("fr", "French"), - CheckboxFilterOption("ka", "Georgian"), - CheckboxFilterOption("de", "German"), - CheckboxFilterOption("el", "Greek"), - CheckboxFilterOption("gn", "Guarani"), - CheckboxFilterOption("gu", "Gujarati"), - CheckboxFilterOption("ht", "Haitian Creole"), - CheckboxFilterOption("ha", "Hausa"), - CheckboxFilterOption("he", "Hebrew"), - CheckboxFilterOption("hi", "Hindi"), - CheckboxFilterOption("hu", "Hungarian"), - CheckboxFilterOption("is", "Icelandic"), - CheckboxFilterOption("ig", "Igbo"), - CheckboxFilterOption("id", "Indonesian"), - CheckboxFilterOption("ga", "Irish"), - CheckboxFilterOption("it", "Italian"), - CheckboxFilterOption("jv", "Javanese"), - CheckboxFilterOption("kn", "Kannada"), - CheckboxFilterOption("kk", "Kazakh"), - CheckboxFilterOption("ku", "Kurdish"), - CheckboxFilterOption("ky", "Kyrgyz"), - CheckboxFilterOption("lo", "Laothian"), - CheckboxFilterOption("lv", "Latvian"), - CheckboxFilterOption("lt", "Lithuanian"), - CheckboxFilterOption("lb", "Luxembourgish"), - CheckboxFilterOption("mk", "Macedonian"), - CheckboxFilterOption("mg", "Malagasy"), - CheckboxFilterOption("ms", "Malay"), - CheckboxFilterOption("ml", "Malayalam"), - CheckboxFilterOption("mt", "Maltese"), - CheckboxFilterOption("mi", "Maori"), - CheckboxFilterOption("mr", "Marathi"), - CheckboxFilterOption("mo", "Moldavian"), - CheckboxFilterOption("mn", "Mongolian"), - CheckboxFilterOption("ne", "Nepali"), - CheckboxFilterOption("no", "Norwegian"), - CheckboxFilterOption("ny", "Nyanja"), - CheckboxFilterOption("ps", "Pashto"), - CheckboxFilterOption("fa", "Persian"), - CheckboxFilterOption("pl", "Polish"), - CheckboxFilterOption("pt", "Portuguese"), - CheckboxFilterOption("pt_br", "Portuguese (Brazil)"), - CheckboxFilterOption("ro", "Romanian"), - CheckboxFilterOption("rm", "Romansh"), - CheckboxFilterOption("ru", "Russian"), - CheckboxFilterOption("sm", "Samoan"), - CheckboxFilterOption("sr", "Serbian"), - CheckboxFilterOption("sh", "Serbo-Croatian"), - CheckboxFilterOption("st", "Sesotho"), - CheckboxFilterOption("sn", "Shona"), - CheckboxFilterOption("sd", "Sindhi"), - CheckboxFilterOption("si", "Sinhalese"), - CheckboxFilterOption("sk", "Slovak"), - CheckboxFilterOption("sl", "Slovenian"), - CheckboxFilterOption("so", "Somali"), - CheckboxFilterOption("es", "Spanish"), - CheckboxFilterOption("es_419", "Spanish (Latin America)"), - CheckboxFilterOption("sw", "Swahili"), - CheckboxFilterOption("sv", "Swedish"), - CheckboxFilterOption("tg", "Tajik"), - CheckboxFilterOption("ta", "Tamil"), - CheckboxFilterOption("th", "Thai"), - CheckboxFilterOption("ti", "Tigrinya"), - CheckboxFilterOption("to", "Tonga"), - CheckboxFilterOption("tr", "Turkish"), - CheckboxFilterOption("tk", "Turkmen"), - CheckboxFilterOption("uk", "Ukrainian"), - CheckboxFilterOption("ur", "Urdu"), - CheckboxFilterOption("uz", "Uzbek"), - CheckboxFilterOption("vi", "Vietnamese"), - CheckboxFilterOption("yo", "Yoruba"), - CheckboxFilterOption("zu", "Zulu"), - CheckboxFilterOption("_t", "Other"), - ) - - private fun getGenreFilter() = listOf( - TriStateFilterOption("artbook", "Artbook"), - TriStateFilterOption("cartoon", "Cartoon"), - TriStateFilterOption("comic", "Comic"), - TriStateFilterOption("doujinshi", "Doujinshi"), - TriStateFilterOption("imageset", "Imageset"), - TriStateFilterOption("manga", "Manga"), - TriStateFilterOption("manhua", "Manhua"), - TriStateFilterOption("manhwa", "Manhwa"), - TriStateFilterOption("webtoon", "Webtoon"), - TriStateFilterOption("western", "Western"), - - TriStateFilterOption("shoujo", "Shoujo(G)"), - TriStateFilterOption("shounen", "Shounen(B)"), - TriStateFilterOption("josei", "Josei(W)"), - TriStateFilterOption("seinen", "Seinen(M)"), - TriStateFilterOption("yuri", "Yuri(GL)"), - TriStateFilterOption("yaoi", "Yaoi(BL)"), - TriStateFilterOption("futa", "Futa(WL)"), - TriStateFilterOption("bara", "Bara(ML)"), - - TriStateFilterOption("gore", "Gore"), - TriStateFilterOption("bloody", "Bloody"), - TriStateFilterOption("violence", "Violence"), - TriStateFilterOption("ecchi", "Ecchi"), - TriStateFilterOption("adult", "Adult"), - TriStateFilterOption("mature", "Mature"), - TriStateFilterOption("smut", "Smut"), - TriStateFilterOption("hentai", "Hentai"), - - TriStateFilterOption("_4_koma", "4-Koma"), - TriStateFilterOption("action", "Action"), - TriStateFilterOption("adaptation", "Adaptation"), - TriStateFilterOption("adventure", "Adventure"), - TriStateFilterOption("age_gap", "Age Gap"), - TriStateFilterOption("aliens", "Aliens"), - TriStateFilterOption("animals", "Animals"), - TriStateFilterOption("anthology", "Anthology"), - TriStateFilterOption("beasts", "Beasts"), - TriStateFilterOption("bodyswap", "Bodyswap"), - TriStateFilterOption("cars", "cars"), - TriStateFilterOption("cheating_infidelity", "Cheating/Infidelity"), - TriStateFilterOption("childhood_friends", "Childhood Friends"), - TriStateFilterOption("college_life", "College Life"), - TriStateFilterOption("comedy", "Comedy"), - TriStateFilterOption("contest_winning", "Contest Winning"), - TriStateFilterOption("cooking", "Cooking"), - TriStateFilterOption("crime", "crime"), - TriStateFilterOption("crossdressing", "Crossdressing"), - TriStateFilterOption("delinquents", "Delinquents"), - TriStateFilterOption("dementia", "Dementia"), - TriStateFilterOption("demons", "Demons"), - TriStateFilterOption("drama", "Drama"), - TriStateFilterOption("dungeons", "Dungeons"), - TriStateFilterOption("emperor_daughte", "Emperor's Daughter"), - TriStateFilterOption("fantasy", "Fantasy"), - TriStateFilterOption("fan_colored", "Fan-Colored"), - TriStateFilterOption("fetish", "Fetish"), - TriStateFilterOption("full_color", "Full Color"), - TriStateFilterOption("game", "Game"), - TriStateFilterOption("gender_bender", "Gender Bender"), - TriStateFilterOption("genderswap", "Genderswap"), - TriStateFilterOption("ghosts", "Ghosts"), - TriStateFilterOption("gyaru", "Gyaru"), - TriStateFilterOption("harem", "Harem"), - TriStateFilterOption("harlequin", "Harlequin"), - TriStateFilterOption("historical", "Historical"), - TriStateFilterOption("horror", "Horror"), - TriStateFilterOption("incest", "Incest"), - TriStateFilterOption("isekai", "Isekai"), - TriStateFilterOption("kids", "Kids"), - TriStateFilterOption("loli", "Loli"), - TriStateFilterOption("magic", "Magic"), - TriStateFilterOption("magical_girls", "Magical Girls"), - TriStateFilterOption("martial_arts", "Martial Arts"), - TriStateFilterOption("mecha", "Mecha"), - TriStateFilterOption("medical", "Medical"), - TriStateFilterOption("military", "Military"), - TriStateFilterOption("monster_girls", "Monster Girls"), - TriStateFilterOption("monsters", "Monsters"), - TriStateFilterOption("music", "Music"), - TriStateFilterOption("mystery", "Mystery"), - TriStateFilterOption("netorare", "Netorare/NTR"), - TriStateFilterOption("ninja", "Ninja"), - TriStateFilterOption("office_workers", "Office Workers"), - TriStateFilterOption("omegaverse", "Omegaverse"), - TriStateFilterOption("oneshot", "Oneshot"), - TriStateFilterOption("parody", "parody"), - TriStateFilterOption("philosophical", "Philosophical"), - TriStateFilterOption("police", "Police"), - TriStateFilterOption("post_apocalyptic", "Post-Apocalyptic"), - TriStateFilterOption("psychological", "Psychological"), - TriStateFilterOption("regression", "Regression"), - TriStateFilterOption("reincarnation", "Reincarnation"), - TriStateFilterOption("reverse_harem", "Reverse Harem"), - TriStateFilterOption("reverse_isekai", "Reverse Isekai"), - TriStateFilterOption("romance", "Romance"), - TriStateFilterOption("royal_family", "Royal Family"), - TriStateFilterOption("royalty", "Royalty"), - TriStateFilterOption("samurai", "Samurai"), - TriStateFilterOption("school_life", "School Life"), - TriStateFilterOption("sci_fi", "Sci-Fi"), - TriStateFilterOption("shota", "Shota"), - TriStateFilterOption("shoujo_ai", "Shoujo Ai"), - TriStateFilterOption("shounen_ai", "Shounen Ai"), - TriStateFilterOption("showbiz", "Showbiz"), - TriStateFilterOption("slice_of_life", "Slice of Life"), - TriStateFilterOption("sm_bdsm", "SM/BDSM/SUB-DOM"), - TriStateFilterOption("space", "Space"), - TriStateFilterOption("sports", "Sports"), - TriStateFilterOption("super_power", "Super Power"), - TriStateFilterOption("superhero", "Superhero"), - TriStateFilterOption("supernatural", "Supernatural"), - TriStateFilterOption("survival", "Survival"), - TriStateFilterOption("thriller", "Thriller"), - TriStateFilterOption("time_travel", "Time Travel"), - TriStateFilterOption("tower_climbing", "Tower Climbing"), - TriStateFilterOption("traditional_games", "Traditional Games"), - TriStateFilterOption("tragedy", "Tragedy"), - TriStateFilterOption("transmigration", "Transmigration"), - TriStateFilterOption("vampires", "Vampires"), - TriStateFilterOption("villainess", "Villainess"), - TriStateFilterOption("video_games", "Video Games"), - TriStateFilterOption("virtual_reality", "Virtual Reality"), - TriStateFilterOption("wuxia", "Wuxia"), - TriStateFilterOption("xianxia", "Xianxia"), - TriStateFilterOption("xuanhuan", "Xuanhuan"), - TriStateFilterOption("zombies", "Zombies"), - // Hidden Genres - TriStateFilterOption("shotacon", "shotacon"), - TriStateFilterOption("lolicon", "lolicon"), - TriStateFilterOption("award_winning", "Award Winning"), - TriStateFilterOption("youkai", "Youkai"), - TriStateFilterOption("uncategorized", "Uncategorized"), - ) - - private fun getLangFilter() = listOf( - // Values exported from publish.bato.to - CheckboxFilterOption("en", "English"), - CheckboxFilterOption("ar", "Arabic"), - CheckboxFilterOption("bg", "Bulgarian"), - CheckboxFilterOption("zh", "Chinese"), - CheckboxFilterOption("cs", "Czech"), - CheckboxFilterOption("da", "Danish"), - CheckboxFilterOption("nl", "Dutch"), - CheckboxFilterOption("fil", "Filipino"), - CheckboxFilterOption("fi", "Finnish"), - CheckboxFilterOption("fr", "French"), - CheckboxFilterOption("de", "German"), - CheckboxFilterOption("el", "Greek"), - CheckboxFilterOption("he", "Hebrew"), - CheckboxFilterOption("hi", "Hindi"), - CheckboxFilterOption("hu", "Hungarian"), - CheckboxFilterOption("id", "Indonesian"), - CheckboxFilterOption("it", "Italian"), - CheckboxFilterOption("ja", "Japanese"), - CheckboxFilterOption("ko", "Korean"), - CheckboxFilterOption("ms", "Malay"), - CheckboxFilterOption("pl", "Polish"), - CheckboxFilterOption("pt", "Portuguese"), - CheckboxFilterOption("pt_br", "Portuguese (Brazil)"), - CheckboxFilterOption("ro", "Romanian"), - CheckboxFilterOption("ru", "Russian"), - CheckboxFilterOption("es", "Spanish"), - CheckboxFilterOption("es_419", "Spanish (Latin America)"), - CheckboxFilterOption("sv", "Swedish"), - CheckboxFilterOption("th", "Thai"), - CheckboxFilterOption("tr", "Turkish"), - CheckboxFilterOption("uk", "Ukrainian"), - CheckboxFilterOption("vi", "Vietnamese"), - CheckboxFilterOption("af", "Afrikaans"), - CheckboxFilterOption("sq", "Albanian"), - CheckboxFilterOption("am", "Amharic"), - CheckboxFilterOption("hy", "Armenian"), - CheckboxFilterOption("az", "Azerbaijani"), - CheckboxFilterOption("be", "Belarusian"), - CheckboxFilterOption("bn", "Bengali"), - CheckboxFilterOption("bs", "Bosnian"), - CheckboxFilterOption("my", "Burmese"), - CheckboxFilterOption("km", "Cambodian"), - CheckboxFilterOption("ca", "Catalan"), - CheckboxFilterOption("ceb", "Cebuano"), - CheckboxFilterOption("zh_hk", "Chinese (Cantonese)"), - CheckboxFilterOption("zh_tw", "Chinese (Traditional)"), - CheckboxFilterOption("hr", "Croatian"), - CheckboxFilterOption("en_us", "English (United States)"), - CheckboxFilterOption("eo", "Esperanto"), - CheckboxFilterOption("et", "Estonian"), - CheckboxFilterOption("fo", "Faroese"), - CheckboxFilterOption("ka", "Georgian"), - CheckboxFilterOption("gn", "Guarani"), - CheckboxFilterOption("gu", "Gujarati"), - CheckboxFilterOption("ht", "Haitian Creole"), - CheckboxFilterOption("ha", "Hausa"), - CheckboxFilterOption("is", "Icelandic"), - CheckboxFilterOption("ig", "Igbo"), - CheckboxFilterOption("ga", "Irish"), - CheckboxFilterOption("jv", "Javanese"), - CheckboxFilterOption("kn", "Kannada"), - CheckboxFilterOption("kk", "Kazakh"), - CheckboxFilterOption("ku", "Kurdish"), - CheckboxFilterOption("ky", "Kyrgyz"), - CheckboxFilterOption("lo", "Laothian"), - CheckboxFilterOption("lv", "Latvian"), - CheckboxFilterOption("lt", "Lithuanian"), - CheckboxFilterOption("lb", "Luxembourgish"), - CheckboxFilterOption("mk", "Macedonian"), - CheckboxFilterOption("mg", "Malagasy"), - CheckboxFilterOption("ml", "Malayalam"), - CheckboxFilterOption("mt", "Maltese"), - CheckboxFilterOption("mi", "Maori"), - CheckboxFilterOption("mr", "Marathi"), - CheckboxFilterOption("mo", "Moldavian"), - CheckboxFilterOption("mn", "Mongolian"), - CheckboxFilterOption("ne", "Nepali"), - CheckboxFilterOption("no", "Norwegian"), - CheckboxFilterOption("ny", "Nyanja"), - CheckboxFilterOption("ps", "Pashto"), - CheckboxFilterOption("fa", "Persian"), - CheckboxFilterOption("rm", "Romansh"), - CheckboxFilterOption("sm", "Samoan"), - CheckboxFilterOption("sr", "Serbian"), - CheckboxFilterOption("sh", "Serbo-Croatian"), - CheckboxFilterOption("st", "Sesotho"), - CheckboxFilterOption("sn", "Shona"), - CheckboxFilterOption("sd", "Sindhi"), - CheckboxFilterOption("si", "Sinhalese"), - CheckboxFilterOption("sk", "Slovak"), - CheckboxFilterOption("sl", "Slovenian"), - CheckboxFilterOption("so", "Somali"), - CheckboxFilterOption("sw", "Swahili"), - CheckboxFilterOption("tg", "Tajik"), - CheckboxFilterOption("ta", "Tamil"), - CheckboxFilterOption("ti", "Tigrinya"), - CheckboxFilterOption("to", "Tonga"), - CheckboxFilterOption("tk", "Turkmen"), - CheckboxFilterOption("ur", "Urdu"), - CheckboxFilterOption("uz", "Uzbek"), - CheckboxFilterOption("yo", "Yoruba"), - CheckboxFilterOption("zu", "Zulu"), - CheckboxFilterOption("_t", "Other"), - // Lang options from bato.to brows not in publish.bato.to - CheckboxFilterOption("eu", "Basque"), - CheckboxFilterOption("pt-PT", "Portuguese (Portugal)"), - ).filterNot { it.value == siteLang } - - companion object { - private const val MIRROR_PREF_KEY = "MIRROR" - private const val MIRROR_PREF_TITLE = "Mirror" - private val MIRROR_PREF_ENTRIES = arrayOf( - "bato.to", - "batocomic.com", - "batocomic.net", - "batocomic.org", - "batotoo.com", - "batotwo.com", - "battwo.com", - "comiko.net", - "comiko.org", - "mangatoto.com", - "mangatoto.net", - "mangatoto.org", - "readtoto.com", - "readtoto.net", - "readtoto.org", - "dto.to", - "hto.to", - "mto.to", - "wto.to", - "xbato.com", - "xbato.net", - "xbato.org", - "zbato.com", - "zbato.net", - "zbato.org", - ) - private val MIRROR_PREF_ENTRY_VALUES = MIRROR_PREF_ENTRIES.map { "https://$it" }.toTypedArray() - private val MIRROR_PREF_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES[0] - - private const val ALT_CHAPTER_LIST_PREF_KEY = "ALT_CHAPTER_LIST" - private const val ALT_CHAPTER_LIST_PREF_TITLE = "Alternative Chapter List" - private const val ALT_CHAPTER_LIST_PREF_SUMMARY = "If checked, uses an alternate chapter list" - private const val ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE = false - } -} diff --git a/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoToFactory.kt b/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoToFactory.kt deleted file mode 100644 index 2a4ee05ee..000000000 --- a/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoToFactory.kt +++ /dev/null @@ -1,122 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.batoto - -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.SourceFactory - -class BatoToFactory : SourceFactory { - override fun createSources(): List = languages.map { BatoTo(it.lang, it.siteLang) } -} - -class LanguageOption(val lang: String, val siteLang: String = lang) -private val languages = listOf( - LanguageOption("all", ""), - // Lang options from publish.bato.to - LanguageOption("en"), - LanguageOption("ar"), - LanguageOption("bg"), - LanguageOption("zh"), - LanguageOption("cs"), - LanguageOption("da"), - LanguageOption("nl"), - LanguageOption("fil"), - LanguageOption("fi"), - LanguageOption("fr"), - LanguageOption("de"), - LanguageOption("el"), - LanguageOption("he"), - LanguageOption("hi"), - LanguageOption("hu"), - LanguageOption("id"), - LanguageOption("it"), - LanguageOption("ja"), - LanguageOption("ko"), - LanguageOption("ms"), - LanguageOption("pl"), - LanguageOption("pt"), - LanguageOption("pt-BR", "pt_br"), - LanguageOption("ro"), - LanguageOption("ru"), - LanguageOption("es"), - LanguageOption("es-419", "es_419"), - LanguageOption("sv"), - LanguageOption("th"), - LanguageOption("tr"), - LanguageOption("uk"), - LanguageOption("vi"), - LanguageOption("af"), - LanguageOption("sq"), - LanguageOption("am"), - LanguageOption("hy"), - LanguageOption("az"), - LanguageOption("be"), - LanguageOption("bn"), - LanguageOption("bs"), - LanguageOption("my"), - LanguageOption("km"), - LanguageOption("ca"), - LanguageOption("ceb"), - LanguageOption("zh-Hans", "zh_hk"), - LanguageOption("zh-Hant", "zh_tw"), - LanguageOption("hr"), - LanguageOption("en-US", "en_us"), - LanguageOption("eo"), - LanguageOption("et"), - LanguageOption("fo"), - LanguageOption("ka"), - LanguageOption("gn"), - LanguageOption("gu"), - LanguageOption("ht"), - LanguageOption("ha"), - LanguageOption("is"), - LanguageOption("ig"), - LanguageOption("ga"), - LanguageOption("jv"), - LanguageOption("kn"), - LanguageOption("kk"), - LanguageOption("ku"), - LanguageOption("ky"), - LanguageOption("lo"), - LanguageOption("lv"), - LanguageOption("lt"), - LanguageOption("lb"), - LanguageOption("mk"), - LanguageOption("mg"), - LanguageOption("ml"), - LanguageOption("mt"), - LanguageOption("mi"), - LanguageOption("mr"), - LanguageOption("mo", "ro-MD"), - LanguageOption("mn"), - LanguageOption("ne"), - LanguageOption("no"), - LanguageOption("ny"), - LanguageOption("ps"), - LanguageOption("fa"), - LanguageOption("rm"), - LanguageOption("sm"), - LanguageOption("sr"), - LanguageOption("sh"), - LanguageOption("st"), - LanguageOption("sn"), - LanguageOption("sd"), - LanguageOption("si"), - LanguageOption("sk"), - LanguageOption("sl"), - LanguageOption("so"), - LanguageOption("sw"), - LanguageOption("tg"), - LanguageOption("ta"), - LanguageOption("ti"), - LanguageOption("to"), - LanguageOption("tk"), - LanguageOption("ur"), - LanguageOption("uz"), - LanguageOption("yo"), - LanguageOption("zu"), - LanguageOption("other", "_t"), - // Lang options from bato.to brows not in publish.bato.to - LanguageOption("eu"), - LanguageOption("pt-PT", "pt_pt"), - // Lang options that got removed - // Pair("xh", "xh"), -) diff --git a/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoToUrlActivity.kt b/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoToUrlActivity.kt deleted file mode 100644 index 4f1bd140e..000000000 --- a/src/all/batoto/src/eu/kanade/tachiyomi/extension/all/batoto/BatoToUrlActivity.kt +++ /dev/null @@ -1,51 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.batoto - -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Bundle -import android.util.Log -import kotlin.system.exitProcess - -class BatoToUrlActivity : Activity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val host = intent?.data?.host - val pathSegments = intent?.data?.pathSegments - - if (host != null && pathSegments != null) { - val query = fromBatoTo(pathSegments) - - if (query == null) { - Log.e("BatoToUrlActivity", "Unable to parse URI from intent $intent") - finish() - exitProcess(1) - } - - val mainIntent = Intent().apply { - action = "eu.kanade.tachiyomi.SEARCH" - putExtra("query", query) - putExtra("filter", packageName) - } - - try { - startActivity(mainIntent) - } catch (e: ActivityNotFoundException) { - Log.e("BatoToUrlActivity", e.toString()) - } - } - - finish() - exitProcess(0) - } - - private fun fromBatoTo(pathSegments: MutableList): String? { - return if (pathSegments.size >= 2) { - val id = pathSegments[1] - "ID:$id" - } else { - null - } - } -} diff --git a/src/all/mangadex/AndroidManifest.xml b/src/all/mangadex/AndroidManifest.xml deleted file mode 100644 index 4faeb6138..000000000 --- a/src/all/mangadex/AndroidManifest.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/all/mangadex/README.md b/src/all/mangadex/README.md deleted file mode 100644 index e1859cc12..000000000 --- a/src/all/mangadex/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# MangaDex - -Table of Content -- [FAQ](#FAQ) - - [Version 5 API Rewrite](#version-5-api-rewrite) -- [Guides](#Guides) - - [How can I block particular Scanlator Groups?](#how-can-i-block-particular-scanlator-groups) - -Don't find the question you are look for go check out our general FAQs and Guides over at [Extension FAQ](https://tachiyomi.org/help/faq/#extensions) or [Getting Started](https://tachiyomi.org/help/guides/getting-started/#installation) - -## FAQ - -### Version 5 API Rewrite - -#### Why are all my manga saying "Manga ID format has changed, migrate from MangaDex to MangaDex to continue reading"? -You need to [migrate](https://tachiyomi.org/help/guides/source-migration/) all your MangaDex manga from MangaDex to MangaDex as MangaDex has changed their manga ID system from IDs to UUIDs. - -#### Why can I not restore from a JSON backup? -JSON backups are now unusable due to the ID change. You will have to manually re-add your manga. - -## Guides - -### What does the Status of a Manga in Tachiyomi mean? - -Please refer to the following table - -| Status in Tachiyomi | in MangaDex | Remarks | -|---------------------|------------------------|---------| -| Ongoing | Publication: Ongoing | | -| Cancelled | Publication: Cancelled | This title was abruptly stopped and will not resume | -| Publishing Finished | Publication: Completed | The title is finished in its original language. However, Translations remain | -| On_Hiatus | Publication: Hiatus | The title is not currently receiving any new chapters | -| Completed | Completed/Cancelled | All chapters are translated and available | -| Unknown | Unknown | There is no info about the Status of this Entry | - -### How can I block particular Scanlator Groups? - -The **MangaDex** extension allows blocking **Scanlator Groups**. Chapters uploaded by a **Blocked Scanlator Group** will not show up in **Latest** or in **Manga feed** (chapters list). For now, you can only block Groups by entering their UUIDs manually. - -Follow the following steps to easily block a group from the Tachiyomi MangaDex extension: - -A. Finding the **UUIDs**: -- Go to [https://mangadex.org](https://mangadex.org) and **Search** for the Scanlation Group that you wish to block and view their Group Details -- Using the URL of this page, get the 16-digit alphanumeric string which will be the UUID for that scanlation group -- For Example: - * The Group *Tristan's test scans* has the URL - - [https://mangadex.org/group/6410209a-0f39-4f51-a139-bc559ad61a4f/tristan-s-test-scans](https://mangadex.org/group/6410209a-0f39-4f51-a139-bc559ad61a4f/tristan-s-test-scans) - - Therefore, their UUID will be `6410209a-0f39-4f51-a139-bc559ad61a4f` - * Other Examples include: - + Azuki Manga | `5fed0576-8b94-4f9a-b6a7-08eecd69800d` - + Bilibili Comics | `06a9fecb-b608-4f19-b93c-7caab06b7f44` - + Comikey | `8d8ecf83-8d42-4f8c-add8-60963f9f28d9` - + INKR | `caa63201-4a17-4b7f-95ff-ed884a2b7e60` - + MangaHot | `319c1b10-cbd0-4f55-a46e-c4ee17e65139` - + MangaPlus | `4f1de6a2-f0c5-4ac5-bce5-02c7dbb67deb` - -B. Blocking a group using their UUID in Tachiyomi MangaDex extension `v1.2.150+`: -1. Go to **Browse** → **Extensions**. -1. Click on **MangaDex** extension and then **Settings** under your Language of choice. -1. Tap on the option **Block Groups by UUID** and enter the UUIDs. - - By Default, the following groups are blocked: - ``` - Azuki Manga, Bilibili Comics, Comikey, INKR, MangaHot & MangaPlus - ``` - - Which are entered as: - ``` - 5fed0576-8b94-4f9a-b6a7-08eecd69800d, 06a9fecb-b608-4f19-b93c-7caab06b7f44, - 8d8ecf83-8d42-4f8c-add8-60963f9f28d9, caa63201-4a17-4b7f-95ff-ed884a2b7e60, - 319c1b10-cbd0-4f55-a46e-c4ee17e65139, 4f1de6a2-f0c5-4ac5-bce5-02c7dbb67deb - ``` diff --git a/src/all/mangadex/assets/i18n/messages_en.properties b/src/all/mangadex/assets/i18n/messages_en.properties deleted file mode 100644 index 435187ebc..000000000 --- a/src/all/mangadex/assets/i18n/messages_en.properties +++ /dev/null @@ -1,150 +0,0 @@ -alternative_titles=Alternative titles: -alternative_titles_in_description=Alternative titles in description -alternative_titles_in_description_summary=Include a manga's alternative titles at the end of its description -block_group_by_uuid=Block groups by UUID -block_group_by_uuid_summary=Chapters from blocked groups will not show up in Latest or Manga feed. Enter as a Comma-separated list of group UUIDs -block_uploader_by_uuid=Block uploader by UUID -block_uploader_by_uuid_summary=Chapters from blocked uploaders will not show up in Latest or Manga feed. Enter as a Comma-separated list of uploader UUIDs -content=Content -content_gore=Gore -content_rating=Content rating -content_rating_erotica=Erotica -content_rating_genre=Content rating: %s -content_rating_pornographic=Pornographic -content_rating_safe=Safe -content_rating_suggestive=Suggestive -content_sexual_violence=Sexual violence -cover_quality=Cover quality -cover_quality_low=Low -cover_quality_medium=Medium -cover_quality_original=Original -data_saver=Data saver -data_saver_summary=Enables smaller, more compressed images -excluded_tags_mode=Excluded tags mode -filter_original_languages=Filter original languages -filter_original_languages_summary=Only show content that was originally published in the selected languages in both latest and browse -format=Format -format_adaptation=Adaptation -format_anthology=Anthology -format_award_winning=Award Winning -format_doujinshi=Doujinshi -format_fan_colored=Fan Colored -format_full_color=Full Color -format_long_strip=Long Strip -format_official_colored=Official Colored -format_oneshot=Oneshot -format_user_created=User Created -format_web_comic=Web Comic -format_yonkoma=4-Koma -genre=Genre -genre_action=Action -genre_adventure=Adventure -genre_boys_love=Boy's Love -genre_comedy=Comedy -genre_crime=Crime -genre_drama=Drama -genre_fantasy=Fantasy -genre_girls_love=Girl's Love -genre_historical=Historical -genre_horror=Horror -genre_isekai=Isekai -genre_magical_girls=Magical Girls -genre_mecha=Mecha -genre_medical=Medical -genre_mystery=Mystery -genre_philosophical=Philosophical -genre_romance=Romance -genre_sci_fi=Sci-Fi -genre_slice_of_life=Slice of Life -genre_sports=Sports -genre_superhero=Superhero -genre_thriller=Thriller -genre_tragedy=Tragedy -genre_wuxia=Wuxia -has_available_chapters=Has available chapters -included_tags_mode=Included tags mode -invalid_author_id=Not a valid author ID -invalid_manga_id=Not a valid manga ID -invalid_group_id=Not a valid group ID -invalid_uuids=The text contains invalid UUIDs -migrate_warning=Migrate this entry from MangaDex to MangaDex to update it -mode_and=And -mode_or=Or -no_group=No Group -no_series_in_list=No series in the list -original_language=Original language -original_language_filter_chinese=%s (Manhua) -original_language_filter_japanese=%s (Manga) -original_language_filter_korean=%s (Manhwa) -publication_demographic=Publication demographic -publication_demographic_josei=Josei -publication_demographic_none=None -publication_demographic_seinen=Seinen -publication_demographic_shoujo=Shoujo -publication_demographic_shounen=Shounen -sort=Sort -sort_alphabetic=Alphabetic -sort_chapter_uploaded_at=Chapter uploaded at -sort_content_created_at=Content created at -sort_content_info_updated_at=Content info updated at -sort_number_of_follows=Number of follows -sort_rating=Rating -sort_relevance=Relevance -sort_year=Year -standard_content_rating=Default content rating -standard_content_rating_summary=Show content with the selected ratings by default -standard_https_port=Use HTTPS port 443 only -standard_https_port_summary=Enable to only request image servers that use port 443. This allows users with stricter firewall restrictions to access MangaDex images -status=Status -status_cancelled=Cancelled -status_completed=Completed -status_hiatus=Hiatus -status_ongoing=Ongoing -tags_mode=Tags mode -theme=Theme -theme_aliens=Aliens -theme_animals=Animals -theme_cooking=Cooking -theme_crossdressing=Crossdressing -theme_delinquents=Delinquents -theme_demons=Demons -theme_gender_swap=Genderswap -theme_ghosts=Ghosts -theme_gyaru=Gyaru -theme_harem=Harem -theme_incest=Incest -theme_loli=Loli -theme_mafia=Mafia -theme_magic=Magic -theme_martial_arts=Martial Arts -theme_military=Military -theme_monster_girls=Monster Girls -theme_monsters=Monsters -theme_music=Music -theme_ninja=Ninja -theme_office_workers=Office Workers -theme_police=Police -theme_post_apocalyptic=Post-Apocalyptic -theme_psychological=Psychological -theme_reincarnation=Reincarnation -theme_reverse_harem=Reverse Harem -theme_samurai=Samurai -theme_school_life=School Life -theme_shota=Shota -theme_supernatural=Supernatural -theme_survival=Survival -theme_time_travel=Time Travel -theme_traditional_games=Traditional Games -theme_vampires=Vampires -theme_video_games=Video Games -theme_villainess=Villainess -theme_virtual_reality=Virtual Reality -theme_zombies=Zombies -try_using_first_volume_cover=Attempt to use the first volume cover as cover -try_using_first_volume_cover_summary=May need to manually refresh entries already in library. Otherwise, clear database to have new covers to show up. -unable_to_process_chapter_request=Unable to process Chapter request. HTTP code: %d -uploaded_by=Uploaded by %s -set_custom_useragent=Set custom User-Agent -set_custom_useragent_summary=Keep it as default -set_custom_useragent_dialog=\n\nSpecify a custom user agent\n After each modification, the application needs to be restarted.\n\nDefault value:\n%s -set_custom_useragent_error_invalid=Invalid User-Agent: %s diff --git a/src/all/mangadex/assets/i18n/messages_es.properties b/src/all/mangadex/assets/i18n/messages_es.properties deleted file mode 100644 index fc1dc27ab..000000000 --- a/src/all/mangadex/assets/i18n/messages_es.properties +++ /dev/null @@ -1,108 +0,0 @@ -block_group_by_uuid=Bloquear grupos por UUID -block_group_by_uuid_summary=Los capítulos de los grupos bloqueados no aparecerán en Recientes o en el Feed de mangas. Introduce una coma para separar la lista de UUIDs -block_uploader_by_uuid=Bloquear uploader por UUID -block_uploader_by_uuid_summary=Los capítulos de los uploaders bloqueados no aparecerán en Recientes o en el Feed de mangas. Introduce una coma para separar la lista de UUIDs -content=Contenido -content_rating=Clasificación de contenido -content_rating_erotica=Erótico -content_rating_genre=Clasificación: %s -content_rating_pornographic=Pornográfico -content_rating_safe=Seguro -content_rating_suggestive=Sugestivo -content_sexual_violence=Violencia sexual -cover_quality=Calidad de la portada -cover_quality_low=Bajo -cover_quality_medium=Medio -data_saver=Ahorro de datos -data_saver_summary=Utiliza imágenes más pequeñas y más comprimidas -excluded_tags_mode=Modo de etiquetas excluidas -filter_original_languages=Filtrar por lenguajes -filter_original_languages_summary=Muestra solo el contenido publicado en los idiomas seleccionados en recientes y en la búsqueda -format=Formato -format_adaptation=Adaptación -format_anthology=Antología -format_award_winning=Ganador de premio -format_fan_colored=Coloreado por fans -format_full_color=Todo a color -format_long_strip=Tira larga -format_official_colored=Coloreo oficial -format_user_created=Creado por usuario -genre=Genero -genre_action=Acción -genre_adventure=Aventura -genre_comedy=Comedia -genre_crime=Crimen -genre_fantasy=Fantasia -genre_historical=Histórico -genre_magical_girls=Chicas mágicas -genre_medical=Medico -genre_mystery=Misterio -genre_philosophical=Filosófico -genre_sci_fi=Ciencia ficción -genre_slice_of_life=Recuentos de la vida -genre_sports=Deportes -genre_superhero=Superhéroes -genre_tragedy=Tragedia -has_available_chapters=Tiene capítulos disponibles -included_tags_mode=Modo de etiquetas incluidas -invalid_author_id=ID de autor inválida -invalid_group_id=ID de grupo inválida -migrate_warning=Migre la entrada MangaDex a MangaDex para actualizarla -mode_and=Y -mode_or=O -no_group=Sin grupo -no_series_in_list=No hay series en la lista -original_language=Lenguaje original -publication_demographic=Demografía -publication_demographic_none=Ninguna -sort=Ordenar -sort_alphabetic=Alfabeticamente -sort_chapter_uploaded_at=Capítulo subido en -sort_content_created_at=Contenido creado en -sort_content_info_updated_at=Información del contenido actualizada en -sort_number_of_follows=Número de seguidores -sort_rating=Calificación -sort_relevance=Relevancia -sort_year=Año -standard_content_rating=Clasificación de contenido por defecto -standard_content_rating_summary=Muestra el contenido con la clasificación de contenido seleccionada por defecto -standard_https_port=Utilizar el puerto 443 de HTTPS -standard_https_port_summary=Habilite esta opción solicitar las imágenes a los servidores que usan el puerto 443. Esto permite a los usuarios con restricciones estrictas de firewall acceder a las imagenes en MangaDex -status=Estado -status_cancelled=Cancelado -status_completed=Completado -status_hiatus=Pausado -status_ongoing=Publicandose -tags_mode=Modo de etiquetas -theme=Tema -theme_aliens=Alienígenas -theme_animals=Animales -theme_cooking=Cocina -theme_crossdressing=Travestismo -theme_delinquents=Delincuentes -theme_demons=Demonios -theme_gender_swap=Cambio de sexo -theme_ghosts=Fantasmas -theme_incest=Incesto -theme_magic=Magia -theme_martial_arts=Artes marciales -theme_military=Militar -theme_monster_girls=Chicas monstruo -theme_monsters=Monstruos -theme_music=Musica -theme_office_workers=Oficinistas -theme_police=Policial -theme_post_apocalyptic=Post-apocalíptico -theme_psychological=Psicológico -theme_reincarnation=Reencarnación -theme_reverse_harem=Harem inverso -theme_school_life=Vida escolar -theme_supernatural=Sobrenatural -theme_survival=Supervivencia -theme_time_travel=Viaje en el tiempo -theme_traditional_games=Juegos tradicionales -theme_vampires=Vampiros -theme_villainess=Villana -theme_virtual_reality=Realidad virtual -unable_to_process_chapter_request=No se ha podido procesar la solicitud del capítulo. Código HTTP: %d -uploaded_by=Subido por %s \ No newline at end of file diff --git a/src/all/mangadex/assets/i18n/messages_pt_br.properties b/src/all/mangadex/assets/i18n/messages_pt_br.properties deleted file mode 100644 index ecd4ca7be..000000000 --- a/src/all/mangadex/assets/i18n/messages_pt_br.properties +++ /dev/null @@ -1,119 +0,0 @@ -alternative_titles=Títulos alternativos: -alternative_titles_in_description=Títulos alternativos na descrição -alternative_titles_in_description_summary=Inclui os títulos alternativos das séries no final de cada descrição -block_group_by_uuid=Bloquear grupos por UUID -block_group_by_uuid_summary=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 -block_uploader_by_uuid=Bloquear uploaders por UUID -block_uploader_by_uuid_summary=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 -content=Conteúdo -content_rating=Classificação de conteúdo -content_rating_erotica=Erótico -content_rating_genre=Classificação: %s -content_rating_pornographic=Pornográfico -content_rating_safe=Seguro -content_rating_suggestive=Sugestivo -content_sexual_violence=Violência sexual -cover_quality=Qualidade da capa -cover_quality_low=Baixa -cover_quality_medium=Média -data_saver=Economia de dados -data_saver_summary=Utiliza imagens menores e mais compactadas -excluded_tags_mode=Modo de exclusão de tags -filter_original_languages=Filtrar os idiomas originais -filter_original_languages_summary=Mostra somente conteúdos que foram publicados originalmente nos idiomas selecionados nas seções de recentes e navegar -format=Formato -format_adaptation=Adaptação -format_anthology=Antologia -format_award_winning=Premiado -format_fan_colored=Colorizado por fãs -format_full_color=Colorido -format_long_strip=Vertical -format_official_colored=Colorizado oficialmente -format_user_created=Criado por usuários -genre=Gênero -genre_action=Ação -genre_adventure=Aventura -genre_comedy=Comédia -genre_crime=Crime -genre_fantasy=Fantasia -genre_historical=Histórico -genre_magical_girls=Garotas mágicas -genre_medical=Médico -genre_mystery=Mistério -genre_philosophical=Filosófico -genre_sci_fi=Ficção científica -genre_slice_of_life=Cotidiano -genre_sports=Esportes -genre_superhero=Super-heroi -genre_tragedy=Tragédia -has_available_chapters=Há capítulos disponíveis -included_tags_mode=Modo de inclusão de tags -invalid_author_id=ID do autor inválido -invalid_manga_id=ID do mangá inválido -invalid_group_id=ID do grupo inválido -invalid_uuids=O texto contém UUIDs inválidos -migrate_warning=Migre esta entrada do MangaDex para o MangaDex para atualizar -mode_and=E -mode_or=Ou -no_group=Sem grupo -no_series_in_list=Sem séries na lista -original_language=Idioma original -original_language_filter_japanese=%s (Mangá) -publication_demographic=Demografia da publicação -publication_demographic_none=Nenhuma -sort=Ordenar -sort_alphabetic=Alfabeticamente -sort_chapter_uploaded_at=Upload do capítulo -sort_content_created_at=Criação do conteúdo -sort_content_info_updated_at=Atualização das informações -sort_number_of_follows=Número de seguidores -sort_rating=Nota -sort_relevance=Relevância -sort_year=Ano de lançamento -standard_content_rating=Classificação de conteúdo padrão -standard_content_rating_summary=Mostra os conteúdos com as classificações selecionadas por padrão -standard_https_port=Utilizar somente a porta 443 do HTTPS -standard_https_port_summary=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. -status=Estado -status_cancelled=Cancelado -status_completed=Completo -status_hiatus=Hiato -status_ongoing=Em andamento -tags_mode=Modo das tags -theme=Tema -theme_aliens=Alienígenas -theme_animals=Animais -theme_cooking=Culinária -theme_delinquents=Delinquentes -theme_demons=Demônios -theme_gender_swap=Troca de gêneros -theme_ghosts=Fantasmas -theme_harem=Harém -theme_incest=Incesto -theme_mafia=Máfia -theme_magic=Magia -theme_martial_arts=Artes marciais -theme_military=Militar -theme_monster_girls=Garotas monstro -theme_monsters=Monstros -theme_music=Musical -theme_office_workers=Funcionários de escritório -theme_police=Policial -theme_post_apocalyptic=Pós-apocalíptico -theme_psychological=Psicológico -theme_reincarnation=Reencarnação -theme_reverse_harem=Harém reverso -theme_school_life=Vida escolar -theme_supernatural=Sobrenatural -theme_survival=Sobrevivência -theme_time_travel=Viagem no tempo -theme_traditional_games=Jogos tradicionais -theme_vampires=Vampiros -theme_video_games=Videojuegos -theme_villainess=Villainess -theme_virtual_reality=Realidade virtual -theme_zombies=Zumbis -try_using_first_volume_cover=Tentar usar a capa do primeiro volume como capa -try_using_first_volume_cover_summary=Pode ser necessário atualizar os itens já adicionados na biblioteca. Alternativamente, limpe o banco de dados para as novas capas aparecerem. -unable_to_process_chapter_request=Não foi possível processar a requisição do capítulo. Código HTTP: %d -uploaded_by=Enviado por %s \ No newline at end of file diff --git a/src/all/mangadex/assets/i18n/messages_ru.properties b/src/all/mangadex/assets/i18n/messages_ru.properties deleted file mode 100644 index fbc49b49f..000000000 --- a/src/all/mangadex/assets/i18n/messages_ru.properties +++ /dev/null @@ -1,138 +0,0 @@ -block_group_by_uuid=Заблокировать группы по UUID -block_group_by_uuid_summary=Главы от заблокированных групп не будут отображаться в последних обновлениях и в списке глав тайтла. Введите через запятую список UUID групп. -block_uploader_by_uuid=Заблокировать загрузчика по UUID -block_uploader_by_uuid_summary=Главы от заблокированных загрузчиков не будут отображаться в последних обновлениях и в списке глав тайтла. Введите через запятую список UUID загрузчиков. -content=Неприемлемый контент -content_gore=Жестокость -content_rating=Рейтинг контента -content_rating_erotica=Эротический -content_rating_genre=Рейтинг контента: %s -content_rating_pornographic=Порнографический -content_rating_safe=Безопасный -content_rating_suggestive=Намекающий -content_sexual_violence=Сексуальное насилие -cover_quality=Качество обложки -cover_quality_low=Низкое -cover_quality_medium=Среднее -cover_quality_original=Оригинальное -data_saver=Экономия трафика -data_saver_summary=Использует меньшие по размеру, сжатые изображения -excluded_tags_mode=Исключая -filter_original_languages=Фильтр по языку оригинала -filter_original_languages_summary=Показывать тайтлы которые изначально были выпущены только в выбранных языках в последних обновлениях и при поиске -format=Формат -format_adaptation=Адаптация -format_anthology=Антология -format_award_winning=Отмеченный наградами -format_doujinshi=Додзинси -format_fan_colored=Раскрашенная фанатами -format_full_color=В цвете -format_long_strip=Веб -format_official_colored=Официально раскрашенная -format_oneshot=Сингл -format_user_created=Созданная пользователями -format_web_comic=Веб-комикс -format_yonkoma=Ёнкома -genre=Жанр -genre_action=Боевик -genre_adventure=Приключения -genre_boys_love=BL -genre_comedy=Комедия -genre_crime=Криминал -genre_drama=Драма -genre_fantasy=Фэнтези -genre_girls_love=GL -genre_historical=История -genre_horror=Ужасы -genre_isekai=Исекай -genre_magical_girls=Махо-сёдзё -genre_mecha=Меха -genre_medical=Медицина -genre_mystery=Мистика -genre_philosophical=Философия -genre_romance=Романтика -genre_sci_fi=Научная фантастика -genre_slice_of_life=Повседневность -genre_sports=Спорт -genre_superhero=Супергерои -genre_thriller=Триллер -genre_tragedy=Трагедия -genre_wuxia=Культивация -has_available_chapters=Есть главы -included_tags_mode=Включая -invalid_author_id=Недействительный ID автора -invalid_group_id=Недействительный ID группы -mode_and=И -mode_or=Или -no_group=Нет группы -no_series_in_list=Лист пуст -original_language=Язык оригинала -original_language_filter_chinese=%s (Манхуа) -original_language_filter_japanese=%s (Манга) -original_language_filter_korean=%s (Манхва) -publication_demographic=Целевая аудитория -publication_demographic_josei=Дзёсэй -publication_demographic_none=Нет -publication_demographic_seinen=Сэйнэн -publication_demographic_shoujo=Сёдзё -publication_demographic_shounen=Сёнэн -sort=Сортировать по -sort_alphabetic=Алфавиту -sort_chapter_uploaded_at=Загруженной главе -sort_content_created_at=По дате создания -sort_content_info_updated_at=По дате обновления -sort_number_of_follows=Количеству фолловеров -sort_rating=Популярности -sort_relevance=Лучшему соответствию -sort_year=Год -standard_content_rating=Рейтинг контента по умолчанию -standard_content_rating_summary=Показывать контент с выбранным рейтингом по умолчанию -standard_https_port=Использовать только HTTPS порт 443 -standard_https_port_summary=Запрашивает изображения только с серверов которые используют порт 443. Это позволяет пользователям со строгими правилами брандмауэра загружать изображения с MangaDex. -status=Статус -status_cancelled=Отменён -status_completed=Завершён -status_hiatus=Приостановлен -status_ongoing=Онгоинг -tags_mode=Режим поиска -theme=Теги -theme_aliens=Инопланетяне -theme_animals=Животные -theme_cooking=Животные -theme_crossdressing=Кроссдрессинг -theme_delinquents=Хулиганы -theme_demons=Демоны -theme_gender_swap=Смена гендера -theme_ghosts=Призраки -theme_gyaru=Гяру -theme_harem=Гарем -theme_incest=Инцест -theme_loli=Лоли -theme_mafia=Мафия -theme_magic=Магия -theme_martial_arts=Боевые исскуства -theme_military=Военные -theme_monster_girls=Монстродевушки -theme_monsters=Монстры -theme_music=Музыка -theme_ninja=Ниндзя -theme_office_workers=Офисные работники -theme_police=Полиция -theme_post_apocalyptic=Постапокалиптика -theme_psychological=Психология -theme_reincarnation=Реинкарнация -theme_reverse_harem=Обратный гарем -theme_samurai=Самураи -theme_school_life=Школа -theme_shota=Шота -theme_supernatural=Сверхъестественное -theme_survival=Выживание -theme_time_travel=Путешествие во времени -theme_traditional_games=Путешествие во времени -theme_vampires=Вампиры -theme_video_games=Видеоигры -theme_villainess=Злодейка -theme_virtual_reality=Виртуальная реальность -theme_zombies=Зомби -unable_to_process_chapter_request=Не удалось обработать ссылку на главу. Ошибка: %d -uploaded_by=Загрузил %s \ No newline at end of file diff --git a/src/all/mangadex/build.gradle b/src/all/mangadex/build.gradle deleted file mode 100644 index 101966ce6..000000000 --- a/src/all/mangadex/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlinx-serialization' - -ext { - extName = 'MangaDex' - pkgNameSuffix = 'all.mangadex' - extClass = '.MangaDexFactory' - extVersionCode = 192 - isNsfw = true -} - -dependencies { - implementation(project(":lib-i18n")) -} - -apply from: "$rootDir/common.gradle" diff --git a/src/all/mangadex/res/mipmap-hdpi/ic_launcher.png b/src/all/mangadex/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 8de6d7f3b..000000000 Binary files a/src/all/mangadex/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src/all/mangadex/res/mipmap-mdpi/ic_launcher.png b/src/all/mangadex/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 04105b6cd..000000000 Binary files a/src/all/mangadex/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src/all/mangadex/res/mipmap-xhdpi/ic_launcher.png b/src/all/mangadex/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 6f5065fa3..000000000 Binary files a/src/all/mangadex/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src/all/mangadex/res/mipmap-xxhdpi/ic_launcher.png b/src/all/mangadex/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index fc989680e..000000000 Binary files a/src/all/mangadex/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/all/mangadex/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/mangadex/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 48d4d3037..000000000 Binary files a/src/all/mangadex/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/all/mangadex/res/web_hi_res_512.png b/src/all/mangadex/res/web_hi_res_512.png deleted file mode 100644 index 24ff723c0..000000000 Binary files a/src/all/mangadex/res/web_hi_res_512.png and /dev/null differ diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt deleted file mode 100644 index 8fc471311..000000000 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MDConstants.kt +++ /dev/null @@ -1,163 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.mangadex - -import eu.kanade.tachiyomi.lib.i18n.Intl -import java.text.SimpleDateFormat -import java.util.Locale -import java.util.TimeZone -import kotlin.time.Duration.Companion.minutes - -object MDConstants { - - val uuidRegex = - Regex("[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}") - - const val mangaLimit = 20 - const val latestChapterLimit = 100 - - const val chapter = "chapter" - const val manga = "manga" - const val coverArt = "cover_art" - const val scanlationGroup = "scanlation_group" - const val user = "user" - const val author = "author" - const val artist = "artist" - const val tag = "tag" - const val list = "custom_list" - const val legacyNoGroupId = "00e03853-1b96-4f41-9542-c71b8692033b" - - const val cdnUrl = "https://uploads.mangadex.org" - const val apiUrl = "https://api.mangadex.org" - const val apiMangaUrl = "$apiUrl/manga" - const val apiChapterUrl = "$apiUrl/chapter" - const val apiListUrl = "$apiUrl/list" - const val atHomePostUrl = "https://api.mangadex.network/report" - val whitespaceRegex = "\\s".toRegex() - - val mdAtHomeTokenLifespan = 5.minutes.inWholeMilliseconds - - val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss+SSS", Locale.US) - .apply { timeZone = TimeZone.getTimeZone("UTC") } - - const val prefixIdSearch = "id:" - const val prefixChSearch = "ch:" - const val prefixGrpSearch = "grp:" - const val prefixAuthSearch = "author:" - const val prefixUsrSearch = "usr:" - const val prefixListSearch = "list:" - - private const val coverQualityPref = "thumbnailQuality" - - fun getCoverQualityPreferenceKey(dexLang: String): String { - return "${coverQualityPref}_$dexLang" - } - - fun getCoverQualityPreferenceEntries(intl: Intl) = - arrayOf(intl["cover_quality_original"], intl["cover_quality_medium"], intl["cover_quality_low"]) - - fun getCoverQualityPreferenceEntryValues() = arrayOf("", ".512.jpg", ".256.jpg") - - fun getCoverQualityPreferenceDefaultValue() = getCoverQualityPreferenceEntryValues()[0] - - private const val dataSaverPref = "dataSaverV5" - - fun getDataSaverPreferenceKey(dexLang: String): String { - return "${dataSaverPref}_$dexLang" - } - - private const val standardHttpsPortPref = "usePort443" - - fun getStandardHttpsPreferenceKey(dexLang: String): String { - return "${standardHttpsPortPref}_$dexLang" - } - - private const val contentRatingPref = "contentRating" - const val contentRatingPrefValSafe = "safe" - const val contentRatingPrefValSuggestive = "suggestive" - const val contentRatingPrefValErotica = "erotica" - const val contentRatingPrefValPornographic = "pornographic" - val contentRatingPrefDefaults = setOf(contentRatingPrefValSafe, contentRatingPrefValSuggestive) - val allContentRatings = setOf( - contentRatingPrefValSafe, - contentRatingPrefValSuggestive, - contentRatingPrefValErotica, - contentRatingPrefValPornographic, - ) - - fun getContentRatingPrefKey(dexLang: String): String { - return "${contentRatingPref}_$dexLang" - } - - private const val originalLanguagePref = "originalLanguage" - const val originalLanguagePrefValJapanese = MangaDexIntl.JAPANESE - const val originalLanguagePrefValChinese = MangaDexIntl.CHINESE - const val originalLanguagePrefValChineseHk = "zh-hk" - const val originalLanguagePrefValKorean = MangaDexIntl.KOREAN - val originalLanguagePrefDefaults = emptySet() - - fun getOriginalLanguagePrefKey(dexLang: String): String { - return "${originalLanguagePref}_$dexLang" - } - - private const val groupAzuki = "5fed0576-8b94-4f9a-b6a7-08eecd69800d" - private const val groupBilibili = "06a9fecb-b608-4f19-b93c-7caab06b7f44" - private const val groupComikey = "8d8ecf83-8d42-4f8c-add8-60963f9f28d9" - private const val groupInkr = "caa63201-4a17-4b7f-95ff-ed884a2b7e60" - private const val groupMangaHot = "319c1b10-cbd0-4f55-a46e-c4ee17e65139" - private const val groupMangaPlus = "4f1de6a2-f0c5-4ac5-bce5-02c7dbb67deb" - val defaultBlockedGroups = setOf( - groupAzuki, - groupBilibili, - groupComikey, - groupInkr, - groupMangaHot, - groupMangaPlus, - ) - private const val blockedGroupsPref = "blockedGroups" - fun getBlockedGroupsPrefKey(dexLang: String): String { - return "${blockedGroupsPref}_$dexLang" - } - - private const val blockedUploaderPref = "blockedUploader" - fun getBlockedUploaderPrefKey(dexLang: String): String { - return "${blockedUploaderPref}_$dexLang" - } - - private const val hasSanitizedUuidsPref = "hasSanitizedUuids" - fun getHasSanitizedUuidsPrefKey(dexLang: String): String { - return "${hasSanitizedUuidsPref}_$dexLang" - } - - private const val tryUsingFirstVolumeCoverPref = "tryUsingFirstVolumeCover" - const val tryUsingFirstVolumeCoverDefault = false - fun getTryUsingFirstVolumeCoverPrefKey(dexLang: String): String { - return "${tryUsingFirstVolumeCoverPref}_$dexLang" - } - - private const val altTitlesInDescPref = "altTitlesInDesc" - fun getAltTitlesInDescPrefKey(dexLang: String): String { - return "${altTitlesInDescPref}_$dexLang" - } - - private const val customUserAgentPref = "customUserAgent" - fun getCustomUserAgentPrefKey(dexLang: String): String { - return "${customUserAgentPref}_$dexLang" - } - - val defaultUserAgent = "Tachiyomi " + System.getProperty("http.agent") - - private const val tagGroupContent = "content" - private const val tagGroupFormat = "format" - private const val tagGroupGenre = "genre" - private const val tagGroupTheme = "theme" - val tagGroupsOrder = arrayOf(tagGroupContent, tagGroupFormat, tagGroupGenre, tagGroupTheme) - - const val tagAnthologyUuid = "51d83883-4103-437c-b4b1-731cb73d786c" - const val tagOneShotUuid = "0234a31e-a729-4e28-9d6a-3f87c4966b9e" - - val romanizedLangCodes = mapOf( - MangaDexIntl.JAPANESE to "ja-ro", - MangaDexIntl.KOREAN to "ko-ro", - MangaDexIntl.CHINESE to "zh-ro", - "zh-hk" to "zh-ro", - ) -} diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDex.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDex.kt deleted file mode 100644 index 06b72b1d0..000000000 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDex.kt +++ /dev/null @@ -1,903 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.mangadex - -import android.app.Application -import android.content.SharedPreferences -import android.os.Build -import android.widget.Toast -import androidx.preference.EditTextPreference -import androidx.preference.ListPreference -import androidx.preference.MultiSelectListPreference -import androidx.preference.PreferenceScreen -import androidx.preference.SwitchPreferenceCompat -import eu.kanade.tachiyomi.AppInfo -import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateVolume -import eu.kanade.tachiyomi.extension.all.mangadex.dto.AtHomeDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterListDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.CoverArtDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.CoverArtListDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.ListDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaDataDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaListDto -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.asObservable -import eu.kanade.tachiyomi.network.asObservableSuccess -import eu.kanade.tachiyomi.network.interceptor.rateLimit -import eu.kanade.tachiyomi.source.ConfigurableSource -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.MangasPage -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.HttpSource -import kotlinx.serialization.decodeFromString -import okhttp3.CacheControl -import okhttp3.Headers -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Request -import okhttp3.Response -import rx.Observable -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.util.Date - -abstract class MangaDex(final override val lang: String, private val dexLang: String = lang) : - ConfigurableSource, HttpSource() { - - override val name = MangaDexIntl.MANGADEX_NAME - - override val baseUrl = "https://mangadex.org" - - override val supportsLatest = true - - private val preferences: SharedPreferences by lazy { - Injekt.get().getSharedPreferences("source_$id", 0x0000) - } - - private val helper = MangaDexHelper(lang) - - final override fun headersBuilder(): Headers.Builder { - val extraHeader = "Android/${Build.VERSION.RELEASE} " + - "Tachiyomi/${AppInfo.getVersionName()} " + - "MangaDex/1.4.190" - - val builder = super.headersBuilder().apply { - set("Referer", "$baseUrl/") - set("Extra", extraHeader) - } - - return builder - } - - override val client = network.client.newBuilder() - .rateLimit(3) - .addInterceptor(MdAtHomeReportInterceptor(network.client, headers)) - .addInterceptor(MdUserAgentInterceptor(preferences, dexLang)) - .build() - - init { - preferences.sanitizeExistingUuidPrefs() - } - - // Popular manga section - - override fun popularMangaRequest(page: Int): Request { - val url = MDConstants.apiMangaUrl.toHttpUrl().newBuilder() - .addQueryParameter("order[followedCount]", "desc") - .addQueryParameter("availableTranslatedLanguage[]", dexLang) - .addQueryParameter("limit", MDConstants.mangaLimit.toString()) - .addQueryParameter("offset", helper.getMangaListOffset(page)) - .addQueryParameter("includes[]", MDConstants.coverArt) - .addQueryParameter("contentRating[]", preferences.contentRating) - .addQueryParameter("originalLanguage[]", preferences.originalLanguages) - .build() - - return GET(url, headers, CacheControl.FORCE_NETWORK) - } - - override fun popularMangaParse(response: Response): MangasPage { - if (response.code == 204) { - return MangasPage(emptyList(), false) - } - - val mangaListDto = response.parseAs() - - val coverSuffix = preferences.coverQuality - val firstVolumeCovers = fetchFirstVolumeCovers(mangaListDto.data).orEmpty() - - val mangaList = mangaListDto.data.map { mangaDataDto -> - val fileName = firstVolumeCovers.getOrElse(mangaDataDto.id) { - mangaDataDto.relationships - .firstInstanceOrNull() - ?.attributes?.fileName - } - helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang) - } - - return MangasPage(mangaList, mangaListDto.hasNextPage) - } - - // Latest manga section - - override fun latestUpdatesRequest(page: Int): Request { - val url = MDConstants.apiChapterUrl.toHttpUrl().newBuilder() - .addQueryParameter("offset", helper.getLatestChapterOffset(page)) - .addQueryParameter("limit", MDConstants.latestChapterLimit.toString()) - .addQueryParameter("translatedLanguage[]", dexLang) - .addQueryParameter("order[publishAt]", "desc") - .addQueryParameter("includeFutureUpdates", "0") - .addQueryParameter("originalLanguage[]", preferences.originalLanguages) - .addQueryParameter("contentRating[]", preferences.contentRating) - .addQueryParameter( - "excludedGroups[]", - MDConstants.defaultBlockedGroups + preferences.blockedGroups, - ) - .addQueryParameter("excludedUploaders[]", preferences.blockedUploaders) - .addQueryParameter("includeFuturePublishAt", "0") - .addQueryParameter("includeEmptyPages", "0") - .build() - - return GET(url, headers, CacheControl.FORCE_NETWORK) - } - - /** - * The API endpoint can't sort by date yet, so not implemented. - */ - override fun latestUpdatesParse(response: Response): MangasPage { - val chapterListDto = response.parseAs() - - val mangaIds = chapterListDto.data - .flatMap { it.relationships } - .filterIsInstance() - .map { it.id } - .distinct() - .toSet() - - val mangaApiUrl = MDConstants.apiMangaUrl.toHttpUrl().newBuilder() - .addQueryParameter("includes[]", MDConstants.coverArt) - .addQueryParameter("limit", mangaIds.size.toString()) - .addQueryParameter("contentRating[]", preferences.contentRating) - .addQueryParameter("ids[]", mangaIds) - .build() - - val mangaRequest = GET(mangaApiUrl, headers, CacheControl.FORCE_NETWORK) - val mangaResponse = client.newCall(mangaRequest).execute() - val mangaListDto = mangaResponse.parseAs() - val firstVolumeCovers = fetchFirstVolumeCovers(mangaListDto.data).orEmpty() - - val mangaDtoMap = mangaListDto.data.associateBy({ it.id }, { it }) - - val coverSuffix = preferences.coverQuality - - val mangaList = mangaIds.mapNotNull { mangaDtoMap[it] }.map { mangaDataDto -> - val fileName = firstVolumeCovers.getOrElse(mangaDataDto.id) { - mangaDataDto.relationships - .firstInstanceOrNull() - ?.attributes?.fileName - } - helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang) - } - - return MangasPage(mangaList, chapterListDto.hasNextPage) - } - - // Search manga section - - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { - return when { - query.startsWith(MDConstants.prefixChSearch) -> - getMangaIdFromChapterId(query.removePrefix(MDConstants.prefixChSearch)) - .flatMap { mangaId -> - super.fetchSearchManga( - page = page, - query = MDConstants.prefixIdSearch + mangaId, - filters = filters, - ) - } - - query.startsWith(MDConstants.prefixUsrSearch) -> - client - .newCall( - request = searchMangaUploaderRequest( - page = page, - uploader = query.removePrefix(MDConstants.prefixUsrSearch), - ), - ) - .asObservableSuccess() - .map { latestUpdatesParse(it) } - - query.startsWith(MDConstants.prefixListSearch) -> - client - .newCall( - request = searchMangaListRequest( - list = query.removePrefix(MDConstants.prefixListSearch), - ), - ) - .asObservableSuccess() - .map { searchMangaListParse(it, page, filters) } - - else -> super.fetchSearchManga(page, query.trim(), filters) - } - } - - private fun getMangaIdFromChapterId(id: String): Observable { - return client.newCall(GET("${MDConstants.apiChapterUrl}/$id", headers)) - .asObservable() - .map { response -> - if (response.isSuccessful.not()) { - throw Exception(helper.intl.format("unable_to_process_chapter_request", response.code)) - } - - response.parseAs().data!!.relationships - .firstInstanceOrNull()!!.id - } - } - - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - if (query.startsWith(MDConstants.prefixIdSearch)) { - val mangaId = query.removePrefix(MDConstants.prefixIdSearch) - - if (!helper.containsUuid(mangaId)) { - throw Exception(helper.intl["invalid_manga_id"]) - } - - val url = MDConstants.apiMangaUrl.toHttpUrl().newBuilder() - .addQueryParameter("ids[]", query.removePrefix(MDConstants.prefixIdSearch)) - .addQueryParameter("includes[]", MDConstants.coverArt) - .addQueryParameter("contentRating[]", MDConstants.allContentRatings) - .build() - - return GET(url, headers, CacheControl.FORCE_NETWORK) - } - - val tempUrl = MDConstants.apiMangaUrl.toHttpUrl().newBuilder() - .addQueryParameter("limit", MDConstants.mangaLimit.toString()) - .addQueryParameter("offset", helper.getMangaListOffset(page)) - .addQueryParameter("includes[]", MDConstants.coverArt) - - when { - query.startsWith(MDConstants.prefixGrpSearch) -> { - val groupId = query.removePrefix(MDConstants.prefixGrpSearch) - - if (!helper.containsUuid(groupId)) { - throw Exception(helper.intl["invalid_group_id"]) - } - - tempUrl.addQueryParameter("group", groupId) - } - - query.startsWith(MDConstants.prefixAuthSearch) -> { - val authorId = query.removePrefix(MDConstants.prefixAuthSearch) - - if (!helper.containsUuid(authorId)) { - throw Exception(helper.intl["invalid_author_id"]) - } - - tempUrl.addQueryParameter("authorOrArtist", authorId) - } - - else -> { - val actualQuery = query.replace(MDConstants.whitespaceRegex, " ") - - if (actualQuery.isNotBlank()) { - tempUrl.addQueryParameter("title", actualQuery) - } - } - } - - val finalUrl = helper.mdFilters.addFiltersToUrl( - url = tempUrl, - filters = filters.ifEmpty { getFilterList() }, - dexLang = dexLang, - ) - - return GET(finalUrl, headers, CacheControl.FORCE_NETWORK) - } - - override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response) - - private fun searchMangaListRequest(list: String): Request { - return GET("${MDConstants.apiListUrl}/$list", headers, CacheControl.FORCE_NETWORK) - } - - private fun searchMangaListParse(response: Response, page: Int, filters: FilterList): MangasPage { - val listDto = response.parseAs() - val listDtoFiltered = listDto.data!!.relationships.filterIsInstance() - val amount = listDtoFiltered.count() - - if (amount < 1) { - throw Exception(helper.intl["no_series_in_list"]) - } - - val minIndex = (page - 1) * MDConstants.mangaLimit - - val tempUrl = MDConstants.apiMangaUrl.toHttpUrl().newBuilder() - .addQueryParameter("limit", MDConstants.mangaLimit.toString()) - .addQueryParameter("offset", "0") - .addQueryParameter("includes[]", MDConstants.coverArt) - - val ids = listDtoFiltered - .filterIndexed { i, _ -> i >= minIndex && i < (minIndex + MDConstants.mangaLimit) } - .map(MangaDataDto::id) - .toSet() - - tempUrl.addQueryParameter("ids[]", ids) - - val finalUrl = helper.mdFilters.addFiltersToUrl( - url = tempUrl, - filters = filters.ifEmpty { getFilterList() }, - dexLang = dexLang, - ) - - val mangaRequest = GET(finalUrl, headers, CacheControl.FORCE_NETWORK) - val mangaResponse = client.newCall(mangaRequest).execute() - val mangaList = searchMangaListParse(mangaResponse) - - val hasNextPage = amount.toFloat() / MDConstants.mangaLimit - (page.toFloat() - 1) > 1 && - ids.size == MDConstants.mangaLimit - - return MangasPage(mangaList, hasNextPage) - } - - private fun searchMangaListParse(response: Response): List { - // This check will be used as the source is doing additional requests to this - // that are not parsed by the asObservableSuccess() method. It should throw the - // HttpException from the app if it becomes available in a future version of extensions-lib. - if (response.isSuccessful.not()) { - throw Exception("HTTP error ${response.code}") - } - - val mangaListDto = response.parseAs() - val firstVolumeCovers = fetchFirstVolumeCovers(mangaListDto.data).orEmpty() - - val coverSuffix = preferences.coverQuality - - val mangaList = mangaListDto.data.map { mangaDataDto -> - val fileName = firstVolumeCovers.getOrElse(mangaDataDto.id) { - mangaDataDto.relationships - .firstInstanceOrNull() - ?.attributes?.fileName - } - helper.createBasicManga(mangaDataDto, fileName, coverSuffix, dexLang) - } - - return mangaList - } - - private fun searchMangaUploaderRequest(page: Int, uploader: String): Request { - val url = MDConstants.apiChapterUrl.toHttpUrl().newBuilder() - .addQueryParameter("offset", helper.getLatestChapterOffset(page)) - .addQueryParameter("limit", MDConstants.latestChapterLimit.toString()) - .addQueryParameter("translatedLanguage[]", dexLang) - .addQueryParameter("order[publishAt]", "desc") - .addQueryParameter("includeFutureUpdates", "0") - .addQueryParameter("includeFuturePublishAt", "0") - .addQueryParameter("includeEmptyPages", "0") - .addQueryParameter("uploader", uploader) - .addQueryParameter("originalLanguage[]", preferences.originalLanguages) - .addQueryParameter("contentRating[]", preferences.contentRating) - .addQueryParameter( - "excludedGroups[]", - MDConstants.defaultBlockedGroups + preferences.blockedGroups, - ) - .addQueryParameter("excludedUploaders[]", preferences.blockedUploaders) - .build() - - return GET(url, headers, CacheControl.FORCE_NETWORK) - } - - // Manga Details section - - override fun getMangaUrl(manga: SManga): String { - return baseUrl + manga.url + "/" + helper.titleToSlug(manga.title) - } - - /** - * Get the API endpoint URL for the entry details. - * - * @throws Exception if the url is the old format so people migrate - */ - override fun mangaDetailsRequest(manga: SManga): Request { - if (!helper.containsUuid(manga.url.trim())) { - throw Exception(helper.intl["migrate_warning"]) - } - - val url = (MDConstants.apiUrl + manga.url).toHttpUrl().newBuilder() - .addQueryParameter("includes[]", MDConstants.coverArt) - .addQueryParameter("includes[]", MDConstants.author) - .addQueryParameter("includes[]", MDConstants.artist) - .build() - - return GET(url, headers, CacheControl.FORCE_NETWORK) - } - - override fun mangaDetailsParse(response: Response): SManga { - val manga = response.parseAs() - - return helper.createManga( - manga.data!!, - fetchSimpleChapterList(manga, dexLang), - fetchFirstVolumeCover(manga), - dexLang, - preferences.coverQuality, - preferences.altTitlesInDesc, - ) - } - - /** - * Get a quick-n-dirty list of the chapters to be used in determining the manga status. - * Uses the 'aggregate' endpoint. - * - * @see MangaDexHelper.getPublicationStatus - * @see AggregateDto - */ - private fun fetchSimpleChapterList(manga: MangaDto, langCode: String): Map { - val url = "${MDConstants.apiMangaUrl}/${manga.data!!.id}/aggregate?translatedLanguage[]=$langCode" - val response = client.newCall(GET(url, headers)).execute() - - return runCatching { response.parseAs() } - .getOrNull()?.volumes.orEmpty() - } - - /** - * Attempt to get the first volume cover if the setting is enabled. - * Uses the 'covers' endpoint. - * - * @see CoverArtListDto - */ - private fun fetchFirstVolumeCover(manga: MangaDto): String? { - return fetchFirstVolumeCovers(listOf(manga.data!!))?.get(manga.data.id) - } - - /** - * Attempt to get the first volume cover if the setting is enabled. - * Uses the 'covers' endpoint. - * - * @see CoverArtListDto - */ - private fun fetchFirstVolumeCovers(mangaList: List): Map? { - if (!preferences.tryUsingFirstVolumeCover || mangaList.isEmpty()) { - return null - } - - val safeMangaList = mangaList.filterNot { it.attributes?.originalLanguage.isNullOrEmpty() } - val mangaMap = safeMangaList.associate { it.id to it.attributes!! } - val locales = safeMangaList.mapNotNull { it.attributes!!.originalLanguage }.distinct() - val limit = (mangaMap.size * locales.size).coerceAtMost(100) - - val apiUrl = "${MDConstants.apiUrl}/cover".toHttpUrl().newBuilder() - .addQueryParameter("order[volume]", "asc") - .addQueryParameter("manga[]", mangaMap.keys) - .addQueryParameter("locales[]", locales.toSet()) - .addQueryParameter("limit", limit.toString()) - .addQueryParameter("offset", "0") - .build() - - val result = runCatching { - client.newCall(GET(apiUrl, headers)).execute().parseAs().data - } - - val covers = result.getOrNull() ?: return null - - return covers - .groupBy { it.relationships.firstInstanceOrNull()!!.id } - .mapValues { - it.value.find { c -> c.attributes?.locale == mangaMap[it.key]?.originalLanguage } - } - .filterValues { !it?.attributes?.fileName.isNullOrEmpty() } - .mapValues { it.value!!.attributes!!.fileName!! } - } - - // Chapter list section - - /** - * Get the API endpoint URL for the first page of chapter list. - * - * @throws Exception if the url is the old format so people migrate - */ - override fun chapterListRequest(manga: SManga): Request { - if (!helper.containsUuid(manga.url)) { - throw Exception(helper.intl["migrate_warning"]) - } - - return paginatedChapterListRequest(helper.getUUIDFromUrl(manga.url), 0) - } - - /** - * Required because the chapter list API endpoint is paginated. - */ - private fun paginatedChapterListRequest(mangaId: String, offset: Int): Request { - val url = helper.getChapterEndpoint(mangaId, offset, dexLang).toHttpUrl().newBuilder() - .addQueryParameter("contentRating[]", MDConstants.allContentRatings) - .addQueryParameter("excludedGroups[]", preferences.blockedGroups) - .addQueryParameter("excludedUploaders[]", preferences.blockedUploaders) - .build() - - return GET(url, headers, CacheControl.FORCE_NETWORK) - } - - override fun chapterListParse(response: Response): List { - if (response.code == 204) { - return emptyList() - } - - val chapterListResponse = response.parseAs() - - val chapterListResults = chapterListResponse.data.toMutableList() - - val mangaId = response.request.url.toString() - .substringBefore("/feed") - .substringAfter("${MDConstants.apiMangaUrl}/") - - var offset = chapterListResponse.offset - var hasNextPage = chapterListResponse.hasNextPage - - // Max results that can be returned is 500 so need to make more API - // calls if the chapter list response has a next page. - while (hasNextPage) { - offset += chapterListResponse.limit - - val newRequest = paginatedChapterListRequest(mangaId, offset) - val newResponse = client.newCall(newRequest).execute() - val newChapterList = newResponse.parseAs() - chapterListResults.addAll(newChapterList.data) - - hasNextPage = newChapterList.hasNextPage - } - - return chapterListResults - .filterNot { it.attributes!!.isInvalid } - .map(helper::createChapter) - } - - override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url - - override fun pageListRequest(chapter: SChapter): Request { - if (!helper.containsUuid(chapter.url)) { - throw Exception(helper.intl["migrate_warning"]) - } - - val chapterId = chapter.url.substringAfter("/chapter/") - val atHomeRequestUrl = if (preferences.forceStandardHttps) { - "${MDConstants.apiUrl}/at-home/server/$chapterId?forcePort443=true" - } else { - "${MDConstants.apiUrl}/at-home/server/$chapterId" - } - - return helper.mdAtHomeRequest(atHomeRequestUrl, headers, CacheControl.FORCE_NETWORK) - } - - override fun pageListParse(response: Response): List { - val atHomeRequestUrl = response.request.url - val atHomeDto = response.parseAs() - val host = atHomeDto.baseUrl - - // Have to add the time, and url to the page because pages timeout within 30 minutes now. - val now = Date().time - - val hash = atHomeDto.chapter.hash - val pageSuffix = if (preferences.useDataSaver) { - atHomeDto.chapter.dataSaver.map { "/data-saver/$hash/$it" } - } else { - atHomeDto.chapter.data.map { "/data/$hash/$it" } - } - - return pageSuffix.mapIndexed { index, imgUrl -> - val mdAtHomeMetadataUrl = "$host,$atHomeRequestUrl,$now" - Page(index, mdAtHomeMetadataUrl, imgUrl) - } - } - - override fun imageRequest(page: Page): Request { - return helper.getValidImageUrlForPage(page, headers, client) - } - - override fun imageUrlParse(response: Response): String = "" - - @Suppress("UNCHECKED_CAST") - override fun setupPreferenceScreen(screen: PreferenceScreen) { - val coverQualityPref = ListPreference(screen.context).apply { - key = MDConstants.getCoverQualityPreferenceKey(dexLang) - title = helper.intl["cover_quality"] - entries = MDConstants.getCoverQualityPreferenceEntries(helper.intl) - entryValues = MDConstants.getCoverQualityPreferenceEntryValues() - setDefaultValue(MDConstants.getCoverQualityPreferenceDefaultValue()) - summary = "%s" - - setOnPreferenceChangeListener { _, newValue -> - val selected = newValue as String - val index = findIndexOfValue(selected) - val entry = entryValues[index] as String - - preferences.edit() - .putString(MDConstants.getCoverQualityPreferenceKey(dexLang), entry) - .commit() - } - } - - val tryUsingFirstVolumeCoverPref = SwitchPreferenceCompat(screen.context).apply { - key = MDConstants.getTryUsingFirstVolumeCoverPrefKey(dexLang) - title = helper.intl["try_using_first_volume_cover"] - summary = helper.intl["try_using_first_volume_cover_summary"] - setDefaultValue(MDConstants.tryUsingFirstVolumeCoverDefault) - - setOnPreferenceChangeListener { _, newValue -> - val checkValue = newValue as Boolean - - preferences.edit() - .putBoolean(MDConstants.getTryUsingFirstVolumeCoverPrefKey(dexLang), checkValue) - .commit() - } - } - - val dataSaverPref = SwitchPreferenceCompat(screen.context).apply { - key = MDConstants.getDataSaverPreferenceKey(dexLang) - title = helper.intl["data_saver"] - summary = helper.intl["data_saver_summary"] - setDefaultValue(false) - - setOnPreferenceChangeListener { _, newValue -> - val checkValue = newValue as Boolean - - preferences.edit() - .putBoolean(MDConstants.getDataSaverPreferenceKey(dexLang), checkValue) - .commit() - } - } - - val standardHttpsPortPref = SwitchPreferenceCompat(screen.context).apply { - key = MDConstants.getStandardHttpsPreferenceKey(dexLang) - title = helper.intl["standard_https_port"] - summary = helper.intl["standard_https_port_summary"] - setDefaultValue(false) - - setOnPreferenceChangeListener { _, newValue -> - val checkValue = newValue as Boolean - - preferences.edit() - .putBoolean(MDConstants.getStandardHttpsPreferenceKey(dexLang), checkValue) - .commit() - } - } - - val contentRatingPref = MultiSelectListPreference(screen.context).apply { - key = MDConstants.getContentRatingPrefKey(dexLang) - title = helper.intl["standard_content_rating"] - summary = helper.intl["standard_content_rating_summary"] - entries = arrayOf( - helper.intl["content_rating_safe"], - helper.intl["content_rating_suggestive"], - helper.intl["content_rating_erotica"], - helper.intl["content_rating_pornographic"], - ) - entryValues = arrayOf( - MDConstants.contentRatingPrefValSafe, - MDConstants.contentRatingPrefValSuggestive, - MDConstants.contentRatingPrefValErotica, - MDConstants.contentRatingPrefValPornographic, - ) - setDefaultValue(MDConstants.contentRatingPrefDefaults) - - setOnPreferenceChangeListener { _, newValue -> - val checkValue = newValue as Set - - preferences.edit() - .putStringSet(MDConstants.getContentRatingPrefKey(dexLang), checkValue) - .commit() - } - } - - val originalLanguagePref = MultiSelectListPreference(screen.context).apply { - key = MDConstants.getOriginalLanguagePrefKey(dexLang) - title = helper.intl["filter_original_languages"] - summary = helper.intl["filter_original_languages_summary"] - 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(MDConstants.originalLanguagePrefDefaults) - - setOnPreferenceChangeListener { _, newValue -> - val checkValue = newValue as Set - - preferences.edit() - .putStringSet(MDConstants.getOriginalLanguagePrefKey(dexLang), checkValue) - .commit() - } - } - - val blockedGroupsPref = EditTextPreference(screen.context).apply { - key = MDConstants.getBlockedGroupsPrefKey(dexLang) - title = helper.intl["block_group_by_uuid"] - summary = helper.intl["block_group_by_uuid_summary"] - - setOnBindEditTextListener(helper::setupEditTextUuidValidator) - - setOnPreferenceChangeListener { _, newValue -> - preferences.edit() - .putString(MDConstants.getBlockedGroupsPrefKey(dexLang), newValue.toString()) - .commit() - } - } - - val blockedUploaderPref = EditTextPreference(screen.context).apply { - key = MDConstants.getBlockedUploaderPrefKey(dexLang) - title = helper.intl["block_uploader_by_uuid"] - summary = helper.intl["block_uploader_by_uuid_summary"] - - setOnBindEditTextListener(helper::setupEditTextUuidValidator) - - setOnPreferenceChangeListener { _, newValue -> - preferences.edit() - .putString(MDConstants.getBlockedUploaderPrefKey(dexLang), newValue.toString()) - .commit() - } - } - - val altTitlesInDescPref = SwitchPreferenceCompat(screen.context).apply { - key = MDConstants.getAltTitlesInDescPrefKey(dexLang) - title = helper.intl["alternative_titles_in_description"] - summary = helper.intl["alternative_titles_in_description_summary"] - setDefaultValue(false) - - setOnPreferenceChangeListener { _, newValue -> - val checkValue = newValue as Boolean - - preferences.edit() - .putBoolean(MDConstants.getAltTitlesInDescPrefKey(dexLang), checkValue) - .commit() - } - } - - val userAgentPref = EditTextPreference(screen.context).apply { - key = MDConstants.getCustomUserAgentPrefKey(dexLang) - title = helper.intl["set_custom_useragent"] - summary = helper.intl["set_custom_useragent_summary"] - dialogMessage = helper.intl.format( - "set_custom_useragent_dialog", - MDConstants.defaultUserAgent, - ) - - setDefaultValue(MDConstants.defaultUserAgent) - - setOnPreferenceChangeListener { _, newValue -> - try { - Headers.Builder().add("User-Agent", newValue as String) - summary = newValue - true - } catch (e: Throwable) { - val errorMessage = helper.intl.format("set_custom_useragent_error_invalid", e.message) - Toast.makeText(screen.context, errorMessage, Toast.LENGTH_LONG).show() - false - } - } - } - - screen.addPreference(coverQualityPref) - screen.addPreference(tryUsingFirstVolumeCoverPref) - screen.addPreference(dataSaverPref) - screen.addPreference(standardHttpsPortPref) - screen.addPreference(altTitlesInDescPref) - screen.addPreference(contentRatingPref) - screen.addPreference(originalLanguagePref) - screen.addPreference(blockedGroupsPref) - screen.addPreference(blockedUploaderPref) - screen.addPreference(userAgentPref) - } - - override fun getFilterList(): FilterList = - helper.mdFilters.getMDFilterList(preferences, dexLang, helper.intl) - - private fun HttpUrl.Builder.addQueryParameter(name: String, value: Set?) = apply { - value?.forEach { addQueryParameter(name, it) } - } - - private inline fun Response.parseAs(): T = use { - helper.json.decodeFromString(body.string()) - } - - private inline fun List<*>.firstInstanceOrNull(): T? = - firstOrNull { it is T } as? T? - - private val SharedPreferences.contentRating - get() = getStringSet( - MDConstants.getContentRatingPrefKey(dexLang), - MDConstants.contentRatingPrefDefaults, - ) - - private val SharedPreferences.originalLanguages: Set - get() { - val prefValues = getStringSet( - MDConstants.getOriginalLanguagePrefKey(dexLang), - MDConstants.originalLanguagePrefDefaults, - ) - - val originalLanguages = prefValues.orEmpty().toMutableSet() - - if (MDConstants.originalLanguagePrefValChinese in originalLanguages) { - originalLanguages.add(MDConstants.originalLanguagePrefValChineseHk) - } - - return originalLanguages - } - - private val SharedPreferences.coverQuality - get() = getString(MDConstants.getCoverQualityPreferenceKey(dexLang), "") - - private val SharedPreferences.tryUsingFirstVolumeCover - get() = getBoolean( - MDConstants.getTryUsingFirstVolumeCoverPrefKey(dexLang), - MDConstants.tryUsingFirstVolumeCoverDefault, - ) - - private val SharedPreferences.blockedGroups - get() = getString(MDConstants.getBlockedGroupsPrefKey(dexLang), "") - ?.split(",") - ?.map(String::trim) - ?.filter(String::isNotEmpty) - ?.sorted() - .orEmpty() - .toSet() - - private val SharedPreferences.blockedUploaders - get() = getString(MDConstants.getBlockedUploaderPrefKey(dexLang), "") - ?.split(",") - ?.map(String::trim) - ?.filter(String::isNotEmpty) - ?.sorted() - .orEmpty() - .toSet() - - private val SharedPreferences.forceStandardHttps - get() = getBoolean(MDConstants.getStandardHttpsPreferenceKey(dexLang), false) - - private val SharedPreferences.useDataSaver - get() = getBoolean(MDConstants.getDataSaverPreferenceKey(dexLang), false) - - private val SharedPreferences.altTitlesInDesc - get() = getBoolean(MDConstants.getAltTitlesInDescPrefKey(dexLang), false) - - private val SharedPreferences.customUserAgent - get() = getString( - MDConstants.getCustomUserAgentPrefKey(dexLang), - MDConstants.defaultUserAgent, - ) - - /** - * Previous versions of the extension allowed invalid UUID values to be stored in the - * preferences. This method clear invalid UUIDs in case the user have updated from - * a previous version with that behaviour. - */ - private fun SharedPreferences.sanitizeExistingUuidPrefs() { - if (getBoolean(MDConstants.getHasSanitizedUuidsPrefKey(dexLang), false)) { - return - } - - val blockedGroups = getString(MDConstants.getBlockedGroupsPrefKey(dexLang), "")!! - .split(",") - .map(String::trim) - .filter(helper::isUuid) - .joinToString(", ") - - val blockedUploaders = getString(MDConstants.getBlockedUploaderPrefKey(dexLang), "")!! - .split(",") - .map(String::trim) - .filter(helper::isUuid) - .joinToString(", ") - - edit() - .putString(MDConstants.getBlockedGroupsPrefKey(dexLang), blockedGroups) - .putString(MDConstants.getBlockedUploaderPrefKey(dexLang), blockedUploaders) - .putBoolean(MDConstants.getHasSanitizedUuidsPrefKey(dexLang), true) - .apply() - } -} diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFactory.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFactory.kt deleted file mode 100644 index 10a39bc6c..000000000 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFactory.kt +++ /dev/null @@ -1,116 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.mangadex - -import eu.kanade.tachiyomi.source.Source -import eu.kanade.tachiyomi.source.SourceFactory - -class MangaDexFactory : SourceFactory { - override fun createSources(): List = listOf( - MangaDexEnglish(), - MangaDexAlbanian(), - MangaDexArabic(), - MangaDexAzerbaijani(), - MangaDexBengali(), - MangaDexBulgarian(), - MangaDexBurmese(), - MangaDexCatalan(), - MangaDexChineseSimplified(), - MangaDexChineseTraditional(), - MangaDexCroatian(), - MangaDexCzech(), - MangaDexDanish(), - MangaDexDutch(), - MangaDexEsperanto(), - MangaDexEstonian(), - MangaDexFilipino(), - MangaDexFinnish(), - MangaDexFrench(), - MangaDexGeorgian(), - MangaDexGerman(), - MangaDexGreek(), - MangaDexHebrew(), - MangaDexHindi(), - MangaDexHungarian(), - MangaDexIndonesian(), - MangaDexItalian(), - MangaDexJapanese(), - MangaDexKazakh(), - MangaDexKorean(), - MangaDexLatin(), - MangaDexLithuanian(), - MangaDexMalay(), - MangaDexMongolian(), - MangaDexNepali(), - MangaDexNorwegian(), - MangaDexPersian(), - MangaDexPolish(), - MangaDexPortugueseBrazil(), - MangaDexPortuguesePortugal(), - MangaDexRomanian(), - MangaDexRussian(), - MangaDexSerbian(), - MangaDexSlovak(), - MangaDexSpanishLatinAmerica(), - MangaDexSpanishSpain(), - MangaDexSwedish(), - MangaDexTamil(), - MangaDexTelugu(), - MangaDexThai(), - MangaDexTurkish(), - MangaDexUkrainian(), - MangaDexVietnamese(), - ) -} - -class MangaDexAlbanian : MangaDex("sq") -class MangaDexArabic : MangaDex("ar") -class MangaDexAzerbaijani : MangaDex("az") -class MangaDexBengali : MangaDex("bn") -class MangaDexBulgarian : MangaDex("bg") -class MangaDexBurmese : MangaDex("my") -class MangaDexCatalan : MangaDex("ca") -class MangaDexChineseSimplified : MangaDex("zh-Hans", "zh") -class MangaDexChineseTraditional : MangaDex("zh-Hant", "zh-hk") -class MangaDexCroatian : MangaDex("hr") -class MangaDexCzech : MangaDex("cs") -class MangaDexDanish : MangaDex("da") -class MangaDexDutch : MangaDex("nl") -class MangaDexEnglish : MangaDex("en") -class MangaDexEsperanto : MangaDex("eo") -class MangaDexEstonian : MangaDex("et") -class MangaDexFilipino : MangaDex("fil", "tl") -class MangaDexFinnish : MangaDex("fi") -class MangaDexFrench : MangaDex("fr") -class MangaDexGeorgian : MangaDex("ka") -class MangaDexGerman : MangaDex("de") -class MangaDexGreek : MangaDex("el") -class MangaDexHebrew : MangaDex("he") -class MangaDexHindi : MangaDex("hi") -class MangaDexHungarian : MangaDex("hu") -class MangaDexIndonesian : MangaDex("id") -class MangaDexItalian : MangaDex("it") -class MangaDexJapanese : MangaDex("ja") -class MangaDexKazakh : MangaDex("kk") -class MangaDexKorean : MangaDex("ko") -class MangaDexLatin : MangaDex("la") -class MangaDexLithuanian : MangaDex("lt") -class MangaDexMalay : MangaDex("ms") -class MangaDexMongolian : MangaDex("mn") -class MangaDexNepali : MangaDex("ne") -class MangaDexNorwegian : MangaDex("no") -class MangaDexPersian : MangaDex("fa") -class MangaDexPolish : MangaDex("pl") -class MangaDexPortugueseBrazil : MangaDex("pt-BR", "pt-br") -class MangaDexPortuguesePortugal : MangaDex("pt") -class MangaDexRomanian : MangaDex("ro") -class MangaDexRussian : MangaDex("ru") -class MangaDexSerbian : MangaDex("sr") -class MangaDexSlovak : MangaDex("sk") -class MangaDexSpanishLatinAmerica : MangaDex("es-419", "es-la") -class MangaDexSpanishSpain : MangaDex("es") -class MangaDexSwedish : MangaDex("sv") -class MangaDexTamil : MangaDex("ta") -class MangaDexTelugu : MangaDex("te") -class MangaDexThai : MangaDex("th") -class MangaDexTurkish : MangaDex("tr") -class MangaDexUkrainian : MangaDex("uk") -class MangaDexVietnamese : MangaDex("vi") diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFilters.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFilters.kt deleted file mode 100644 index 05b452a01..000000000 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexFilters.kt +++ /dev/null @@ -1,400 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.mangadex - -import android.content.SharedPreferences -import eu.kanade.tachiyomi.extension.all.mangadex.dto.ContentRatingDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.PublicationDemographicDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.StatusDto -import eu.kanade.tachiyomi.lib.i18n.Intl -import eu.kanade.tachiyomi.source.model.Filter -import eu.kanade.tachiyomi.source.model.FilterList -import okhttp3.HttpUrl - -class MangaDexFilters { - - internal fun getMDFilterList( - preferences: SharedPreferences, - dexLang: String, - intl: Intl, - ): FilterList = FilterList( - 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 interface UrlQueryFilter { - fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) - } - - private class HasAvailableChaptersFilter(intl: Intl) : - Filter.CheckBox(intl["has_available_chapters"]), - UrlQueryFilter { - - override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { - if (state) { - url.addQueryParameter("hasAvailableChapters", "true") - url.addQueryParameter("availableTranslatedLanguage[]", dexLang) - } - } - } - - private class OriginalLanguage( - name: String, - val isoCode: String, - state: Boolean = false, - ) : Filter.CheckBox(name, state) - private class OriginalLanguageList(intl: Intl, originalLanguage: List) : - Filter.Group(intl["original_language"], originalLanguage), - UrlQueryFilter { - - override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { - state.filter(OriginalLanguage::state) - .forEach { lang -> - // 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: Intl, - ): List { - val originalLanguages = preferences.getStringSet( - MDConstants.getOriginalLanguagePrefKey(dexLang), - setOf(), - )!! - - return listOf( - OriginalLanguage( - name = intl.format( - "original_language_filter_japanese", - intl.languageDisplayName(MangaDexIntl.JAPANESE), - ), - isoCode = MDConstants.originalLanguagePrefValJapanese, - state = MDConstants.originalLanguagePrefValJapanese in originalLanguages, - ), - OriginalLanguage( - name = intl.format( - "original_language_filter_chinese", - intl.languageDisplayName(MangaDexIntl.CHINESE), - ), - isoCode = MDConstants.originalLanguagePrefValChinese, - state = MDConstants.originalLanguagePrefValChinese in originalLanguages, - ), - OriginalLanguage( - name = intl.format( - "original_language_filter_korean", - intl.languageDisplayName(MangaDexIntl.KOREAN), - ), - isoCode = MDConstants.originalLanguagePrefValKorean, - state = MDConstants.originalLanguagePrefValKorean in originalLanguages, - ), - ) - } - - private class ContentRating(name: String, val value: String) : Filter.CheckBox(name) - private class ContentRatingList(intl: Intl, contentRating: List) : - Filter.Group(intl["content_rating"], contentRating), - UrlQueryFilter { - - override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { - state.filter(ContentRating::state) - .forEach { url.addQueryParameter("contentRating[]", it.value) } - } - } - - private fun getContentRating( - preferences: SharedPreferences, - dexLang: String, - intl: Intl, - ): List { - val contentRatings = preferences.getStringSet( - MDConstants.getContentRatingPrefKey(dexLang), - MDConstants.contentRatingPrefDefaults, - ) - - return listOf( - ContentRating(intl["content_rating_safe"], ContentRatingDto.SAFE.value).apply { - state = contentRatings?.contains(MDConstants.contentRatingPrefValSafe) ?: true - }, - ContentRating(intl["content_rating_suggestive"], ContentRatingDto.SUGGESTIVE.value).apply { - state = contentRatings?.contains(MDConstants.contentRatingPrefValSuggestive) ?: true - }, - ContentRating(intl["content_rating_erotica"], ContentRatingDto.EROTICA.value).apply { - state = contentRatings?.contains(MDConstants.contentRatingPrefValErotica) ?: false - }, - ContentRating(intl["content_rating_pornographic"], ContentRatingDto.PORNOGRAPHIC.value).apply { - state = contentRatings?.contains(MDConstants.contentRatingPrefValPornographic) ?: false - }, - ) - } - - private class Demographic(name: String, val value: String) : Filter.CheckBox(name) - private class DemographicList(intl: Intl, demographics: List) : - Filter.Group(intl["publication_demographic"], demographics), - UrlQueryFilter { - - override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { - state.filter(Demographic::state) - .forEach { url.addQueryParameter("publicationDemographic[]", it.value) } - } - } - - private fun getDemographics(intl: Intl) = listOf( - Demographic(intl["publication_demographic_none"], PublicationDemographicDto.NONE.value), - Demographic(intl["publication_demographic_shounen"], PublicationDemographicDto.SHOUNEN.value), - Demographic(intl["publication_demographic_shoujo"], PublicationDemographicDto.SHOUJO.value), - Demographic(intl["publication_demographic_seinen"], PublicationDemographicDto.SEINEN.value), - Demographic(intl["publication_demographic_josei"], PublicationDemographicDto.JOSEI.value), - ) - - private class Status(name: String, val value: String) : Filter.CheckBox(name) - private class StatusList(intl: Intl, status: List) : - Filter.Group(intl["status"], status), - UrlQueryFilter { - - override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { - state.filter(Status::state) - .forEach { url.addQueryParameter("status[]", it.value) } - } - } - - private fun getStatus(intl: Intl) = listOf( - Status(intl["status_ongoing"], StatusDto.ONGOING.value), - Status(intl["status_completed"], StatusDto.COMPLETED.value), - Status(intl["status_hiatus"], StatusDto.HIATUS.value), - Status(intl["status_cancelled"], StatusDto.CANCELLED.value), - ) - - data class Sortable(val title: String, val value: String) { - override fun toString(): String = title - } - - private fun getSortables(intl: Intl) = arrayOf( - Sortable(intl["sort_alphabetic"], "title"), - Sortable(intl["sort_chapter_uploaded_at"], "latestUploadedChapter"), - Sortable(intl["sort_number_of_follows"], "followedCount"), - Sortable(intl["sort_content_created_at"], "createdAt"), - Sortable(intl["sort_content_info_updated_at"], "updatedAt"), - Sortable(intl["sort_relevance"], "relevance"), - Sortable(intl["sort_year"], "year"), - Sortable(intl["sort_rating"], "rating"), - ) - - class SortFilter(intl: Intl, private val sortables: Array) : - Filter.Sort( - intl["sort"], - sortables.map(Sortable::title).toTypedArray(), - Selection(5, false), - ), - UrlQueryFilter { - - override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { - if (state != null) { - val query = sortables[state!!.index].value - val value = if (state!!.ascending) "asc" else "desc" - - url.addQueryParameter("order[$query]", value) - } - } - } - - internal class Tag(val id: String, name: String) : Filter.TriState(name) - - private class TagList(collection: String, tags: List) : - Filter.Group(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) - } - } - } - } - - private fun getContents(intl: Intl): List { - val tags = listOf( - Tag("b29d6a3d-1569-4e7a-8caf-7557bc92cd5d", intl["content_gore"]), - Tag("97893a4c-12af-4dac-b6be-0dffb353568e", intl["content_sexual_violence"]), - ) - - return tags.sortIfTranslated(intl) - } - - private fun getFormats(intl: Intl): List { - val tags = listOf( - Tag("b11fda93-8f1d-4bef-b2ed-8803d3733170", intl["format_yonkoma"]), - Tag("f4122d1c-3b44-44d0-9936-ff7502c39ad3", intl["format_adaptation"]), - Tag("51d83883-4103-437c-b4b1-731cb73d786c", intl["format_anthology"]), - Tag("0a39b5a1-b235-4886-a747-1d05d216532d", intl["format_award_winning"]), - Tag("b13b2a48-c720-44a9-9c77-39c9979373fb", intl["format_doujinshi"]), - Tag("7b2ce280-79ef-4c09-9b58-12b7c23a9b78", intl["format_fan_colored"]), - Tag("f5ba408b-0e7a-484d-8d49-4e9125ac96de", intl["format_full_color"]), - Tag("3e2b8dae-350e-4ab8-a8ce-016e844b9f0d", intl["format_long_strip"]), - Tag("320831a8-4026-470b-94f6-8353740e6f04", intl["format_official_colored"]), - Tag("0234a31e-a729-4e28-9d6a-3f87c4966b9e", intl["format_oneshot"]), - Tag("891cf039-b895-47f0-9229-bef4c96eccd4", intl["format_user_created"]), - Tag("e197df38-d0e7-43b5-9b09-2842d0c326dd", intl["format_web_comic"]), - ) - - return tags.sortIfTranslated(intl) - } - - private fun getGenres(intl: Intl): List { - val tags = listOf( - Tag("391b0423-d847-456f-aff0-8b0cfc03066b", intl["genre_action"]), - Tag("87cc87cd-a395-47af-b27a-93258283bbc6", intl["genre_adventure"]), - Tag("5920b825-4181-4a17-beeb-9918b0ff7a30", intl["genre_boys_love"]), - Tag("4d32cc48-9f00-4cca-9b5a-a839f0764984", intl["genre_comedy"]), - Tag("5ca48985-9a9d-4bd8-be29-80dc0303db72", intl["genre_crime"]), - Tag("b9af3a63-f058-46de-a9a0-e0c13906197a", intl["genre_drama"]), - Tag("cdc58593-87dd-415e-bbc0-2ec27bf404cc", intl["genre_fantasy"]), - Tag("a3c67850-4684-404e-9b7f-c69850ee5da6", intl["genre_girls_love"]), - Tag("33771934-028e-4cb3-8744-691e866a923e", intl["genre_historical"]), - Tag("cdad7e68-1419-41dd-bdce-27753074a640", intl["genre_horror"]), - Tag("ace04997-f6bd-436e-b261-779182193d3d", intl["genre_isekai"]), - Tag("81c836c9-914a-4eca-981a-560dad663e73", intl["genre_magical_girls"]), - Tag("50880a9d-5440-4732-9afb-8f457127e836", intl["genre_mecha"]), - Tag("c8cbe35b-1b2b-4a3f-9c37-db84c4514856", intl["genre_medical"]), - Tag("ee968100-4191-4968-93d3-f82d72be7e46", intl["genre_mystery"]), - Tag("b1e97889-25b4-4258-b28b-cd7f4d28ea9b", intl["genre_philosophical"]), - Tag("423e2eae-a7a2-4a8b-ac03-a8351462d71d", intl["genre_romance"]), - Tag("256c8bd9-4904-4360-bf4f-508a76d67183", intl["genre_sci_fi"]), - Tag("e5301a23-ebd9-49dd-a0cb-2add944c7fe9", intl["genre_slice_of_life"]), - Tag("69964a64-2f90-4d33-beeb-f3ed2875eb4c", intl["genre_sports"]), - Tag("7064a261-a137-4d3a-8848-2d385de3a99c", intl["genre_superhero"]), - Tag("07251805-a27e-4d59-b488-f0bfbec15168", intl["genre_thriller"]), - Tag("f8f62932-27da-4fe4-8ee1-6779a8c5edba", intl["genre_tragedy"]), - Tag("acc803a4-c95a-4c22-86fc-eb6b582d82a2", intl["genre_wuxia"]), - ) - - return tags.sortIfTranslated(intl) - } - - private fun getThemes(intl: Intl): List { - val tags = listOf( - Tag("e64f6742-c834-471d-8d72-dd51fc02b835", intl["theme_aliens"]), - Tag("3de8c75d-8ee3-48ff-98ee-e20a65c86451", intl["theme_animals"]), - Tag("ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", intl["theme_cooking"]), - Tag("9ab53f92-3eed-4e9b-903a-917c86035ee3", intl["theme_crossdressing"]), - Tag("da2d50ca-3018-4cc0-ac7a-6b7d472a29ea", intl["theme_delinquents"]), - Tag("39730448-9a5f-48a2-85b0-a70db87b1233", intl["theme_demons"]), - Tag("2bd2e8d0-f146-434a-9b51-fc9ff2c5fe6a", intl["theme_gender_swap"]), - Tag("3bb26d85-09d5-4d2e-880c-c34b974339e9", intl["theme_ghosts"]), - Tag("fad12b5e-68ba-460e-b933-9ae8318f5b65", intl["theme_gyaru"]), - Tag("aafb99c1-7f60-43fa-b75f-fc9502ce29c7", intl["theme_harem"]), - Tag("5bd0e105-4481-44ca-b6e7-7544da56b1a3", intl["theme_incest"]), - Tag("2d1f5d56-a1e5-4d0d-a961-2193588b08ec", intl["theme_loli"]), - Tag("85daba54-a71c-4554-8a28-9901a8b0afad", intl["theme_mafia"]), - Tag("a1f53773-c69a-4ce5-8cab-fffcd90b1565", intl["theme_magic"]), - Tag("799c202e-7daa-44eb-9cf7-8a3c0441531e", intl["theme_martial_arts"]), - Tag("ac72833b-c4e9-4878-b9db-6c8a4a99444a", intl["theme_military"]), - Tag("dd1f77c5-dea9-4e2b-97ae-224af09caf99", intl["theme_monster_girls"]), - Tag("36fd93ea-e8b8-445e-b836-358f02b3d33d", intl["theme_monsters"]), - Tag("f42fbf9e-188a-447b-9fdc-f19dc1e4d685", intl["theme_music"]), - Tag("489dd859-9b61-4c37-af75-5b18e88daafc", intl["theme_ninja"]), - Tag("92d6d951-ca5e-429c-ac78-451071cbf064", intl["theme_office_workers"]), - Tag("df33b754-73a3-4c54-80e6-1a74a8058539", intl["theme_police"]), - Tag("9467335a-1b83-4497-9231-765337a00b96", intl["theme_post_apocalyptic"]), - Tag("3b60b75c-a2d7-4860-ab56-05f391bb889c", intl["theme_psychological"]), - Tag("0bc90acb-ccc1-44ca-a34a-b9f3a73259d0", intl["theme_reincarnation"]), - Tag("65761a2a-415e-47f3-bef2-a9dababba7a6", intl["theme_reverse_harem"]), - Tag("81183756-1453-4c81-aa9e-f6e1b63be016", intl["theme_samurai"]), - Tag("caaa44eb-cd40-4177-b930-79d3ef2afe87", intl["theme_school_life"]), - Tag("ddefd648-5140-4e5f-ba18-4eca4071d19b", intl["theme_shota"]), - Tag("eabc5b4c-6aff-42f3-b657-3e90cbd00b75", intl["theme_supernatural"]), - Tag("5fff9cde-849c-4d78-aab0-0d52b2ee1d25", intl["theme_survival"]), - Tag("292e862b-2d17-4062-90a2-0356caa4ae27", intl["theme_time_travel"]), - Tag("31932a7e-5b8e-49a6-9f12-2afa39dc544c", intl["theme_traditional_games"]), - Tag("d7d1730f-6eb0-4ba6-9437-602cac38664c", intl["theme_vampires"]), - Tag("9438db5a-7e2a-4ac0-b39e-e0d95a34b8a8", intl["theme_video_games"]), - Tag("d14322ac-4d6f-4e9b-afd9-629d5f4d8a41", intl["theme_villainess"]), - Tag("8c86611e-fab7-4986-9dec-d1a2f44acdd5", intl["theme_virtual_reality"]), - Tag("631ef465-9aba-4afb-b0fc-ea10efe274a8", intl["theme_zombies"]), - ) - - return tags.sortIfTranslated(intl) - } - - // to get all tags from dex https://api.mangadex.org/manga/tag - internal fun getTags(intl: Intl): List { - 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: Intl) = arrayOf( - TagMode(intl["mode_and"], "AND"), - TagMode(intl["mode_or"], "OR"), - ) - - private class TagInclusionMode(intl: Intl, modes: Array) : - Filter.Select(intl["included_tags_mode"], modes, 0), - UrlQueryFilter { - - override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { - url.addQueryParameter("includedTagsMode", values[state].value) - } - } - - private class TagExclusionMode(intl: Intl, modes: Array) : - Filter.Select(intl["excluded_tags_mode"], modes, 1), - UrlQueryFilter { - - override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { - url.addQueryParameter("excludedTagsMode", values[state].value) - } - } - - private class TagsFilter(intl: Intl, innerFilters: FilterList) : - Filter.Group>(intl["tags_mode"], innerFilters), - UrlQueryFilter { - - override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { - state.filterIsInstance() - .forEach { filter -> filter.addQueryParameter(url, dexLang) } - } - } - - private fun getTagFilters(intl: Intl): FilterList = FilterList( - TagInclusionMode(intl, getTagModes(intl)), - TagExclusionMode(intl, getTagModes(intl)), - ) - - internal fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList, dexLang: String): HttpUrl { - filters.filterIsInstance() - .forEach { filter -> filter.addQueryParameter(url, dexLang) } - - return url.build() - } - - private fun List.sortIfTranslated(intl: Intl): List = apply { - if (intl.chosenLanguage == MangaDexIntl.ENGLISH) { - return this - } - - return sortedWith(compareBy(intl.collator, Tag::name)) - } -} diff --git a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexHelper.kt b/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexHelper.kt deleted file mode 100644 index 1a72db3bb..000000000 --- a/src/all/mangadex/src/eu/kanade/tachiyomi/extension/all/mangadex/MangaDexHelper.kt +++ /dev/null @@ -1,490 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.mangadex - -import android.text.Editable -import android.text.TextWatcher -import android.util.Log -import android.widget.Button -import android.widget.EditText -import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateVolume -import eu.kanade.tachiyomi.extension.all.mangadex.dto.ArtistDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.AtHomeDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.AttributesDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.AuthorArtistAttributesDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.AuthorDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterAttributesDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.ChapterDataDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.ContentRatingDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.CoverArtAttributesDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.CoverArtDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.EntityDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.ListAttributesDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.ListDataDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaAttributesDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.MangaDataDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.ScanlationGroupAttributes -import eu.kanade.tachiyomi.extension.all.mangadex.dto.ScanlationGroupDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.StatusDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.TagAttributesDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.TagDto -import eu.kanade.tachiyomi.extension.all.mangadex.dto.UnknownEntity -import eu.kanade.tachiyomi.extension.all.mangadex.dto.UserAttributes -import eu.kanade.tachiyomi.extension.all.mangadex.dto.UserDto -import eu.kanade.tachiyomi.lib.i18n.Intl -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.plus -import kotlinx.serialization.modules.polymorphic -import kotlinx.serialization.modules.subclass -import okhttp3.CacheControl -import okhttp3.Headers -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.OkHttpClient -import okhttp3.Request -import org.jsoup.parser.Parser -import java.util.Date -import java.util.Locale -import java.util.concurrent.TimeUnit - -class MangaDexHelper(lang: String) { - - val mdFilters = MangaDexFilters() - - val json = Json { - isLenient = true - ignoreUnknownKeys = true - allowSpecialFloatingPointValues = true - prettyPrint = true - serializersModule += SerializersModule { - polymorphic(EntityDto::class) { - subclass(AuthorDto::class) - subclass(ArtistDto::class) - subclass(ChapterDataDto::class) - subclass(CoverArtDto::class) - subclass(ListDataDto::class) - subclass(MangaDataDto::class) - subclass(ScanlationGroupDto::class) - subclass(TagDto::class) - subclass(UserDto::class) - defaultDeserializer { UnknownEntity.serializer() } - } - - polymorphic(AttributesDto::class) { - subclass(AuthorArtistAttributesDto::class) - subclass(ChapterAttributesDto::class) - subclass(CoverArtAttributesDto::class) - subclass(ListAttributesDto::class) - subclass(MangaAttributesDto::class) - subclass(ScanlationGroupAttributes::class) - subclass(TagAttributesDto::class) - subclass(UserAttributes::class) - } - } - } - - val intl = Intl( - language = lang, - baseLanguage = MangaDexIntl.ENGLISH, - availableLanguages = MangaDexIntl.AVAILABLE_LANGS, - classLoader = this::class.java.classLoader!!, - createMessageFileName = { lang -> - when (lang) { - MangaDexIntl.SPANISH_LATAM -> Intl.createDefaultMessageFileName(MangaDexIntl.SPANISH) - MangaDexIntl.PORTUGUESE -> Intl.createDefaultMessageFileName(MangaDexIntl.BRAZILIAN_PORTUGUESE) - else -> Intl.createDefaultMessageFileName(lang) - } - }, - ) - - /** - * Gets the UUID from the url - */ - fun getUUIDFromUrl(url: String) = url.substringAfterLast("/") - - /** - * Get chapters for manga (aka manga/$id/feed endpoint) - */ - fun getChapterEndpoint(mangaId: String, offset: Int, langCode: String) = - "${MDConstants.apiMangaUrl}/$mangaId/feed".toHttpUrl().newBuilder() - .addQueryParameter("includes[]", MDConstants.scanlationGroup) - .addQueryParameter("includes[]", MDConstants.user) - .addQueryParameter("limit", "500") - .addQueryParameter("offset", offset.toString()) - .addQueryParameter("translatedLanguage[]", langCode) - .addQueryParameter("order[volume]", "desc") - .addQueryParameter("order[chapter]", "desc") - .addQueryParameter("includeFuturePublishAt", "0") - .addQueryParameter("includeEmptyPages", "0") - .toString() - - /** - * Check if the manga url is a valid uuid - */ - fun containsUuid(url: String) = url.contains(MDConstants.uuidRegex) - - /** - * Check if the string is a valid uuid - */ - fun isUuid(text: String) = MDConstants.uuidRegex matches text - - /** - * Get the manga offset pages are 1 based, so subtract 1 - */ - fun getMangaListOffset(page: Int): String = (MDConstants.mangaLimit * (page - 1)).toString() - - /** - * Get the latest chapter offset pages are 1 based, so subtract 1 - */ - fun getLatestChapterOffset(page: Int): String = - (MDConstants.latestChapterLimit * (page - 1)).toString() - - /** - * Remove any HTML characters in manga or chapter name to actual - * characters. For example ♥ will show ♥. - */ - private fun String.removeEntities(): String { - return Parser.unescapeEntities(this, false) - } - - /** - * Remove any HTML characters in description to actual characters. - * It also removes Markdown syntax for links, italic and bold. - */ - private fun String.removeEntitiesAndMarkdown(): String { - return removeEntities() - .substringBefore("---") - .replace(markdownLinksRegex, "$1") - .replace(markdownItalicBoldRegex, "$1") - .replace(markdownItalicRegex, "$1") - .trim() - } - - /** - * Maps MangaDex status to Tachiyomi status. - * Adapted from the MangaDex handler from TachiyomiSY. - */ - fun getPublicationStatus(attr: MangaAttributesDto, volumes: Map): Int { - val chaptersList = volumes.values - .flatMap { it.chapters.values } - .map { it.chapter } - - val tempStatus = when (attr.status) { - StatusDto.ONGOING -> SManga.ONGOING - StatusDto.CANCELLED -> SManga.CANCELLED - StatusDto.COMPLETED -> SManga.PUBLISHING_FINISHED - StatusDto.HIATUS -> SManga.ON_HIATUS - else -> SManga.UNKNOWN - } - - val publishedOrCancelled = tempStatus == SManga.PUBLISHING_FINISHED || - tempStatus == SManga.CANCELLED - - val isOneShot = attr.tags.any { it.id == MDConstants.tagOneShotUuid } && - attr.tags.none { it.id == MDConstants.tagAnthologyUuid } - - return when { - chaptersList.contains(attr.lastChapter) && publishedOrCancelled -> SManga.COMPLETED - isOneShot && volumes["none"]?.chapters?.get("none") != null -> SManga.COMPLETED - else -> tempStatus - } - } - - private fun parseDate(dateAsString: String): Long = - MDConstants.dateFormatter.parse(dateAsString)?.time ?: 0 - - /** - * Chapter URL where we get the token, last request time. - */ - private val tokenTracker = hashMapOf() - - companion object { - val USE_CACHE = CacheControl.Builder() - .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS) - .build() - - val markdownLinksRegex = "\\[([^]]+)\\]\\(([^)]+)\\)".toRegex() - val markdownItalicBoldRegex = "\\*+\\s*([^\\*]*)\\s*\\*+".toRegex() - val markdownItalicRegex = "_+\\s*([^_]*)\\s*_+".toRegex() - - val titleSpecialCharactersRegex = "[^a-z0-9]+".toRegex() - - val trailingHyphenRegex = "-+$".toRegex() - } - - /** - * Check the token map to see if the MD@Home host is still valid. - */ - fun getValidImageUrlForPage(page: Page, headers: Headers, client: OkHttpClient): Request { - val (host, tokenRequestUrl, time) = page.url.split(",") - - val mdAtHomeServerUrl = - when (Date().time - time.toLong() > MDConstants.mdAtHomeTokenLifespan) { - false -> host - true -> { - val tokenLifespan = Date().time - (tokenTracker[tokenRequestUrl] ?: 0) - val cacheControl = if (tokenLifespan > MDConstants.mdAtHomeTokenLifespan) { - CacheControl.FORCE_NETWORK - } else { - USE_CACHE - } - getMdAtHomeUrl(tokenRequestUrl, client, headers, cacheControl) - } - } - - return GET(mdAtHomeServerUrl + page.imageUrl, headers) - } - - /** - * Get the MD@Home URL. - */ - private fun getMdAtHomeUrl( - tokenRequestUrl: String, - client: OkHttpClient, - headers: Headers, - cacheControl: CacheControl, - ): String { - val request = mdAtHomeRequest(tokenRequestUrl, headers, cacheControl) - val response = client.newCall(request).execute() - - // This check is for the error that causes pages to fail to load. - // It should never be entered, but in case it is, we retry the request. - if (response.code == 504) { - Log.wtf("MangaDex", "Failed to read cache for \"$tokenRequestUrl\"") - return getMdAtHomeUrl(tokenRequestUrl, client, headers, CacheControl.FORCE_NETWORK) - } - - return response.use { json.decodeFromString(it.body.string()).baseUrl } - } - - /** - * create an md at home Request - */ - fun mdAtHomeRequest( - tokenRequestUrl: String, - headers: Headers, - cacheControl: CacheControl, - ): Request { - if (cacheControl == CacheControl.FORCE_NETWORK) { - tokenTracker[tokenRequestUrl] = Date().time - } - - return GET(tokenRequestUrl, headers, cacheControl) - } - - /** - * Create a [SManga] from the JSON element with only basic attributes filled. - */ - fun createBasicManga( - mangaDataDto: MangaDataDto, - coverFileName: String?, - coverSuffix: String?, - lang: String, - ): SManga = SManga.create().apply { - url = "/manga/${mangaDataDto.id}" - val titleMap = mangaDataDto.attributes!!.title - val dirtyTitle = - titleMap.values.firstOrNull() // use literally anything from title as first resort - ?: mangaDataDto.attributes.altTitles - .find { (it[lang] ?: it["en"]) !== null } - ?.values?.singleOrNull() // find something else from alt titles - title = dirtyTitle?.removeEntities().orEmpty() - - coverFileName?.let { - thumbnail_url = when (!coverSuffix.isNullOrEmpty()) { - true -> "${MDConstants.cdnUrl}/covers/${mangaDataDto.id}/$coverFileName$coverSuffix" - else -> "${MDConstants.cdnUrl}/covers/${mangaDataDto.id}/$coverFileName" - } - } - } - - /** - * Create an [SManga] from the JSON element with all attributes filled. - */ - fun createManga( - mangaDataDto: MangaDataDto, - chapters: Map, - firstVolumeCover: String?, - lang: String, - coverSuffix: String?, - altTitlesInDesc: Boolean, - ): SManga { - val attr = mangaDataDto.attributes!! - - // Things that will go with the genre tags but aren't actually genre - val dexLocale = Locale.forLanguageTag(lang) - - val nonGenres = listOfNotNull( - attr.publicationDemographic - ?.let { intl["publication_demographic_${it.name.lowercase()}"] }, - attr.contentRating - .takeIf { it != ContentRatingDto.SAFE } - ?.let { intl.format("content_rating_genre", intl["content_rating_${it.name.lowercase()}"]) }, - attr.originalLanguage - ?.let { Locale.forLanguageTag(it) } - ?.getDisplayName(dexLocale) - ?.replaceFirstChar { it.uppercase(dexLocale) }, - ) - - val authors = mangaDataDto.relationships - .filterIsInstance() - .mapNotNull { it.attributes?.name } - .distinct() - - val artists = mangaDataDto.relationships - .filterIsInstance() - .mapNotNull { it.attributes?.name } - .distinct() - - val coverFileName = firstVolumeCover ?: mangaDataDto.relationships - .filterIsInstance() - .firstOrNull() - ?.attributes?.fileName - - val tags = mdFilters.getTags(intl).associate { it.id to it.name } - - val genresMap = attr.tags - .groupBy({ it.attributes!!.group }) { tagDto -> tags[tagDto.id] } - .mapValues { it.value.filterNotNull().sortedWith(intl.collator) } - - val genreList = MDConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } + nonGenres - - var desc = (attr.description[lang] ?: attr.description["en"]) - ?.removeEntitiesAndMarkdown() - .orEmpty() - - if (altTitlesInDesc) { - val romanizedOriginalLang = MDConstants.romanizedLangCodes[attr.originalLanguage].orEmpty() - val altTitles = attr.altTitles - .filter { it.containsKey(lang) || it.containsKey(romanizedOriginalLang) } - .mapNotNull { it.values.singleOrNull() } - .filter(String::isNotEmpty) - - if (altTitles.isNotEmpty()) { - val altTitlesDesc = altTitles - .joinToString("\n", "${intl["alternative_titles"]}\n") { "• $it" } - desc += (if (desc.isBlank()) "" else "\n\n") + altTitlesDesc.removeEntities() - } - } - - return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang).apply { - description = desc - author = authors.joinToString() - artist = artists.joinToString() - status = getPublicationStatus(attr, chapters) - genre = genreList - .filter(String::isNotEmpty) - .joinToString() - } - } - - /** - * Create the [SChapter] from the JSON element. - */ - fun createChapter(chapterDataDto: ChapterDataDto): SChapter { - val attr = chapterDataDto.attributes!! - - val groups = chapterDataDto.relationships - .filterIsInstance() - .filterNot { it.id == MDConstants.legacyNoGroupId } // 'no group' left over from MDv3 - .mapNotNull { it.attributes?.name } - .joinToString(" & ") - .ifEmpty { - // Fallback to uploader name if no group is set. - val users = chapterDataDto.relationships - .filterIsInstance() - .mapNotNull { it.attributes?.username } - if (users.isNotEmpty()) intl.format("uploaded_by", users.joinToString(" & ")) else "" - } - .ifEmpty { intl["no_group"] } // "No Group" as final resort - - val chapterName = mutableListOf() - // Build chapter name - - attr.volume?.let { - if (it.isNotEmpty()) { - chapterName.add("Vol.$it") - } - } - - attr.chapter?.let { - if (it.isNotEmpty()) { - chapterName.add("Ch.$it") - } - } - - attr.title?.let { - if (it.isNotEmpty()) { - if (chapterName.isNotEmpty()) { - chapterName.add("-") - } - chapterName.add(it) - } - } - - // if volume, chapter and title is empty its a oneshot - if (chapterName.isEmpty()) { - chapterName.add("Oneshot") - } - - // In future calculate [END] if non mvp api doesn't provide it - - return SChapter.create().apply { - url = "/chapter/${chapterDataDto.id}" - name = chapterName.joinToString(" ").removeEntities() - date_upload = parseDate(attr.publishAt) - scanlator = groups - } - } - - fun titleToSlug(title: String) = title.trim() - .lowercase(Locale.US) - .replace(titleSpecialCharactersRegex, "-") - .replace(trailingHyphenRegex, "") - .split("-") - .reduce { accumulator, element -> - val currentSlug = "$accumulator-$element" - if (currentSlug.length > 100) { - accumulator - } else { - currentSlug - } - } - - /** - * Adds a custom [TextWatcher] to the preference's [EditText] that show an - * error if the input value contains invalid UUIDs. If the validation fails, - * the Ok button is disabled to prevent the user from saving the value. - * - * This will likely need to be removed or revisited when the app migrates the - * extension preferences screen to Compose. - */ - fun setupEditTextUuidValidator(editText: EditText) { - editText.addTextChangedListener( - object : TextWatcher { - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit - - override fun afterTextChanged(editable: Editable?) { - requireNotNull(editable) - - val text = editable.toString() - - val isValid = text.isBlank() || text - .split(",") - .map(String::trim) - .all(::isUuid) - - editText.error = if (!isValid) intl["invalid_uuids"] else null - editText.rootView.findViewById