re-add removed extensions
|
@ -0,0 +1,34 @@
|
|||
package eu.kanade.tachiyomi.lib.unpacker
|
||||
|
||||
/**
|
||||
* A helper class to extract substrings efficiently.
|
||||
*
|
||||
* Note that all methods move [startIndex] over the ending delimiter.
|
||||
*/
|
||||
class SubstringExtractor(private val text: String) {
|
||||
private var startIndex = 0
|
||||
|
||||
fun skipOver(str: String) {
|
||||
val index = text.indexOf(str, startIndex)
|
||||
if (index == -1) return
|
||||
startIndex = index + str.length
|
||||
}
|
||||
|
||||
fun substringBefore(str: String): String {
|
||||
val index = text.indexOf(str, startIndex)
|
||||
if (index == -1) return ""
|
||||
val result = text.substring(startIndex, index)
|
||||
startIndex = index + str.length
|
||||
return result
|
||||
}
|
||||
|
||||
fun substringBetween(left: String, right: String): String {
|
||||
val index = text.indexOf(left, startIndex)
|
||||
if (index == -1) return ""
|
||||
val leftIndex = index + left.length
|
||||
val rightIndex = text.indexOf(right, leftIndex)
|
||||
if (rightIndex == -1) return ""
|
||||
startIndex = rightIndex + right.length
|
||||
return text.substring(leftIndex, rightIndex)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package eu.kanade.tachiyomi.lib.unpacker
|
||||
|
||||
/**
|
||||
* Helper class to unpack JavaScript code compressed by [packer](http://dean.edwards.name/packer/).
|
||||
*
|
||||
* Source code of packer can be found [here](https://github.com/evanw/packer/blob/master/packer.js).
|
||||
*/
|
||||
object Unpacker {
|
||||
|
||||
/**
|
||||
* Unpacks JavaScript code compressed by packer.
|
||||
*
|
||||
* Specify [left] and [right] to unpack only the data between them.
|
||||
*
|
||||
* Note: single quotes `\'` in the data will be replaced with double quotes `"`.
|
||||
*/
|
||||
fun unpack(script: String, left: String? = null, right: String? = null): String =
|
||||
unpack(SubstringExtractor(script), left, right)
|
||||
|
||||
/**
|
||||
* Unpacks JavaScript code compressed by packer.
|
||||
*
|
||||
* Specify [left] and [right] to unpack only the data between them.
|
||||
*
|
||||
* Note: single quotes `\'` in the data will be replaced with double quotes `"`.
|
||||
*/
|
||||
fun unpack(script: SubstringExtractor, left: String? = null, right: String? = null): String {
|
||||
val packed = script
|
||||
.substringBetween("}('", ".split('|'),0,{}))")
|
||||
.replace("\\'", "\"")
|
||||
|
||||
val parser = SubstringExtractor(packed)
|
||||
val data: String
|
||||
if (left != null && right != null) {
|
||||
data = parser.substringBetween(left, right)
|
||||
parser.skipOver("',")
|
||||
} else {
|
||||
data = parser.substringBefore("',")
|
||||
}
|
||||
if (data.isEmpty()) return ""
|
||||
|
||||
val dictionary = parser.substringBetween("'", "'").split("|")
|
||||
val size = dictionary.size
|
||||
|
||||
return wordRegex.replace(data) {
|
||||
val key = it.value
|
||||
val index = parseRadix62(key)
|
||||
if (index >= size) return@replace key
|
||||
dictionary[index].ifEmpty { key }
|
||||
}
|
||||
}
|
||||
|
||||
private val wordRegex by lazy { Regex("""\w+""") }
|
||||
|
||||
private fun parseRadix62(str: String): Int {
|
||||
var result = 0
|
||||
for (ch in str.toCharArray()) {
|
||||
result = result * 62 + when {
|
||||
ch.code <= '9'.code -> { // 0-9
|
||||
ch.code - '0'.code
|
||||
}
|
||||
|
||||
ch.code >= 'a'.code -> { // a-z
|
||||
// ch - 'a' + 10
|
||||
ch.code - ('a'.code - 10)
|
||||
}
|
||||
|
||||
else -> { // A-Z
|
||||
// ch - 'A' + 36
|
||||
ch.code - ('A'.code - 36)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 7.9 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 338 KiB |
|
@ -0,0 +1,11 @@
|
|||
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,6 +422,7 @@ class MadaraGenerator : ThemeSourceGenerator {
|
|||
SingleLang("ROG Mangás", "https://rogmangas.com", "pt-BR", pkgName = "mangasoverall", className = "RogMangas", overrideVersionCode = 1),
|
||||
SingleLang("Romantik Manga", "https://romantikmanga.com", "tr"),
|
||||
SingleLang("Rüya Manga", "https://www.ruyamanga.com", "tr", className = "RuyaManga", overrideVersionCode = 1),
|
||||
SingleLang("S2Manga", "https://www.s2manga.com", "en", overrideVersionCode = 2),
|
||||
SingleLang("Sagrado Império da Britannia", "https://imperiodabritannia.com", "pt-BR", className = "ImperioDaBritannia"),
|
||||
SingleLang("SamuraiScan", "https://samuraiscan.com", "es", overrideVersionCode = 3),
|
||||
SingleLang("Sawamics", "https://sawamics.com", "en"),
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
<?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>
|
|
@ -0,0 +1,201 @@
|
|||
## 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
|
|
@ -0,0 +1,20 @@
|
|||
# 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)
|
|
@ -0,0 +1,17 @@
|
|||
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'))
|
||||
}
|
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 67 KiB |
|
@ -0,0 +1,974 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
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"),
|
||||
)
|
|
@ -0,0 +1,51 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?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>
|
|
@ -0,0 +1,70 @@
|
|||
# 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
|
||||
```
|
|
@ -0,0 +1,150 @@
|
|||
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
|
|
@ -0,0 +1,108 @@
|
|||
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
|
|
@ -0,0 +1,119 @@
|
|||
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
|
|
@ -0,0 +1,138 @@
|
|||
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
|
|
@ -0,0 +1,17 @@
|
|||
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"
|
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 24 KiB |
|
@ -0,0 +1,163 @@
|
|||
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",
|
||||
)
|
||||
}
|
|
@ -0,0 +1,903 @@
|
|||
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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
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")
|
|
@ -0,0 +1,400 @@
|
|||
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))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,490 @@
|
|||
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
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
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"
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
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,
|
||||
)
|
|
@ -0,0 +1,25 @@
|
|||
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,
|
||||
)
|
|
@ -0,0 +1,16 @@
|
|||
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()
|
|
@ -0,0 +1,30 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
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()
|
|
@ -0,0 +1,16 @@
|
|||
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()
|
|
@ -0,0 +1,18 @@
|
|||
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()
|
|
@ -0,0 +1,114 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
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,
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
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()
|
|
@ -0,0 +1,12 @@
|
|||
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()
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
|
@ -0,0 +1,48 @@
|
|||
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)
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 348 KiB |
|
@ -0,0 +1,73 @@
|
|||
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""") }
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
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"
|
|
@ -0,0 +1,174 @@
|
|||
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(),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
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\+='([^']+)'""") }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
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(),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
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)
|
|
@ -0,0 +1,76 @@
|
|||
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
|
|
@ -0,0 +1,7 @@
|
|||
package eu.kanade.tachiyomi.extension.ko.newtoki
|
||||
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class TokiFactory : SourceFactory {
|
||||
override fun createSources() = listOf(ManaToki, NewTokiWebtoon)
|
||||
}
|