Remove sources as per request by Kakao Entertainment
| @ -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 | ||||
|  | ||||
| Before Width: | Height: | Size: 7.9 KiB | 
| Before Width: | Height: | Size: 3.8 KiB | 
| Before Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 26 KiB | 
| Before Width: | Height: | Size: 43 KiB | 
| Before Width: | Height: | Size: 338 KiB | 
| @ -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\"]" | ||||
| } | ||||
| @ -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"), | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -1,53 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
| <application> | ||||
|     <activity | ||||
|         android:name=".all.batoto.BatoToUrlActivity" | ||||
|         android:excludeFromRecents="true" | ||||
|         android:exported="true" | ||||
|         android:theme="@android:style/Theme.NoDisplay"> | ||||
|         <intent-filter> | ||||
|             <action android:name="android.intent.action.VIEW" /> | ||||
| 
 | ||||
|             <category android:name="android.intent.category.DEFAULT" /> | ||||
|             <category android:name="android.intent.category.BROWSABLE" /> | ||||
| 
 | ||||
|             <data android:host="*.bato.to" /> | ||||
|             <data android:host="bato.to" /> | ||||
|             <data android:host="*.batocc.com" /> | ||||
|             <data android:host="batocc.com" /> | ||||
|             <data android:host="*.batotoo.com" /> | ||||
|             <data android:host="batotoo.com" /> | ||||
|             <data android:host="*.batotwo.com" /> | ||||
|             <data android:host="batotwo.com" /> | ||||
|             <data android:host="*.battwo.com" /> | ||||
|             <data android:host="battwo.com" /> | ||||
|             <data android:host="*.comiko.net" /> | ||||
|             <data android:host="comiko.net" /> | ||||
|             <data android:host="*.mangatoto.com" /> | ||||
|             <data android:host="mangatoto.com" /> | ||||
|             <data android:host="*.mangatoto.net" /> | ||||
|             <data android:host="mangatoto.net" /> | ||||
|             <data android:host="*.mangatoto.org" /> | ||||
|             <data android:host="mangatoto.org" /> | ||||
|             <data android:host="*.mycordant.co.uk" /> | ||||
|             <data android:host="mycordant.co.uk" /> | ||||
|             <data android:host="*.dto.to" /> | ||||
|             <data android:host="dto.to" /> | ||||
|             <data android:host="*.hto.to" /> | ||||
|             <data android:host="hto.to" /> | ||||
|             <data android:host="*.mto.to" /> | ||||
|             <data android:host="mto.to" /> | ||||
|             <data android:host="*.wto.to" /> | ||||
|             <data android:host="wto.to" /> | ||||
| 
 | ||||
|             <data | ||||
|                 android:pathPattern="/series/..*" | ||||
|                 android:scheme="https" /> | ||||
|             <data | ||||
|                 android:pathPattern="/subject-overview/..*" | ||||
|                 android:scheme="https" /> | ||||
|         </intent-filter> | ||||
|     </activity> | ||||
| </application> | ||||
| </manifest> | ||||
| @ -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 | ||||
| @ -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) | ||||
| @ -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')) | ||||
| } | ||||
| Before Width: | Height: | Size: 3.8 KiB | 
| Before Width: | Height: | Size: 2.2 KiB | 
| Before Width: | Height: | Size: 5.1 KiB | 
| Before Width: | Height: | Size: 9.3 KiB | 
| Before Width: | Height: | Size: 14 KiB | 
| Before Width: | Height: | Size: 67 KiB | 
| @ -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<Application>().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<MangasPage> { | ||||
|         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<JsonObject>(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<List<SChapter>> { | ||||
|         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<SChapter> { | ||||
|         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<Page> { | ||||
|         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<SelectFilterOption>, default: Int = 0) : Filter.Select<String>(name, options.map { it.name }.toTypedArray(), default) { | ||||
|         val selected: String | ||||
|             get() = options[state].value | ||||
|     } | ||||
| 
 | ||||
|     abstract class CheckboxGroupFilter(name: String, options: List<CheckboxFilterOption>) : Filter.Group<CheckboxFilterOption>(name, options) { | ||||
|         val selected: List<String> | ||||
|             get() = state.filter { it.state }.map { it.value } | ||||
|     } | ||||
| 
 | ||||
|     abstract class TriStateGroupFilter(name: String, options: List<TriStateFilterOption>) : Filter.Group<TriStateFilterOption>(name, options) { | ||||
|         val included: List<String> | ||||
|             get() = state.filter { it.isIncluded() }.map { it.value } | ||||
| 
 | ||||
|         val excluded: List<String> | ||||
|             get() = state.filter { it.isExcluded() }.map { it.value } | ||||
|     } | ||||
| 
 | ||||
|     abstract class TextFilter(name: String) : Filter.Text(name) | ||||
| 
 | ||||
|     class SortFilter(sortables: Array<String>) : Filter.Sort("Sort", sortables, Selection(5, false)) | ||||
|     class StatusFilter(options: List<SelectFilterOption>, default: Int) : SelectFilter("Status", options, default) | ||||
|     class OriginGroupFilter(options: List<CheckboxFilterOption>) : CheckboxGroupFilter("Origin", options) | ||||
|     class GenreGroupFilter(options: List<TriStateFilterOption>) : TriStateGroupFilter("Genre", options) | ||||
|     class MinChapterTextFilter : TextFilter("Min. Chapters") | ||||
|     class MaxChapterTextFilter : TextFilter("Max. Chapters") | ||||
|     class LangGroupFilter(options: List<CheckboxFilterOption>) : CheckboxGroupFilter("Languages", options) | ||||
|     class LetterFilter(options: List<SelectFilterOption>, default: Int) : SelectFilter("Letter matching mode (Slow)", options, default) | ||||
|     class UtilsFilter(options: List<SelectFilterOption>, default: Int) : SelectFilter("Utils comic list", options, default) | ||||
|     class HistoryFilter(options: List<SelectFilterOption>, 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 | ||||
|     } | ||||
| } | ||||
| @ -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<Source> = 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"), | ||||
| ) | ||||
| @ -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>): String? { | ||||
|         return if (pathSegments.size >= 2) { | ||||
|             val id = pathSegments[1] | ||||
|             "ID:$id" | ||||
|         } else { | ||||
|             null | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,30 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
| 
 | ||||
|     <application> | ||||
|         <activity | ||||
|             android:name=".all.mangadex.MangadexUrlActivity" | ||||
|             android:excludeFromRecents="true" | ||||
|             android:exported="true" | ||||
|             android:theme="@android:style/Theme.NoDisplay"> | ||||
|             <intent-filter android:autoVerify="true"> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
| 
 | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <category android:name="android.intent.category.BROWSABLE" /> | ||||
| 
 | ||||
|                 <data android:host="mangadex.org" /> | ||||
|                 <data android:host="canary.mangadex.dev" /> | ||||
|                 <data android:scheme="https" /> | ||||
| 
 | ||||
|                 <data android:pathPattern="/title/..*" /> | ||||
|                 <data android:pathPattern="/manga/..*" /> | ||||
|                 <data android:pathPattern="/chapter/..*" /> | ||||
|                 <data android:pathPattern="/group/..*" /> | ||||
|                 <data android:pathPattern="/author/..*" /> | ||||
|                 <data android:pathPattern="/user/..*" /> | ||||
|                 <data android:pathPattern="/list/..*" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|     </application> | ||||
| </manifest> | ||||
| @ -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 | ||||
|      ``` | ||||
| @ -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 | ||||
| @ -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 | ||||
| @ -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 | ||||
| @ -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 | ||||
| @ -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" | ||||
| Before Width: | Height: | Size: 3.6 KiB | 
| Before Width: | Height: | Size: 2.0 KiB | 
| Before Width: | Height: | Size: 4.7 KiB | 
| Before Width: | Height: | Size: 8.6 KiB | 
| Before Width: | Height: | Size: 12 KiB | 
| Before Width: | Height: | Size: 24 KiB | 
| @ -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<String>() | ||||
| 
 | ||||
|     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", | ||||
|     ) | ||||
| } | ||||
| @ -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<Application>().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<MangaListDto>() | ||||
| 
 | ||||
|         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<CoverArtDto>() | ||||
|                     ?.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<ChapterListDto>() | ||||
| 
 | ||||
|         val mangaIds = chapterListDto.data | ||||
|             .flatMap { it.relationships } | ||||
|             .filterIsInstance<MangaDataDto>() | ||||
|             .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<MangaListDto>() | ||||
|         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<CoverArtDto>() | ||||
|                     ?.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<MangasPage> { | ||||
|         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<String> { | ||||
|         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<ChapterDto>().data!!.relationships | ||||
|                     .firstInstanceOrNull<MangaDataDto>()!!.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<ListDto>() | ||||
|         val listDtoFiltered = listDto.data!!.relationships.filterIsInstance<MangaDataDto>() | ||||
|         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<SManga> { | ||||
|         // 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<MangaListDto>() | ||||
|         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<CoverArtDto>() | ||||
|                     ?.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<MangaDto>() | ||||
| 
 | ||||
|         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<String, AggregateVolume> { | ||||
|         val url = "${MDConstants.apiMangaUrl}/${manga.data!!.id}/aggregate?translatedLanguage[]=$langCode" | ||||
|         val response = client.newCall(GET(url, headers)).execute() | ||||
| 
 | ||||
|         return runCatching { response.parseAs<AggregateDto>() } | ||||
|             .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<MangaDataDto>): Map<String, String>? { | ||||
|         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<CoverArtListDto>().data | ||||
|         } | ||||
| 
 | ||||
|         val covers = result.getOrNull() ?: return null | ||||
| 
 | ||||
|         return covers | ||||
|             .groupBy { it.relationships.firstInstanceOrNull<MangaDataDto>()!!.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<SChapter> { | ||||
|         if (response.code == 204) { | ||||
|             return emptyList() | ||||
|         } | ||||
| 
 | ||||
|         val chapterListResponse = response.parseAs<ChapterListDto>() | ||||
| 
 | ||||
|         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<ChapterListDto>() | ||||
|             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<Page> { | ||||
|         val atHomeRequestUrl = response.request.url | ||||
|         val atHomeDto = response.parseAs<AtHomeDto>() | ||||
|         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<String> | ||||
| 
 | ||||
|                 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<String> | ||||
| 
 | ||||
|                 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<String>?) = apply { | ||||
|         value?.forEach { addQueryParameter(name, it) } | ||||
|     } | ||||
| 
 | ||||
|     private inline fun <reified T> Response.parseAs(): T = use { | ||||
|         helper.json.decodeFromString(body.string()) | ||||
|     } | ||||
| 
 | ||||
|     private inline fun <reified T> 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<String> | ||||
|         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() | ||||
|     } | ||||
| } | ||||
| @ -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<Source> = 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") | ||||
| @ -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<OriginalLanguage>) : | ||||
|         Filter.Group<OriginalLanguage>(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<OriginalLanguage> { | ||||
|         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<ContentRating>) : | ||||
|         Filter.Group<ContentRating>(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<ContentRating> { | ||||
|         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<Demographic>) : | ||||
|         Filter.Group<Demographic>(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<Status>) : | ||||
|         Filter.Group<Status>(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<Sortable>) : | ||||
|         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<Tag>) : | ||||
|         Filter.Group<Tag>(collection, tags), | ||||
|         UrlQueryFilter { | ||||
| 
 | ||||
|         override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { | ||||
|             state.forEach { tag -> | ||||
|                 if (tag.isIncluded()) { | ||||
|                     url.addQueryParameter("includedTags[]", tag.id) | ||||
|                 } else if (tag.isExcluded()) { | ||||
|                     url.addQueryParameter("excludedTags[]", tag.id) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun getContents(intl: Intl): List<Tag> { | ||||
|         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<Tag> { | ||||
|         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<Tag> { | ||||
|         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<Tag> { | ||||
|         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<Tag> { | ||||
|         return getContents(intl) + getFormats(intl) + getGenres(intl) + getThemes(intl) | ||||
|     } | ||||
| 
 | ||||
|     private data class TagMode(val title: String, val value: String) { | ||||
|         override fun toString(): String = title | ||||
|     } | ||||
| 
 | ||||
|     private fun getTagModes(intl: Intl) = arrayOf( | ||||
|         TagMode(intl["mode_and"], "AND"), | ||||
|         TagMode(intl["mode_or"], "OR"), | ||||
|     ) | ||||
| 
 | ||||
|     private class TagInclusionMode(intl: Intl, modes: Array<TagMode>) : | ||||
|         Filter.Select<TagMode>(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<TagMode>) : | ||||
|         Filter.Select<TagMode>(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<Filter<*>>(intl["tags_mode"], innerFilters), | ||||
|         UrlQueryFilter { | ||||
| 
 | ||||
|         override fun addQueryParameter(url: HttpUrl.Builder, dexLang: String) { | ||||
|             state.filterIsInstance<UrlQueryFilter>() | ||||
|                 .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<UrlQueryFilter>() | ||||
|             .forEach { filter -> filter.addQueryParameter(url, dexLang) } | ||||
| 
 | ||||
|         return url.build() | ||||
|     } | ||||
| 
 | ||||
|     private fun List<Tag>.sortIfTranslated(intl: Intl): List<Tag> = apply { | ||||
|         if (intl.chosenLanguage == MangaDexIntl.ENGLISH) { | ||||
|             return this | ||||
|         } | ||||
| 
 | ||||
|         return sortedWith(compareBy(intl.collator, Tag::name)) | ||||
|     } | ||||
| } | ||||
| @ -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<String, AggregateVolume>): 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<String, Long>() | ||||
| 
 | ||||
|     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<AtHomeDto>(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<String, AggregateVolume>, | ||||
|         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<AuthorDto>() | ||||
|             .mapNotNull { it.attributes?.name } | ||||
|             .distinct() | ||||
| 
 | ||||
|         val artists = mangaDataDto.relationships | ||||
|             .filterIsInstance<ArtistDto>() | ||||
|             .mapNotNull { it.attributes?.name } | ||||
|             .distinct() | ||||
| 
 | ||||
|         val coverFileName = firstVolumeCover ?: mangaDataDto.relationships | ||||
|             .filterIsInstance<CoverArtDto>() | ||||
|             .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<ScanlationGroupDto>() | ||||
|             .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<UserDto>() | ||||
|                     .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<String>() | ||||
|         // 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<Button>(android.R.id.button1) | ||||
|                         ?.isEnabled = editText.error == null | ||||
|                 } | ||||
|             }, | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @ -1,24 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.all.mangadex | ||||
| 
 | ||||
| object MangaDexIntl { | ||||
|     const val BRAZILIAN_PORTUGUESE = "pt-BR" | ||||
|     const val CHINESE = "zh" | ||||
|     const val ENGLISH = "en" | ||||
|     const val JAPANESE = "ja" | ||||
|     const val KOREAN = "ko" | ||||
|     const val PORTUGUESE = "pt" | ||||
|     const val SPANISH_LATAM = "es-419" | ||||
|     const val SPANISH = "es" | ||||
|     const val RUSSIAN = "ru" | ||||
| 
 | ||||
|     val AVAILABLE_LANGS = setOf( | ||||
|         ENGLISH, | ||||
|         BRAZILIAN_PORTUGUESE, | ||||
|         PORTUGUESE, | ||||
|         SPANISH, | ||||
|         SPANISH_LATAM, | ||||
|         RUSSIAN, | ||||
|     ) | ||||
| 
 | ||||
|     const val MANGADEX_NAME = "MangaDex" | ||||
| } | ||||
| @ -1,53 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.all.mangadex | ||||
| 
 | ||||
| import android.app.Activity | ||||
| import android.content.ActivityNotFoundException | ||||
| import android.content.Intent | ||||
| import android.os.Bundle | ||||
| import android.util.Log | ||||
| import kotlin.system.exitProcess | ||||
| 
 | ||||
| /** | ||||
|  * Springboard that accepts https://mangadex.com/title/xxx intents and redirects them to | ||||
|  * the main tachiyomi process. The idea is to not install the intent filter unless | ||||
|  * you have this extension installed, but still let the main tachiyomi app control | ||||
|  * things. | ||||
|  * | ||||
|  * Main goal was to make it easier to open manga in Tachiyomi in spite of the DDoS blocking | ||||
|  * the usual search screen from working. | ||||
|  */ | ||||
| class MangadexUrlActivity : Activity() { | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         val pathSegments = intent?.data?.pathSegments | ||||
|         if (pathSegments != null && pathSegments.size > 1) { | ||||
|             val titleId = pathSegments[1] | ||||
|             val mainIntent = Intent().apply { | ||||
|                 action = "eu.kanade.tachiyomi.SEARCH" | ||||
|                 with(pathSegments[0]) { | ||||
|                     when { | ||||
|                         equals("chapter") -> putExtra("query", MDConstants.prefixChSearch + titleId) | ||||
|                         equals("group") -> putExtra("query", MDConstants.prefixGrpSearch + titleId) | ||||
|                         equals("user") -> putExtra("query", MDConstants.prefixUsrSearch + titleId) | ||||
|                         equals("author") -> putExtra("query", MDConstants.prefixAuthSearch + titleId) | ||||
|                         equals("list") -> putExtra("query", MDConstants.prefixListSearch + titleId) | ||||
|                         else -> putExtra("query", MDConstants.prefixIdSearch + titleId) | ||||
|                     } | ||||
|                 } | ||||
|                 putExtra("filter", packageName) | ||||
|             } | ||||
| 
 | ||||
|             try { | ||||
|                 startActivity(mainIntent) | ||||
|             } catch (e: ActivityNotFoundException) { | ||||
|                 Log.e("MangadexUrlActivity", e.toString()) | ||||
|             } | ||||
|         } else { | ||||
|             Log.e("MangadexUrlActivity", "Could not parse URI from intent $intent") | ||||
|         } | ||||
| 
 | ||||
|         finish() | ||||
|         exitProcess(0) | ||||
|     } | ||||
| } | ||||
| @ -1,109 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.all.mangadex | ||||
| 
 | ||||
| import android.util.Log | ||||
| import eu.kanade.tachiyomi.extension.all.mangadex.dto.ImageReportDto | ||||
| import eu.kanade.tachiyomi.network.POST | ||||
| import kotlinx.serialization.encodeToString | ||||
| import kotlinx.serialization.json.Json | ||||
| import okhttp3.Call | ||||
| import okhttp3.Callback | ||||
| import okhttp3.Headers | ||||
| import okhttp3.HttpUrl.Companion.toHttpUrl | ||||
| import okhttp3.Interceptor | ||||
| import okhttp3.MediaType.Companion.toMediaType | ||||
| import okhttp3.OkHttpClient | ||||
| import okhttp3.Request | ||||
| import okhttp3.RequestBody.Companion.toRequestBody | ||||
| import okhttp3.Response | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| 
 | ||||
| /** | ||||
|  * Interceptor to post to MD@Home for MangaDex Stats | ||||
|  */ | ||||
| class MdAtHomeReportInterceptor( | ||||
|     private val client: OkHttpClient, | ||||
|     private val headers: Headers, | ||||
| ) : Interceptor { | ||||
| 
 | ||||
|     private val json: Json by injectLazy() | ||||
| 
 | ||||
|     override fun intercept(chain: Interceptor.Chain): Response { | ||||
|         val originalRequest = chain.request() | ||||
|         val response = chain.proceed(chain.request()) | ||||
|         val url = originalRequest.url.toString() | ||||
| 
 | ||||
|         if (!url.contains(MD_AT_HOME_URL_REGEX)) { | ||||
|             return response | ||||
|         } | ||||
| 
 | ||||
|         Log.e("MangaDex", "Connecting to MD@Home node at $url") | ||||
| 
 | ||||
|         val reportRequest = mdAtHomeReportRequest(response) | ||||
| 
 | ||||
|         // Execute the report endpoint network call asynchronously to avoid blocking | ||||
|         // the reader from showing the image once it's fully loaded if the report call | ||||
|         // gets stuck, as it tend to happens sometimes. | ||||
|         client.newCall(reportRequest).enqueue(REPORT_CALLBACK) | ||||
| 
 | ||||
|         if (response.isSuccessful) { | ||||
|             return response | ||||
|         } | ||||
| 
 | ||||
|         response.close() | ||||
| 
 | ||||
|         Log.e("MangaDex", "Error connecting to MD@Home node, fallback to uploads server") | ||||
| 
 | ||||
|         val imagePath = originalRequest.url.pathSegments | ||||
|             .dropWhile { it != "data" && it != "data-saver" } | ||||
|             .joinToString("/") | ||||
| 
 | ||||
|         val fallbackUrl = MDConstants.cdnUrl.toHttpUrl().newBuilder() | ||||
|             .addPathSegments(imagePath) | ||||
|             .build() | ||||
| 
 | ||||
|         val fallbackRequest = originalRequest.newBuilder() | ||||
|             .url(fallbackUrl) | ||||
|             .headers(headers) | ||||
|             .build() | ||||
| 
 | ||||
|         return chain.proceed(fallbackRequest) | ||||
|     } | ||||
| 
 | ||||
|     private fun mdAtHomeReportRequest(response: Response): Request { | ||||
|         val result = ImageReportDto( | ||||
|             url = response.request.url.toString(), | ||||
|             success = response.isSuccessful, | ||||
|             bytes = response.peekBody(Long.MAX_VALUE).bytes().size, | ||||
|             cached = response.headers["X-Cache"] == "HIT", | ||||
|             duration = response.receivedResponseAtMillis - response.sentRequestAtMillis, | ||||
|         ) | ||||
| 
 | ||||
|         val payload = json.encodeToString(result) | ||||
| 
 | ||||
|         return POST( | ||||
|             url = MDConstants.atHomePostUrl, | ||||
|             headers = headers, | ||||
|             body = payload.toRequestBody(JSON_MEDIA_TYPE), | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private val JSON_MEDIA_TYPE = "application/json".toMediaType() | ||||
|         private val MD_AT_HOME_URL_REGEX = | ||||
|             """^https://[\w\d]+\.[\w\d]+\.mangadex(\b-test\b)?\.network.*${'$'}""".toRegex() | ||||
| 
 | ||||
|         private val REPORT_CALLBACK = object : Callback { | ||||
|             override fun onFailure(call: Call, e: okio.IOException) { | ||||
|                 Log.e("MangaDex", "Error trying to POST report to MD@Home: ${e.message}") | ||||
|             } | ||||
| 
 | ||||
|             override fun onResponse(call: Call, response: Response) { | ||||
|                 if (!response.isSuccessful) { | ||||
|                     Log.e("MangaDex", "Error trying to POST report to MD@Home: HTTP error ${response.code}") | ||||
|                 } | ||||
| 
 | ||||
|                 response.close() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,40 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.all.mangadex | ||||
| 
 | ||||
| import android.content.SharedPreferences | ||||
| import okhttp3.Interceptor | ||||
| import okhttp3.Response | ||||
| import java.io.IOException | ||||
| 
 | ||||
| /** | ||||
|  * Interceptor to set custom useragent for MangaDex | ||||
|  */ | ||||
| class MdUserAgentInterceptor( | ||||
|     private val preferences: SharedPreferences, | ||||
|     private val dexLang: String, | ||||
| ) : Interceptor { | ||||
| 
 | ||||
|     private val SharedPreferences.customUserAgent | ||||
|         get() = getString( | ||||
|             MDConstants.getCustomUserAgentPrefKey(dexLang), | ||||
|             MDConstants.defaultUserAgent, | ||||
|         ) | ||||
| 
 | ||||
|     override fun intercept(chain: Interceptor.Chain): Response { | ||||
|         val originalRequest = chain.request() | ||||
| 
 | ||||
|         val newUserAgent = preferences.customUserAgent | ||||
|             ?: return chain.proceed(originalRequest) | ||||
| 
 | ||||
|         val originalHeaders = originalRequest.headers | ||||
| 
 | ||||
|         val modifiedHeaders = originalHeaders.newBuilder() | ||||
|             .set("User-Agent", newUserAgent) | ||||
|             .build() | ||||
| 
 | ||||
|         val modifiedRequest = originalRequest.newBuilder() | ||||
|             .headers(modifiedHeaders) | ||||
|             .build() | ||||
| 
 | ||||
|         return chain.proceed(modifiedRequest) | ||||
|     } | ||||
| } | ||||
| @ -1,22 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.all.mangadex.dto | ||||
| 
 | ||||
| import kotlinx.serialization.Serializable | ||||
| 
 | ||||
| @Serializable | ||||
| data class AggregateDto( | ||||
|     val result: String, | ||||
|     val volumes: Map<String, AggregateVolume>?, | ||||
| ) | ||||
| 
 | ||||
| @Serializable | ||||
| data class AggregateVolume( | ||||
|     val volume: String, | ||||
|     val count: String, | ||||
|     val chapters: Map<String, AggregateChapter>, | ||||
| ) | ||||
| 
 | ||||
| @Serializable | ||||
| data class AggregateChapter( | ||||
|     val chapter: String, | ||||
|     val count: String, | ||||
| ) | ||||
| @ -1,25 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.all.mangadex.dto | ||||
| 
 | ||||
| import kotlinx.serialization.Serializable | ||||
| 
 | ||||
| @Serializable | ||||
| data class AtHomeDto( | ||||
|     val baseUrl: String, | ||||
|     val chapter: AtHomeChapterDto, | ||||
| ) | ||||
| 
 | ||||
| @Serializable | ||||
| data class AtHomeChapterDto( | ||||
|     val hash: String, | ||||
|     val data: List<String>, | ||||
|     val dataSaver: List<String>, | ||||
| ) | ||||
| 
 | ||||
| @Serializable | ||||
| data class ImageReportDto( | ||||
|     val url: String, | ||||
|     val success: Boolean, | ||||
|     val bytes: Int?, | ||||
|     val cached: Boolean, | ||||
|     val duration: Long, | ||||
| ) | ||||
| @ -1,16 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.all.mangadex.dto | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants | ||||
| import kotlinx.serialization.SerialName | ||||
| import kotlinx.serialization.Serializable | ||||
| 
 | ||||
| @Serializable | ||||
| @SerialName(MDConstants.author) | ||||
| data class AuthorDto(override val attributes: AuthorArtistAttributesDto? = null) : EntityDto() | ||||
| 
 | ||||
| @Serializable | ||||
| @SerialName(MDConstants.artist) | ||||
| data class ArtistDto(override val attributes: AuthorArtistAttributesDto? = null) : EntityDto() | ||||
| 
 | ||||
| @Serializable | ||||
| data class AuthorArtistAttributesDto(val name: String) : AttributesDto() | ||||
| @ -1,30 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.all.mangadex.dto | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants | ||||
| import kotlinx.serialization.SerialName | ||||
| import kotlinx.serialization.Serializable | ||||
| 
 | ||||
| typealias ChapterListDto = PaginatedResponseDto<ChapterDataDto> | ||||
| 
 | ||||
| typealias ChapterDto = ResponseDto<ChapterDataDto> | ||||
| 
 | ||||
| @Serializable | ||||
| @SerialName(MDConstants.chapter) | ||||
| data class ChapterDataDto(override val attributes: ChapterAttributesDto? = null) : EntityDto() | ||||
| 
 | ||||
| @Serializable | ||||
| data class ChapterAttributesDto( | ||||
|     val title: String?, | ||||
|     val volume: String?, | ||||
|     val chapter: String?, | ||||
|     val pages: Int, | ||||
|     val publishAt: String, | ||||
|     val externalUrl: String?, | ||||
| ) : AttributesDto() { | ||||
| 
 | ||||
|     /** | ||||
|      * Returns true if the chapter is from an external website and have no pages. | ||||
|      */ | ||||
|     val isInvalid: Boolean | ||||
|         get() = externalUrl != null && pages == 0 | ||||
| } | ||||
| @ -1,17 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.all.mangadex.dto | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants | ||||
| import kotlinx.serialization.SerialName | ||||
| import kotlinx.serialization.Serializable | ||||
| 
 | ||||
| typealias CoverArtListDto = PaginatedResponseDto<CoverArtDto> | ||||
| 
 | ||||
| @Serializable | ||||
| @SerialName(MDConstants.coverArt) | ||||
| data class CoverArtDto(override val attributes: CoverArtAttributesDto? = null) : EntityDto() | ||||
| 
 | ||||
| @Serializable | ||||
| data class CoverArtAttributesDto( | ||||
|     val fileName: String? = null, | ||||
|     val locale: String? = null, | ||||
| ) : AttributesDto() | ||||
| @ -1,16 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.all.mangadex.dto | ||||
| 
 | ||||
| import kotlinx.serialization.Serializable | ||||
| 
 | ||||
| @Serializable | ||||
| abstract class EntityDto { | ||||
|     val id: String = "" | ||||
|     val relationships: List<EntityDto> = emptyList() | ||||
|     abstract val attributes: AttributesDto? | ||||
| } | ||||
| 
 | ||||
| @Serializable | ||||
| abstract class AttributesDto | ||||
| 
 | ||||
| @Serializable | ||||
| data class UnknownEntity(override val attributes: AttributesDto? = null) : EntityDto() | ||||
| @ -1,18 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.all.mangadex.dto | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants | ||||
| import kotlinx.serialization.SerialName | ||||
| import kotlinx.serialization.Serializable | ||||
| 
 | ||||
| typealias ListDto = ResponseDto<ListDataDto> | ||||
| 
 | ||||
| @Serializable | ||||
| @SerialName(MDConstants.list) | ||||
| data class ListDataDto(override val attributes: ListAttributesDto? = null) : EntityDto() | ||||
| 
 | ||||
| @Serializable | ||||
| data class ListAttributesDto( | ||||
|     val name: String, | ||||
|     val visibility: String, | ||||
|     val version: Int, | ||||
| ) : AttributesDto() | ||||
| @ -1,114 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.all.mangadex.dto | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants | ||||
| import kotlinx.serialization.KSerializer | ||||
| import kotlinx.serialization.SerialName | ||||
| import kotlinx.serialization.Serializable | ||||
| import kotlinx.serialization.descriptors.buildClassSerialDescriptor | ||||
| import kotlinx.serialization.encoding.Decoder | ||||
| import kotlinx.serialization.encoding.Encoder | ||||
| import kotlinx.serialization.json.JsonDecoder | ||||
| import kotlinx.serialization.json.JsonObject | ||||
| import kotlinx.serialization.json.contentOrNull | ||||
| import kotlinx.serialization.json.jsonPrimitive | ||||
| import kotlinx.serialization.serializer | ||||
| 
 | ||||
| typealias MangaListDto = PaginatedResponseDto<MangaDataDto> | ||||
| 
 | ||||
| typealias MangaDto = ResponseDto<MangaDataDto> | ||||
| 
 | ||||
| @Serializable | ||||
| @SerialName(MDConstants.manga) | ||||
| data class MangaDataDto(override val attributes: MangaAttributesDto? = null) : EntityDto() | ||||
| 
 | ||||
| @Serializable | ||||
| data class MangaAttributesDto( | ||||
|     val title: LocalizedString, | ||||
|     val altTitles: List<LocalizedString>, | ||||
|     val description: LocalizedString, | ||||
|     val originalLanguage: String?, | ||||
|     val lastVolume: String?, | ||||
|     val lastChapter: String?, | ||||
|     val contentRating: ContentRatingDto? = null, | ||||
|     val publicationDemographic: PublicationDemographicDto? = null, | ||||
|     val status: StatusDto? = null, | ||||
|     val tags: List<TagDto>, | ||||
| ) : AttributesDto() | ||||
| 
 | ||||
| @Serializable | ||||
| enum class ContentRatingDto(val value: String) { | ||||
|     @SerialName("safe") | ||||
|     SAFE("safe"), | ||||
| 
 | ||||
|     @SerialName("suggestive") | ||||
|     SUGGESTIVE("suggestive"), | ||||
| 
 | ||||
|     @SerialName("erotica") | ||||
|     EROTICA("erotica"), | ||||
| 
 | ||||
|     @SerialName("pornographic") | ||||
|     PORNOGRAPHIC("pornographic"), | ||||
| } | ||||
| 
 | ||||
| @Serializable | ||||
| enum class PublicationDemographicDto(val value: String) { | ||||
|     @SerialName("none") | ||||
|     NONE("none"), | ||||
| 
 | ||||
|     @SerialName("shounen") | ||||
|     SHOUNEN("shounen"), | ||||
| 
 | ||||
|     @SerialName("shoujo") | ||||
|     SHOUJO("shoujo"), | ||||
| 
 | ||||
|     @SerialName("josei") | ||||
|     JOSEI("josei"), | ||||
| 
 | ||||
|     @SerialName("seinen") | ||||
|     SEINEN("seinen"), | ||||
| } | ||||
| 
 | ||||
| @Serializable | ||||
| enum class StatusDto(val value: String) { | ||||
|     @SerialName("ongoing") | ||||
|     ONGOING("ongoing"), | ||||
| 
 | ||||
|     @SerialName("completed") | ||||
|     COMPLETED("completed"), | ||||
| 
 | ||||
|     @SerialName("hiatus") | ||||
|     HIATUS("hiatus"), | ||||
| 
 | ||||
|     @SerialName("cancelled") | ||||
|     CANCELLED("cancelled"), | ||||
| } | ||||
| 
 | ||||
| @Serializable | ||||
| @SerialName(MDConstants.tag) | ||||
| data class TagDto(override val attributes: TagAttributesDto? = null) : EntityDto() | ||||
| 
 | ||||
| @Serializable | ||||
| data class TagAttributesDto(val group: String) : AttributesDto() | ||||
| 
 | ||||
| typealias LocalizedString = @Serializable(LocalizedStringSerializer::class) | ||||
| Map<String, String> | ||||
| 
 | ||||
| /** | ||||
|  * Temporary workaround while Dex API still returns arrays instead of objects | ||||
|  * in the places that uses [LocalizedString]. | ||||
|  */ | ||||
| object LocalizedStringSerializer : KSerializer<Map<String, String>> { | ||||
|     override val descriptor = buildClassSerialDescriptor("LocalizedString") | ||||
| 
 | ||||
|     override fun deserialize(decoder: Decoder): Map<String, String> { | ||||
|         require(decoder is JsonDecoder) | ||||
| 
 | ||||
|         return (decoder.decodeJsonElement() as? JsonObject) | ||||
|             ?.mapValues { it.value.jsonPrimitive.contentOrNull ?: "" } | ||||
|             .orEmpty() | ||||
|     } | ||||
| 
 | ||||
|     override fun serialize(encoder: Encoder, value: Map<String, String>) { | ||||
|         encoder.encodeSerializableValue(serializer(), value) | ||||
|     } | ||||
| } | ||||
| @ -1,24 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.all.mangadex.dto | ||||
| 
 | ||||
| import kotlinx.serialization.Serializable | ||||
| 
 | ||||
| @Serializable | ||||
| data class PaginatedResponseDto<T : EntityDto>( | ||||
|     val result: String, | ||||
|     val response: String = "", | ||||
|     val data: List<T> = emptyList(), | ||||
|     val limit: Int = 0, | ||||
|     val offset: Int = 0, | ||||
|     val total: Int = 0, | ||||
| ) { | ||||
| 
 | ||||
|     val hasNextPage: Boolean | ||||
|         get() = limit + offset < total | ||||
| } | ||||
| 
 | ||||
| @Serializable | ||||
| data class ResponseDto<T : EntityDto>( | ||||
|     val result: String, | ||||
|     val response: String = "", | ||||
|     val data: T? = null, | ||||
| ) | ||||
| @ -1,12 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.all.mangadex.dto | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants | ||||
| import kotlinx.serialization.SerialName | ||||
| import kotlinx.serialization.Serializable | ||||
| 
 | ||||
| @Serializable | ||||
| @SerialName(MDConstants.scanlationGroup) | ||||
| data class ScanlationGroupDto(override val attributes: ScanlationGroupAttributes? = null) : EntityDto() | ||||
| 
 | ||||
| @Serializable | ||||
| data class ScanlationGroupAttributes(val name: String) : AttributesDto() | ||||
| @ -1,12 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.all.mangadex.dto | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.extension.all.mangadex.MDConstants | ||||
| import kotlinx.serialization.SerialName | ||||
| import kotlinx.serialization.Serializable | ||||
| 
 | ||||
| @Serializable | ||||
| @SerialName(MDConstants.user) | ||||
| data class UserDto(override val attributes: UserAttributes? = null) : EntityDto() | ||||
| 
 | ||||
| @Serializable | ||||
| data class UserAttributes(val username: String) : AttributesDto() | ||||
| @ -1,2 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest /> | ||||
| @ -1,48 +0,0 @@ | ||||
| apply plugin: 'com.android.application' | ||||
| apply plugin: 'kotlin-android' | ||||
| 
 | ||||
| ext { | ||||
|     extName = 'NewToki / ManaToki' | ||||
|     pkgNameSuffix = 'ko.newtoki' | ||||
|     extClass = '.TokiFactory' | ||||
|     extVersionCode = 29 | ||||
|     isNsfw = true | ||||
| } | ||||
| 
 | ||||
| apply from: "$rootDir/common.gradle" | ||||
| 
 | ||||
| def domainNumberFileName = "src/ko/newtoki/src/eu/kanade/tachiyomi/extension/ko/newtoki/FallbackDomainNumber.kt" | ||||
| def domainNumberFile = new File(domainNumberFileName) | ||||
| def backupFile = new File(domainNumberFileName + "_bak") | ||||
| 
 | ||||
| task updateDomainNumber { | ||||
|     doLast { | ||||
|         def domainNumberUrl = "https://stevenyomi.github.io/source-domains/newtoki.txt" | ||||
|         def number = new URL(domainNumberUrl).withInputStream { it.readLines()[0] } | ||||
|         println("[NewToki] Updating domain number to $number") | ||||
|         domainNumberFile.renameTo(backupFile) | ||||
|         domainNumberFile.withPrintWriter { | ||||
|             it.println("// THIS FILE IS AUTO-GENERATED, DO NOT COMMIT") | ||||
|             it.println("package eu.kanade.tachiyomi.extension.ko.newtoki") | ||||
|             it.println("const val fallbackDomainNumber = \"$number\"") | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| preBuild.dependsOn updateDomainNumber | ||||
| 
 | ||||
| task restoreBackup { | ||||
|     doLast { | ||||
|         if (backupFile.exists()) { | ||||
|             println("[NewToki] Restoring placeholder file") | ||||
|             domainNumberFile.delete() | ||||
|             backupFile.renameTo(domainNumberFile) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| tasks.whenTaskAdded { task -> | ||||
|     if (task.name == "assembleDebug" || task.name == "assembleRelease") { | ||||
|         task.finalizedBy(restoreBackup) | ||||
|     } | ||||
| } | ||||
| Before Width: | Height: | Size: 6.2 KiB | 
| Before Width: | Height: | Size: 2.9 KiB | 
| Before Width: | Height: | Size: 10 KiB | 
| Before Width: | Height: | Size: 22 KiB | 
| Before Width: | Height: | Size: 36 KiB | 
| Before Width: | Height: | Size: 348 KiB | 
| @ -1,73 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.ko.newtoki | ||||
| 
 | ||||
| import android.util.Log | ||||
| import eu.kanade.tachiyomi.network.GET | ||||
| import okhttp3.Interceptor | ||||
| import okhttp3.Response | ||||
| import java.io.IOException | ||||
| 
 | ||||
| var domainNumber = "" | ||||
|     get() { | ||||
|         val currentValue = field | ||||
|         if (currentValue.isNotEmpty()) return currentValue | ||||
| 
 | ||||
|         val prefValue = newTokiPreferences.domainNumber | ||||
|         if (prefValue.isNotEmpty()) { | ||||
|             field = prefValue | ||||
|             return prefValue | ||||
|         } | ||||
| 
 | ||||
|         val fallback = fallbackDomainNumber | ||||
|         domainNumber = fallback | ||||
|         return fallback | ||||
|     } | ||||
|     set(value) { | ||||
|         for (preference in arrayOf(manaTokiPreferences, newTokiPreferences)) { | ||||
|             preference.domainNumber = value | ||||
|         } | ||||
|         field = value | ||||
|     } | ||||
| 
 | ||||
| object DomainInterceptor : Interceptor { | ||||
|     override fun intercept(chain: Interceptor.Chain): Response { | ||||
|         val request = chain.request() | ||||
| 
 | ||||
|         val response = try { | ||||
|             chain.proceed(request) | ||||
|         } catch (e: IOException) { | ||||
|             if (chain.call().isCanceled()) throw e | ||||
|             Log.e("NewToki", "failed to fetch ${request.url}", e) | ||||
| 
 | ||||
|             val newDomainNumber = try { | ||||
|                 val domainNumberUrl = "https://stevenyomi.github.io/source-domains/newtoki.txt" | ||||
|                 chain.proceed(GET(domainNumberUrl)).body.string().also { it.toInt() } | ||||
|             } catch (_: Throwable) { | ||||
|                 throw IOException(editDomainNumber(), e) | ||||
|             } | ||||
|             domainNumber = newDomainNumber | ||||
| 
 | ||||
|             val url = request.url | ||||
|             val newHost = numberRegex.replaceFirst(url.host, newDomainNumber) | ||||
|             val newUrl = url.newBuilder().host(newHost).build() | ||||
|             try { | ||||
|                 chain.proceed(request.newBuilder().url(newUrl).build()) | ||||
|             } catch (e: IOException) { | ||||
|                 Log.e("NewToki", "failed to fetch $newUrl", e) | ||||
|                 throw IOException(editDomainNumber(), e) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (response.priorResponse == null) return response | ||||
| 
 | ||||
|         val newUrl = response.request.url | ||||
|         if ("captcha" in newUrl.toString()) throw IOException(solveCaptcha()) | ||||
| 
 | ||||
|         val newHost = newUrl.host | ||||
|         if (newHost.startsWith(MANATOKI_PREFIX) || newHost.startsWith(NEWTOKI_PREFIX)) { | ||||
|             numberRegex.find(newHost)?.run { domainNumber = value } | ||||
|         } | ||||
|         return response | ||||
|     } | ||||
| 
 | ||||
|     private val numberRegex by lazy { Regex("""\d+|$fallbackDomainNumber""") } | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.ko.newtoki | ||||
| 
 | ||||
| /** | ||||
|  * This value will be automatically overwritten when building the extension. | ||||
|  * After building, this file will be restored. | ||||
|  * | ||||
|  * Even if this value is built into the extension, the network call will fail | ||||
|  * because of underscore character and the extension will update it on its own. | ||||
|  */ | ||||
| const val fallbackDomainNumber = "_failed_to_fetch_domain_number" | ||||
| @ -1,174 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.ko.newtoki | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.network.GET | ||||
| import eu.kanade.tachiyomi.source.model.Filter | ||||
| import eu.kanade.tachiyomi.source.model.FilterList | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import okhttp3.HttpUrl.Companion.toHttpUrl | ||||
| import okhttp3.Request | ||||
| import org.jsoup.nodes.Element | ||||
| 
 | ||||
| /* | ||||
|  * ManaToki is too big to support in a Factory File., So split into separate file. | ||||
|  */ | ||||
| 
 | ||||
| object ManaToki : NewToki("ManaToki", "comic", manaTokiPreferences) { | ||||
|     // / ! DO NOT CHANGE THIS !  Only the site name changed from newtoki. | ||||
|     override val id = MANATOKI_ID | ||||
| 
 | ||||
|     override val baseUrl get() = "https://$MANATOKI_PREFIX$domainNumber.net" | ||||
| 
 | ||||
|     private val chapterRegex by lazy { Regex(""" [ \d,~.-]+화$""") } | ||||
| 
 | ||||
|     fun latestUpdatesElementParse(element: Element): SManga { | ||||
|         val linkElement = element.select("a.btn-primary") | ||||
|         val rawTitle = element.select(".post-subject > a").first()!!.ownText().trim() | ||||
| 
 | ||||
|         val title = rawTitle.trim().replace(chapterRegex, "") | ||||
| 
 | ||||
|         val manga = SManga.create() | ||||
|         manga.url = getUrlPath(linkElement.attr("href")) | ||||
|         manga.title = title | ||||
|         manga.thumbnail_url = element.select(".img-item > img").attr("src") | ||||
|         manga.initialized = false | ||||
|         return manga | ||||
|     } | ||||
| 
 | ||||
|     override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { | ||||
|         val url = ("$baseUrl/comic" + (if (page > 1) "/p$page" else "")).toHttpUrl().newBuilder() | ||||
| 
 | ||||
|         filters.forEach { filter -> | ||||
|             when (filter) { | ||||
|                 is SearchPublishTypeList -> { | ||||
|                     if (filter.state > 0) { | ||||
|                         url.addQueryParameter("publish", filter.values[filter.state]) | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 is SearchJaumTypeList -> { | ||||
|                     if (filter.state > 0) { | ||||
|                         url.addQueryParameter("jaum", filter.values[filter.state]) | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 is SearchGenreTypeList -> { | ||||
|                     val genres = filter.state.filter { it.state }.joinToString(",") { it.id } | ||||
|                     url.addQueryParameter("tag", genres) | ||||
|                 } | ||||
| 
 | ||||
|                 is SearchSortTypeList -> { | ||||
|                     val state = filter.state ?: return@forEach | ||||
|                     url.addQueryParameter("sst", arrayOf("wr_datetime", "wr_hit", "wr_good", "as_update")[state.index]) | ||||
|                     url.addQueryParameter("sod", if (state.ascending) "asc" else "desc") | ||||
|                 } | ||||
| 
 | ||||
|                 else -> {} | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (query.isNotBlank()) { | ||||
|             url.addQueryParameter("stx", query) | ||||
| 
 | ||||
|             // Remove some filter QueryParams that not working with query | ||||
|             url.removeAllQueryParameters("publish") | ||||
|             url.removeAllQueryParameters("jaum") | ||||
|             url.removeAllQueryParameters("tag") | ||||
|         } | ||||
| 
 | ||||
|         return GET(url.toString(), headers) | ||||
|     } | ||||
| 
 | ||||
|     private class SearchCheckBox(name: String, val id: String = name) : Filter.CheckBox(name) | ||||
| 
 | ||||
|     // [...document.querySelectorAll("form.form td")[3].querySelectorAll("span.btn")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n') | ||||
|     private class SearchPublishTypeList : Filter.Select<String>( | ||||
|         "Publish", | ||||
|         arrayOf( | ||||
|             "전체", | ||||
|             "주간", | ||||
|             "격주", | ||||
|             "월간", | ||||
|             "단편", | ||||
|             "단행본", | ||||
|             "완결", | ||||
|         ), | ||||
|     ) | ||||
| 
 | ||||
|     // [...document.querySelectorAll("form.form td")[4].querySelectorAll("span.btn")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n') | ||||
|     private class SearchJaumTypeList : Filter.Select<String>( | ||||
|         "Jaum", | ||||
|         arrayOf( | ||||
|             "전체", | ||||
|             "ㄱ", | ||||
|             "ㄴ", | ||||
|             "ㄷ", | ||||
|             "ㄹ", | ||||
|             "ㅁ", | ||||
|             "ㅂ", | ||||
|             "ㅅ", | ||||
|             "ㅇ", | ||||
|             "ㅈ", | ||||
|             "ㅊ", | ||||
|             "ㅋ", | ||||
|             "ㅌ", | ||||
|             "ㅍ", | ||||
|             "ㅎ", | ||||
|             "0-9", | ||||
|             "a-z", | ||||
|         ), | ||||
|     ) | ||||
| 
 | ||||
|     // [...document.querySelectorAll("form.form td")[6].querySelectorAll("span.btn")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n') | ||||
|     private class SearchGenreTypeList : Filter.Group<SearchCheckBox>( | ||||
|         "Genres", | ||||
|         arrayOf( | ||||
|             "전체", | ||||
|             "17", | ||||
|             "BL", | ||||
|             "SF", | ||||
|             "TS", | ||||
|             "개그", | ||||
|             "게임", | ||||
|             "도박", | ||||
|             "드라마", | ||||
|             "라노벨", | ||||
|             "러브코미디", | ||||
|             "먹방", | ||||
|             "백합", | ||||
|             "붕탁", | ||||
|             "순정", | ||||
|             "스릴러", | ||||
|             "스포츠", | ||||
|             "시대", | ||||
|             "애니화", | ||||
|             "액션", | ||||
|             "음악", | ||||
|             "이세계", | ||||
|             "일상", | ||||
|             "전생", | ||||
|             "추리", | ||||
|             "판타지", | ||||
|             "학원", | ||||
|             "호러", | ||||
|         ).map { SearchCheckBox(it) }, | ||||
|     ) | ||||
| 
 | ||||
|     private class SearchSortTypeList : Filter.Sort( | ||||
|         "Sort", | ||||
|         arrayOf( | ||||
|             "기본(날짜순)", | ||||
|             "인기순", | ||||
|             "추천순", | ||||
|             "업데이트순", | ||||
|         ), | ||||
|     ) | ||||
| 
 | ||||
|     override fun getFilterList() = FilterList( | ||||
|         SearchSortTypeList(), | ||||
|         Filter.Separator(), | ||||
|         Filter.Header(ignoredForTextSearch()), | ||||
|         SearchPublishTypeList(), | ||||
|         SearchJaumTypeList(), | ||||
|         SearchGenreTypeList(), | ||||
|     ) | ||||
| } | ||||
| @ -1,284 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.ko.newtoki | ||||
| 
 | ||||
| import android.content.SharedPreferences | ||||
| import android.util.Log | ||||
| import eu.kanade.tachiyomi.network.GET | ||||
| 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.ParsedHttpSource | ||||
| import eu.kanade.tachiyomi.util.asJsoup | ||||
| import okhttp3.HttpUrl.Companion.toHttpUrl | ||||
| import okhttp3.Response | ||||
| import org.jsoup.Jsoup | ||||
| import org.jsoup.nodes.Document | ||||
| import org.jsoup.nodes.Element | ||||
| import rx.Observable | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Calendar | ||||
| import java.util.Locale | ||||
| import java.util.concurrent.TimeUnit | ||||
| 
 | ||||
| /** | ||||
|  * NewToki Source | ||||
|  * | ||||
|  * Based on https://github.com/gnuboard/gnuboard5 | ||||
|  **/ | ||||
| abstract class NewToki( | ||||
|     override val name: String, | ||||
|     private val boardName: String, | ||||
|     private val preferences: SharedPreferences, | ||||
| ) : ParsedHttpSource(), ConfigurableSource { | ||||
| 
 | ||||
|     override val lang: String = "ko" | ||||
|     override val supportsLatest = true | ||||
| 
 | ||||
|     override val client by lazy { buildClient(withRateLimit = false) } | ||||
|     private val rateLimitedClient by lazy { buildClient(withRateLimit = true) } | ||||
| 
 | ||||
|     private fun buildClient(withRateLimit: Boolean) = | ||||
|         network.cloudflareClient.newBuilder() | ||||
|             .apply { if (withRateLimit) rateLimit(1, preferences.rateLimitPeriod.toLong()) } | ||||
|             .addInterceptor(DomainInterceptor) // not rate-limited | ||||
|             .connectTimeout(10, TimeUnit.SECONDS) // fail fast | ||||
|             .build() | ||||
| 
 | ||||
|     override fun popularMangaSelector() = "div#webtoon-list > ul > li" | ||||
| 
 | ||||
|     override fun popularMangaFromElement(element: Element): SManga { | ||||
|         val linkElement = element.getElementsByTag("a").first()!! | ||||
| 
 | ||||
|         val manga = SManga.create() | ||||
|         manga.url = getUrlPath(linkElement.attr("href")) | ||||
|         manga.title = element.select("span.title").first()!!.ownText() | ||||
|         manga.thumbnail_url = linkElement.getElementsByTag("img").attr("src") | ||||
|         return manga | ||||
|     } | ||||
| 
 | ||||
|     override fun popularMangaNextPageSelector() = "ul.pagination > li:last-child:not(.disabled)" | ||||
| 
 | ||||
|     // Do not add page parameter if page is 1 to prevent tracking. | ||||
|     override fun popularMangaRequest(page: Int) = GET("$baseUrl/$boardName" + if (page > 1) "/p$page" else "", headers) | ||||
| 
 | ||||
|     override fun searchMangaSelector() = popularMangaSelector() | ||||
|     override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) | ||||
|     override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() | ||||
|     override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { | ||||
|         return if (query.startsWith(PREFIX_ID_SEARCH)) { | ||||
|             val realQuery = query.removePrefix(PREFIX_ID_SEARCH) | ||||
|             val urlPath = "/$boardName/$realQuery" | ||||
|             rateLimitedClient.newCall(GET("$baseUrl$urlPath", headers)) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { response -> | ||||
|                     // the id is matches any of 'post' from their CMS board. | ||||
|                     // Includes Manga Details Page, Chapters, Comments, and etcs... | ||||
|                     actualMangaParseById(urlPath, response) | ||||
|                 } | ||||
|         } else { | ||||
|             super.fetchSearchManga(page, query, filters) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun actualMangaParseById(urlPath: String, response: Response): MangasPage { | ||||
|         val document = response.asJsoup() | ||||
| 
 | ||||
|         // Only exists on detail page. | ||||
|         val firstChapterButton = document.select("tr > th > button.btn-blue").first() | ||||
|         // only exists on chapter with proper manga detail page. | ||||
|         val fullListButton = document.select(".comic-navbar .toon-nav a").last() | ||||
| 
 | ||||
|         val list: List<SManga> = when { | ||||
|             firstChapterButton?.text()?.contains("첫회보기") == true -> { // Check this page is detail page | ||||
|                 val details = mangaDetailsParse(document) | ||||
|                 details.url = urlPath | ||||
|                 listOf(details) | ||||
|             } | ||||
|             fullListButton?.text()?.contains("전체목록") == true -> { // Check this page is chapter page | ||||
|                 val url = fullListButton.attr("abs:href") | ||||
|                 val details = mangaDetailsParse(rateLimitedClient.newCall(GET(url, headers)).execute()) | ||||
|                 details.url = getUrlPath(url) | ||||
|                 listOf(details) | ||||
|             } | ||||
|             else -> emptyList() | ||||
|         } | ||||
| 
 | ||||
|         return MangasPage(list, false) | ||||
|     } | ||||
| 
 | ||||
|     override fun mangaDetailsParse(document: Document): SManga { | ||||
|         val info = document.select("div.view-title > .view-content").first()!! | ||||
|         val title = document.select("div.view-content > span > b").text() | ||||
|         val thumbnail = document.select("div.row div.view-img > img").attr("src") | ||||
|         val descriptionElement = info.select("div.row div.view-content:not([style])") | ||||
|         val description = descriptionElement.map { | ||||
|             it.text().trim() | ||||
|         } | ||||
|         val prefix = if (isCleanPath(document.location())) "" else needMigration() | ||||
| 
 | ||||
|         val manga = SManga.create() | ||||
|         manga.title = title | ||||
|         manga.description = description.joinToString("\n", prefix = prefix) | ||||
|         manga.thumbnail_url = thumbnail | ||||
|         descriptionElement.forEach { | ||||
|             val text = it.text() | ||||
|             when { | ||||
|                 "작가" in text -> manga.author = it.getElementsByTag("a").text() | ||||
|                 "분류" in text -> { | ||||
|                     val genres = mutableListOf<String>() | ||||
|                     it.getElementsByTag("a").forEach { item -> | ||||
|                         genres.add(item.text()) | ||||
|                     } | ||||
|                     manga.genre = genres.joinToString(", ") | ||||
|                 } | ||||
|                 "발행구분" in text -> manga.status = parseStatus(it.getElementsByTag("a").text()) | ||||
|             } | ||||
|         } | ||||
|         return manga | ||||
|     } | ||||
| 
 | ||||
|     private fun parseStatus(status: String) = when (status.trim()) { | ||||
|         "주간", "격주", "월간", "격월/비정기", "단행본" -> SManga.ONGOING | ||||
|         "단편", "완결" -> SManga.COMPLETED | ||||
|         else -> SManga.UNKNOWN | ||||
|     } | ||||
| 
 | ||||
|     override fun chapterListSelector() = "div.serial-list > ul.list-body > li.list-item" | ||||
| 
 | ||||
|     override fun chapterFromElement(element: Element): SChapter { | ||||
|         val linkElement = element.select(".wr-subject > a.item-subject").last()!! | ||||
|         val rawName = linkElement.ownText().trim() | ||||
| 
 | ||||
|         val chapter = SChapter.create() | ||||
|         chapter.setUrlWithoutDomain(linkElement.attr("href")) | ||||
|         chapter.chapter_number = parseChapterNumber(rawName) | ||||
|         chapter.name = rawName | ||||
|         chapter.date_upload = parseChapterDate(element.select(".wr-date").last()!!.text().trim()) | ||||
|         return chapter | ||||
|     } | ||||
| 
 | ||||
|     private fun parseChapterNumber(name: String): Float { | ||||
|         try { | ||||
|             if (name.contains("[단편]")) return 1f | ||||
|             // `특별` means `Special`, so It can be buggy. so pad `편`(Chapter) to prevent false return | ||||
|             if (name.contains("번외") || name.contains("특별편")) return -2f | ||||
|             val regex = chapterNumberRegex | ||||
|             val (ch_primal, ch_second) = regex.find(name)!!.destructured | ||||
|             return (ch_primal + if (ch_second.isBlank()) "" else ".$ch_second").toFloatOrNull() | ||||
|                 ?: -1f | ||||
|         } catch (e: Exception) { | ||||
|             Log.e("NewToki", "failed to parse chapter number '$name'", e) | ||||
|             return -1f | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun mangaDetailsParseWithTitleCheck(manga: SManga, document: Document) = | ||||
|         mangaDetailsParse(document).apply { | ||||
|             // TODO: don't throw when there is download folder rename feature | ||||
|             if (manga.description.isNullOrEmpty() && manga.title != title) { | ||||
|                 throw Exception(titleNotMatch(title)) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     override fun fetchMangaDetails(manga: SManga): Observable<SManga> { | ||||
|         return rateLimitedClient.newCall(mangaDetailsRequest(manga)) | ||||
|             .asObservableSuccess() | ||||
|             .map { response -> | ||||
|                 val document = response.asJsoup() | ||||
|                 mangaDetailsParseWithTitleCheck(manga, document).apply { initialized = true } | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
|     override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { | ||||
|         return rateLimitedClient.newCall(chapterListRequest(manga)) | ||||
|             .asObservableSuccess() | ||||
|             .map { response -> | ||||
|                 val document = response.asJsoup() | ||||
|                 val title = mangaDetailsParseWithTitleCheck(manga, document).title | ||||
|                 document.select(chapterListSelector()).map { | ||||
|                     chapterFromElement(it).apply { | ||||
|                         name = name.removePrefix(title).trimStart() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
| 
 | ||||
|     // not thread-safe | ||||
|     private val dateFormat by lazy { SimpleDateFormat("yyyy.MM.dd", Locale.ENGLISH) } | ||||
| 
 | ||||
|     private fun parseChapterDate(date: String): Long { | ||||
|         return try { | ||||
|             if (date.contains(":")) { | ||||
|                 val calendar = Calendar.getInstance() | ||||
|                 val splitDate = date.split(":") | ||||
| 
 | ||||
|                 val hours = splitDate.first().toInt() | ||||
|                 val minutes = splitDate.last().toInt() | ||||
| 
 | ||||
|                 val calendarHours = calendar.get(Calendar.HOUR) | ||||
|                 val calendarMinutes = calendar.get(Calendar.MINUTE) | ||||
| 
 | ||||
|                 if (calendarHours >= hours && calendarMinutes > minutes) { | ||||
|                     calendar.add(Calendar.DATE, -1) | ||||
|                 } | ||||
| 
 | ||||
|                 calendar.timeInMillis | ||||
|             } else { | ||||
|                 dateFormat.parse(date)?.time ?: 0 | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             Log.e("NewToki", "failed to parse chapter date '$date'", e) | ||||
|             0 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun pageListParse(document: Document): List<Page> { | ||||
|         val script = document.select("script:containsData(html_data)").firstOrNull()?.data() | ||||
|             ?: throw Exception("data script not found") | ||||
|         val loadScript = document.select("script:containsData(data_attribute)").firstOrNull()?.data() | ||||
|             ?: throw Exception("load script not found") | ||||
|         val dataAttr = "abs:data-" + loadScript.substringAfter("data_attribute: '").substringBefore("',") | ||||
| 
 | ||||
|         return htmlDataRegex.findAll(script).map { it.groupValues[1] } | ||||
|             .asIterable() | ||||
|             .flatMap { it.split(".") } | ||||
|             .joinToString("") { it.toIntOrNull(16)?.toChar()?.toString() ?: "" } | ||||
|             .let { Jsoup.parse(it) } | ||||
|             .select("img[src=/img/loading-image.gif], .view-img > img[itemprop]") | ||||
|             .mapIndexed { i, img -> Page(i, "", if (img.hasAttr(dataAttr)) img.attr(dataAttr) else img.attr("abs:content")) } | ||||
|     } | ||||
| 
 | ||||
|     override fun latestUpdatesSelector() = ".media.post-list" | ||||
|     override fun latestUpdatesFromElement(element: Element) = ManaToki.latestUpdatesElementParse(element) | ||||
|     override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/update?hid=update&page=$page", headers) | ||||
|     override fun latestUpdatesNextPageSelector() = ".pg_end" | ||||
| 
 | ||||
|     // We are able to get the image URL directly from the page list | ||||
|     override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("This method should not be called!") | ||||
| 
 | ||||
|     override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) { | ||||
|         getPreferencesInternal(screen.context).map(screen::addPreference) | ||||
|     } | ||||
| 
 | ||||
|     protected fun getUrlPath(orig: String): String { | ||||
|         val url = baseUrl.toHttpUrl().resolve(orig) ?: return orig | ||||
|         val pathSegments = url.pathSegments | ||||
|         return "/${pathSegments[0]}/${pathSegments[1]}" | ||||
|     } | ||||
| 
 | ||||
|     private fun isCleanPath(absUrl: String): Boolean { | ||||
|         val url = absUrl.toHttpUrl() | ||||
|         return url.pathSegments.size == 2 && url.querySize == 0 && url.fragment == null | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         const val PREFIX_ID_SEARCH = "id:" | ||||
| 
 | ||||
|         private val chapterNumberRegex by lazy { Regex("([0-9]+)(?:[-.]([0-9]+))?화") } | ||||
|         private val htmlDataRegex by lazy { Regex("""html_data\+='([^']+)'""") } | ||||
|     } | ||||
| } | ||||
| @ -1,146 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.ko.newtoki | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.network.GET | ||||
| import eu.kanade.tachiyomi.source.model.Filter | ||||
| import eu.kanade.tachiyomi.source.model.FilterList | ||||
| import okhttp3.HttpUrl.Companion.toHttpUrl | ||||
| import okhttp3.Request | ||||
| 
 | ||||
| object NewTokiWebtoon : NewToki("NewToki", "webtoon", newTokiPreferences) { | ||||
|     // / ! DO NOT CHANGE THIS !  Prevent to treating as a new site | ||||
|     override val id = NEWTOKI_ID | ||||
| 
 | ||||
|     override val baseUrl get() = "https://$NEWTOKI_PREFIX$domainNumber.com" | ||||
| 
 | ||||
|     override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { | ||||
|         val url = ("$baseUrl/webtoon" + (if (page > 1) "/p$page" else "")).toHttpUrl().newBuilder() | ||||
|         filters.forEach { filter -> | ||||
|             when (filter) { | ||||
|                 is SearchTargetTypeList -> { | ||||
|                     if (filter.state > 0) { | ||||
|                         url.addQueryParameter("toon", filter.values[filter.state]) | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 is SearchSortTypeList -> { | ||||
|                     val state = filter.state ?: return@forEach | ||||
|                     url.addQueryParameter("sst", arrayOf("as_update", "wr_hit", "wr_good")[state.index]) | ||||
|                     url.addQueryParameter("sod", if (state.ascending) "asc" else "desc") | ||||
|                 } | ||||
| 
 | ||||
|                 else -> {} | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Incompatible with Other Search Parameter | ||||
|         if (!query.isBlank()) { | ||||
|             url.addQueryParameter("stx", query) | ||||
|         } else { | ||||
|             filters.forEach { filter -> | ||||
|                 when (filter) { | ||||
|                     is SearchYoilTypeList -> { | ||||
|                         if (filter.state > 0) { | ||||
|                             url.addQueryParameter("yoil", filter.values[filter.state]) | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     is SearchJaumTypeList -> { | ||||
|                         if (filter.state > 0) { | ||||
|                             url.addQueryParameter("jaum", filter.values[filter.state]) | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     is SearchGenreTypeList -> { | ||||
|                         if (filter.state > 0) { | ||||
|                             url.addQueryParameter("tag", filter.values[filter.state]) | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     else -> {} | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return GET(url.toString(), headers) | ||||
|     } | ||||
| 
 | ||||
|     private class SearchTargetTypeList : Filter.Select<String>("Type", arrayOf("전체", "일반웹툰", "성인웹툰", "BL/GL", "완결웹툰")) | ||||
| 
 | ||||
|     // [...document.querySelectorAll("form.form td")[1].querySelectorAll("a")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n') | ||||
|     private class SearchYoilTypeList : Filter.Select<String>( | ||||
|         "Day of the Week", | ||||
|         arrayOf( | ||||
|             "전체", | ||||
|             "월", | ||||
|             "화", | ||||
|             "수", | ||||
|             "목", | ||||
|             "금", | ||||
|             "토", | ||||
|             "일", | ||||
|             "열흘", | ||||
|         ), | ||||
|     ) | ||||
| 
 | ||||
|     // [...document.querySelectorAll("form.form td")[2].querySelectorAll("a")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n') | ||||
|     private class SearchJaumTypeList : Filter.Select<String>( | ||||
|         "Jaum", | ||||
|         arrayOf( | ||||
|             "전체", | ||||
|             "ㄱ", | ||||
|             "ㄴ", | ||||
|             "ㄷ", | ||||
|             "ㄹ", | ||||
|             "ㅁ", | ||||
|             "ㅂ", | ||||
|             "ㅅ", | ||||
|             "ㅇ", | ||||
|             "ㅈ", | ||||
|             "ㅊ", | ||||
|             "ㅋ", | ||||
|             "ㅌ", | ||||
|             "ㅍ", | ||||
|             "ㅎ", | ||||
|             "a-z", | ||||
|             "0-9", | ||||
|         ), | ||||
|     ) | ||||
| 
 | ||||
|     // [...document.querySelectorAll("form.form td")[3].querySelectorAll("a")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n') | ||||
|     private class SearchGenreTypeList : Filter.Select<String>( | ||||
|         "Genre", | ||||
|         arrayOf( | ||||
|             "전체", | ||||
|             "판타지", | ||||
|             "액션", | ||||
|             "개그", | ||||
|             "미스터리", | ||||
|             "로맨스", | ||||
|             "드라마", | ||||
|             "무협", | ||||
|             "스포츠", | ||||
|             "일상", | ||||
|             "학원", | ||||
|             "성인", | ||||
|         ), | ||||
|     ) | ||||
| 
 | ||||
|     private class SearchSortTypeList : Filter.Sort( | ||||
|         "Sort", | ||||
|         arrayOf( | ||||
|             "기본(업데이트순)", | ||||
|             "인기순", | ||||
|             "추천순", | ||||
|         ), | ||||
|     ) | ||||
| 
 | ||||
|     override fun getFilterList() = FilterList( | ||||
|         SearchTargetTypeList(), | ||||
|         SearchSortTypeList(), | ||||
|         Filter.Separator(), | ||||
|         Filter.Header(ignoredForTextSearch()), | ||||
|         SearchYoilTypeList(), | ||||
|         SearchJaumTypeList(), | ||||
|         SearchGenreTypeList(), | ||||
|     ) | ||||
| } | ||||
| @ -1,90 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.ko.newtoki | ||||
| 
 | ||||
| import android.app.Application | ||||
| import android.content.Context | ||||
| import android.content.SharedPreferences | ||||
| import androidx.preference.EditTextPreference | ||||
| import androidx.preference.ListPreference | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| 
 | ||||
| const val MANATOKI_ID = 2526381983439079467L // "NewToki/ko/1" | ||||
| const val NEWTOKI_ID = 1977818283770282459L // "NewToki (Webtoon)/ko/1" | ||||
| 
 | ||||
| const val MANATOKI_PREFIX = "manatoki" | ||||
| const val NEWTOKI_PREFIX = "newtoki" | ||||
| 
 | ||||
| val manaTokiPreferences = getSharedPreferences(MANATOKI_ID).migrate() | ||||
| val newTokiPreferences = getSharedPreferences(NEWTOKI_ID).migrate() | ||||
| 
 | ||||
| fun getPreferencesInternal(context: Context) = arrayOf( | ||||
| 
 | ||||
|     EditTextPreference(context).apply { | ||||
|         key = DOMAIN_NUMBER_PREF | ||||
|         title = domainNumberTitle() | ||||
|         summary = domainNumberSummary() | ||||
|         setOnPreferenceChangeListener { _, newValue -> | ||||
|             val value = newValue as String | ||||
|             if (value.isEmpty() || value != value.trim()) { | ||||
|                 false | ||||
|             } else { | ||||
|                 domainNumber = value | ||||
|                 true | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     ListPreference(context).apply { | ||||
|         key = RATE_LIMIT_PERIOD_PREF | ||||
|         title = rateLimitTitle() | ||||
|         summary = "%s\n" + requiresAppRestart() | ||||
| 
 | ||||
|         val values = Array(RATE_LIMIT_PERIOD_MAX) { (it + 1).toString() } | ||||
|         entries = Array(RATE_LIMIT_PERIOD_MAX) { rateLimitEntry(values[it]) } | ||||
|         entryValues = values | ||||
| 
 | ||||
|         setDefaultValue(RATE_LIMIT_PERIOD_DEFAULT) | ||||
|     }, | ||||
| ) | ||||
| 
 | ||||
| var SharedPreferences.domainNumber: String | ||||
|     get() = getString(DOMAIN_NUMBER_PREF, "")!! | ||||
|     set(value) = edit().putString(DOMAIN_NUMBER_PREF, value).apply() | ||||
| 
 | ||||
| val SharedPreferences.rateLimitPeriod: Int | ||||
|     get() = getString(RATE_LIMIT_PERIOD_PREF, RATE_LIMIT_PERIOD_DEFAULT)!!.toInt().coerceIn(1, RATE_LIMIT_PERIOD_MAX) | ||||
| 
 | ||||
| private fun SharedPreferences.migrate(): SharedPreferences { | ||||
|     if ("Override BaseUrl" !in this) return this // already migrated | ||||
|     val editor = edit().clear() // clear all legacy preferences listed below | ||||
|     val oldValue = try { // this was a long | ||||
|         getLong(RATE_LIMIT_PERIOD_PREF, -1).toInt() | ||||
|     } catch (_: ClassCastException) { | ||||
|         -1 | ||||
|     } | ||||
|     if (oldValue != -1) { // convert to string | ||||
|         val newValue = oldValue.coerceIn(1, RATE_LIMIT_PERIOD_MAX) | ||||
|         editor.putString(RATE_LIMIT_PERIOD_PREF, newValue.toString()) | ||||
|     } | ||||
|     editor.apply() | ||||
|     return this | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Don't use the following legacy keys: | ||||
|  * - "Override BaseUrl" | ||||
|  * - "overrideBaseUrl_v${AppInfo.getVersionName()}" | ||||
|  * - "Enable Latest (Experimental)" | ||||
|  * - "fetchLatestExperiment" | ||||
|  * - "Fetch Latest with detail (Optional)" | ||||
|  * - "fetchLatestWithDetail" | ||||
|  * - "Rate Limit Request Period Seconds" | ||||
|  */ | ||||
| 
 | ||||
| private const val DOMAIN_NUMBER_PREF = "domainNumber" | ||||
| private const val RATE_LIMIT_PERIOD_PREF = "rateLimitPeriod" | ||||
| private const val RATE_LIMIT_PERIOD_DEFAULT = 2.toString() | ||||
| private const val RATE_LIMIT_PERIOD_MAX = 9 | ||||
| 
 | ||||
| private fun getSharedPreferences(id: Long): SharedPreferences = | ||||
|     Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) | ||||
| @ -1,76 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.ko.newtoki | ||||
| 
 | ||||
| import android.os.Build | ||||
| import android.os.LocaleList | ||||
| import java.util.Locale | ||||
| 
 | ||||
| private val useKorean by lazy { | ||||
|     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { | ||||
|         LocaleList.getDefault().getFirstMatch(arrayOf("ko", "en"))?.language == "ko" | ||||
|     } else { | ||||
|         Locale.getDefault().language == "ko" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // region Prompts | ||||
| 
 | ||||
| fun solveCaptcha() = when { | ||||
|     useKorean -> "WebView에서 캡챠 풀기" | ||||
|     else -> "Solve Captcha with WebView" | ||||
| } | ||||
| 
 | ||||
| fun titleNotMatch(realTitle: String) = when { | ||||
|     useKorean -> "이 만화를 찾으시려면 '$realTitle'으로 검색하세요" | ||||
|     else -> "Find this manga by searching '$realTitle'" | ||||
| } | ||||
| 
 | ||||
| fun needMigration() = when { | ||||
|     useKorean -> "이 항목은 URL 포맷이 틀립니다. 중복된 항목을 피하려면 동일한 소스로 이전하세요.\n\n" | ||||
|     else -> "This entry has wrong URL format. Please migrate to the same source to avoid duplicates.\n\n" | ||||
| } | ||||
| 
 | ||||
| // endregion | ||||
| 
 | ||||
| // region Filters | ||||
| 
 | ||||
| fun ignoredForTextSearch() = when { | ||||
|     useKorean -> "검색에서 다음 필터 항목은 무시됩니다" | ||||
|     else -> "The following filters are ignored for text search" | ||||
| } | ||||
| 
 | ||||
| // endregion | ||||
| 
 | ||||
| // region Preferences | ||||
| 
 | ||||
| fun domainNumberTitle() = when { | ||||
|     useKorean -> "도메인 번호" | ||||
|     else -> "Domain number" | ||||
| } | ||||
| 
 | ||||
| fun domainNumberSummary() = when { | ||||
|     useKorean -> "도메인 번호는 자동으로 갱신됩니다" | ||||
|     else -> "This number is updated automatically" | ||||
| } | ||||
| 
 | ||||
| fun editDomainNumber() = when { | ||||
|     useKorean -> "확장기능 설정에서 도메인 번호를 수정해 주세요" | ||||
|     else -> "Please edit domain number in extension settings" | ||||
| } | ||||
| 
 | ||||
| fun rateLimitTitle() = when { | ||||
|     useKorean -> "요청 제한" | ||||
|     else -> "Rate limit" | ||||
| } | ||||
| 
 | ||||
| fun rateLimitEntry(period: String) = when { | ||||
|     useKorean -> "${period}초마다 요청" | ||||
|     else -> "1 request every $period seconds" | ||||
| } | ||||
| 
 | ||||
| // taken from app strings | ||||
| fun requiresAppRestart() = when { | ||||
|     useKorean -> "설정을 적용하려면 앱을 재시작하세요" | ||||
|     else -> "Requires app restart to take effect" | ||||
| } | ||||
| 
 | ||||
| // endregion | ||||
| @ -1,7 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.ko.newtoki | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.source.SourceFactory | ||||
| 
 | ||||
| class TokiFactory : SourceFactory { | ||||
|     override fun createSources() = listOf(ManaToki, NewTokiWebtoon) | ||||
| } | ||||
 arkon
						arkon