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
|
- SetsuScans https://github.com/tachiyomiorg/tachiyomi-extensions/issues/11040
|
||||||
- ShinobiScans https://github.com/tachiyomiorg/tachiyomi-extensions/issues/14457
|
- ShinobiScans https://github.com/tachiyomiorg/tachiyomi-extensions/issues/14457
|
||||||
- XXX Yaoi https://github.com/tachiyomiorg/tachiyomi-extensions/issues/9535
|
- 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("ROG Mangás", "https://rogmangas.com", "pt-BR", pkgName = "mangasoverall", className = "RogMangas", overrideVersionCode = 1),
|
||||||
SingleLang("Romantik Manga", "https://romantikmanga.com", "tr"),
|
SingleLang("Romantik Manga", "https://romantikmanga.com", "tr"),
|
||||||
SingleLang("Rüya Manga", "https://www.ruyamanga.com", "tr", className = "RuyaManga", overrideVersionCode = 1),
|
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("Sagrado Império da Britannia", "https://imperiodabritannia.com", "pt-BR", className = "ImperioDaBritannia"),
|
||||||
SingleLang("SamuraiScan", "https://samuraiscan.com", "es", overrideVersionCode = 3),
|
SingleLang("SamuraiScan", "https://samuraiscan.com", "es", overrideVersionCode = 3),
|
||||||
SingleLang("Sawamics", "https://sawamics.com", "en"),
|
SingleLang("Sawamics", "https://sawamics.com", "en"),
|
||||||
|
|
|
@ -19,7 +19,7 @@ if (System.getenv("CI") == null || System.getenv("CI_MODULE_GEN") == "true") {
|
||||||
*/
|
*/
|
||||||
loadAllIndividualExtensions()
|
loadAllIndividualExtensions()
|
||||||
loadAllGeneratedMultisrcExtensions()
|
loadAllGeneratedMultisrcExtensions()
|
||||||
// loadIndividualExtension("all", "mangadex")
|
// loadIndividualExtension("all", "komga")
|
||||||
// loadGeneratedMultisrcExtension("en", "guya")
|
// loadGeneratedMultisrcExtension("en", "guya")
|
||||||
} else {
|
} else {
|
||||||
// Running in CI (GitHub Actions)
|
// 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)
|
|
||||||
}
|
|