Compare commits
No commits in common. "2cfdda0bcff616fe7759bfa692761886afd2b1aa" and "dfb448cbffa8cd9ca190cbd1f689911d2f02c2ab" have entirely different histories.
2cfdda0bcf
...
dfb448cbff
1
.gitignore
vendored
@ -10,4 +10,3 @@ repo/
|
|||||||
apk/
|
apk/
|
||||||
gen
|
gen
|
||||||
generated-src/
|
generated-src/
|
||||||
.kotlin
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
@ -38,7 +38,6 @@ kotlinter {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
|
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
|
||||||
implementation(project(":utils"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
@ -52,9 +51,3 @@ tasks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("printDependentExtensions") {
|
|
||||||
doLast {
|
|
||||||
project.printDependentExtensions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -103,7 +103,6 @@ dependencies {
|
|||||||
if (theme != null) implementation(theme) // Overrides core launcher icons
|
if (theme != null) implementation(theme) // Overrides core launcher icons
|
||||||
implementation(project(":core"))
|
implementation(project(":core"))
|
||||||
compileOnly(libs.bundles.common)
|
compileOnly(libs.bundles.common)
|
||||||
implementation(project(":utils"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register("writeManifestFile") {
|
tasks.register("writeManifestFile") {
|
||||||
|
@ -9,7 +9,7 @@ gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.
|
|||||||
gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
|
gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
|
||||||
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }
|
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }
|
||||||
|
|
||||||
tachiyomi-lib = { module = "com.github.keiyoushi:extensions-lib", version = "v1.4.2.1" }
|
tachiyomi-lib = { module = "com.github.tachiyomiorg:extensions-lib", version = "1.4.2" }
|
||||||
|
|
||||||
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin_version" }
|
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin_version" }
|
||||||
kotlin-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization_version" }
|
kotlin-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization_version" }
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 7
|
baseVersionCode = 5
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:synchrony"))
|
api(project(":lib:synchrony"))
|
||||||
|
@ -276,14 +276,14 @@ abstract class ColaManga(
|
|||||||
}.also(screen::addPreference)
|
}.also(screen::addPreference)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val keyMappingRegex = Regex("""if\s*\(\s*([a-zA-Z0-9_]+)\s*==\s*(\d+)\s*\)\s*\{\s*return\s*'([a-zA-Z0-9_]+)'\s*;""")
|
private val keyMappingRegex = Regex("""if\s*\(\s*([a-zA-Z0-9_]+)\s*==\s*(?<keyType>\d+)\s*\)\s*\{\s*return\s*'(?<key>[a-zA-Z0-9_]+)'\s*;""")
|
||||||
|
|
||||||
private val keyMapping by lazy {
|
private val keyMapping by lazy {
|
||||||
val obfuscatedReadJs = client.newCall(GET("$baseUrl/js/manga.read.js")).execute().body.string()
|
val obfuscatedReadJs = client.newCall(GET("$baseUrl/js/manga.read.js")).execute().body.string()
|
||||||
val readJs = Deobfuscator.deobfuscateScript(obfuscatedReadJs)
|
val readJs = Deobfuscator.deobfuscateScript(obfuscatedReadJs)
|
||||||
?: throw Exception(intl.couldNotDeobufscateScript)
|
?: throw Exception(intl.couldNotDeobufscateScript)
|
||||||
|
|
||||||
keyMappingRegex.findAll(readJs).associate { it.groups[2]!!.value to it.groups[3]!!.value }
|
keyMappingRegex.findAll(readJs).associate { it.groups["keyType"]!!.value to it.groups["key"]!!.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun randomString() = buildString(15) {
|
private fun randomString() = buildString(15) {
|
||||||
|
22
lib-multisrc/etoshore/AndroidManifest.xml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application>
|
||||||
|
<activity
|
||||||
|
android:name="eu.kanade.tachiyomi.multisrc.etoshore.EtoshoreUrlActivity"
|
||||||
|
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="${SOURCEHOST}"
|
||||||
|
android:pathPattern="/.*/..*"
|
||||||
|
android:scheme="${SOURCESCHEME}" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
5
lib-multisrc/etoshore/build.gradle.kts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
plugins {
|
||||||
|
id("lib-multisrc")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseVersionCode = 1
|
@ -0,0 +1,242 @@
|
|||||||
|
package eu.kanade.tachiyomi.multisrc.etoshore
|
||||||
|
|
||||||
|
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.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 okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
abstract class Etoshore(
|
||||||
|
override val name: String,
|
||||||
|
override val baseUrl: String,
|
||||||
|
final override val lang: String,
|
||||||
|
) : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
|
// ============================== Popular ==============================
|
||||||
|
|
||||||
|
open val popularFilter = FilterList(
|
||||||
|
SelectionList("", listOf(Tag(value = "views", query = "sort"))),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
|
||||||
|
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = throw UnsupportedOperationException()
|
||||||
|
override fun popularMangaNextPageSelector() = throw UnsupportedOperationException()
|
||||||
|
override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// ============================== Latest ===============================
|
||||||
|
|
||||||
|
open val latestFilter = FilterList(
|
||||||
|
SelectionList("", listOf(Tag(value = "date", query = "sort"))),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
|
||||||
|
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
|
||||||
|
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
|
||||||
|
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// ============================== Search ===============================
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = "$baseUrl/page/$page".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("s", query)
|
||||||
|
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is SelectionList -> {
|
||||||
|
val selected = filter.selected()
|
||||||
|
url.addQueryParameter(selected.query, selected.value)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GET(url.build(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
if (query.startsWith(PREFIX_SEARCH)) {
|
||||||
|
val slug = query.substringAfter(PREFIX_SEARCH)
|
||||||
|
return fetchMangaDetails(SManga.create().apply { url = "/manga/$slug/" })
|
||||||
|
.map { manga -> MangasPage(listOf(manga), false) }
|
||||||
|
}
|
||||||
|
return super.fetchSearchManga(page, query, filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = ".search-posts .chapter-box .poster a"
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = ".navigation .naviright:has(a)"
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
|
title = element.attr("title")
|
||||||
|
thumbnail_url = element.selectFirst("img")?.let(::imageFromElement)
|
||||||
|
setUrlWithoutDomain(element.absUrl("href"))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
if (filterList.isEmpty()) {
|
||||||
|
filterParse(response)
|
||||||
|
}
|
||||||
|
return super.searchMangaParse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Details ===============================
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
|
title = document.selectFirst("h1")!!.text()
|
||||||
|
description = document.selectFirst(".excerpt p")?.text()
|
||||||
|
document.selectFirst(".details-right-con img")?.let { thumbnail_url = imageFromElement(it) }
|
||||||
|
genre = document.select("div.meta-item span.meta-title:contains(Genres) + span a")
|
||||||
|
.joinToString { it.text() }
|
||||||
|
author = document.selectFirst("div.meta-item span.meta-title:contains(Author) + span a")
|
||||||
|
?.text()
|
||||||
|
document.selectFirst(".status")?.text()?.let {
|
||||||
|
status = it.toMangaStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
setUrlWithoutDomain(document.location())
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun imageFromElement(element: Element): String? {
|
||||||
|
return when {
|
||||||
|
element.hasAttr("data-src") -> element.attr("abs:data-src")
|
||||||
|
element.hasAttr("data-lazy-src") -> element.attr("abs:data-lazy-src")
|
||||||
|
element.hasAttr("srcset") -> element.attr("abs:srcset").getSrcSetImage()
|
||||||
|
element.hasAttr("data-cfsrc") -> element.attr("abs:data-cfsrc")
|
||||||
|
else -> element.attr("abs:src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun String.getSrcSetImage(): String? {
|
||||||
|
return this.split(" ")
|
||||||
|
.filter(URL_REGEX::matches)
|
||||||
|
.maxOfOrNull(String::toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val completedStatusList: Array<String> = arrayOf(
|
||||||
|
"Finished",
|
||||||
|
"Completo",
|
||||||
|
)
|
||||||
|
|
||||||
|
protected open val ongoingStatusList: Array<String> = arrayOf(
|
||||||
|
"Publishing",
|
||||||
|
"Ativo",
|
||||||
|
)
|
||||||
|
|
||||||
|
protected val hiatusStatusList: Array<String> = arrayOf(
|
||||||
|
"on hiatus",
|
||||||
|
)
|
||||||
|
|
||||||
|
protected val canceledStatusList: Array<String> = arrayOf(
|
||||||
|
"Canceled",
|
||||||
|
"Discontinued",
|
||||||
|
)
|
||||||
|
|
||||||
|
open fun String.toMangaStatus(): Int {
|
||||||
|
return when {
|
||||||
|
containsIn(completedStatusList) -> SManga.COMPLETED
|
||||||
|
containsIn(ongoingStatusList) -> SManga.ONGOING
|
||||||
|
containsIn(hiatusStatusList) -> SManga.ON_HIATUS
|
||||||
|
containsIn(canceledStatusList) -> SManga.CANCELLED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Chapters ============================
|
||||||
|
|
||||||
|
override fun chapterListSelector() = ".chapter-list li a"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||||
|
name = element.selectFirst(".title")!!.text()
|
||||||
|
setUrlWithoutDomain(element.absUrl("href"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Pages ===============================
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
return document.select(".chapter-images .chapter-item > img").mapIndexed { index, element ->
|
||||||
|
Page(index, imageUrl = imageFromElement(element))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = ""
|
||||||
|
|
||||||
|
// ============================= Filters ==============================
|
||||||
|
|
||||||
|
private var filterList = emptyList<Pair<String, List<Tag>>>()
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList {
|
||||||
|
val filters = mutableListOf<Filter<*>>()
|
||||||
|
|
||||||
|
filters += if (filterList.isNotEmpty()) {
|
||||||
|
filterList.map { SelectionList(it.first, it.second) }
|
||||||
|
} else {
|
||||||
|
listOf(Filter.Header("Aperte 'Redefinir' para tentar mostrar os filtros"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilterList(filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun parseSelection(document: Document, selector: String): Pair<String, List<Tag>>? {
|
||||||
|
val selectorFilter = "#filter-form $selector .select-item-head .text"
|
||||||
|
return document.selectFirst(selectorFilter)?.text()?.let { displayName ->
|
||||||
|
displayName to document.select("#filter-form $selector li").map { element ->
|
||||||
|
element.selectFirst("input")!!.let { input ->
|
||||||
|
Tag(
|
||||||
|
name = element.selectFirst(".text")!!.text(),
|
||||||
|
value = input.attr("value"),
|
||||||
|
query = input.attr("name"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open val filterListSelector: List<String> = listOf(
|
||||||
|
".filter-genre",
|
||||||
|
".filter-status",
|
||||||
|
".filter-type",
|
||||||
|
".filter-year",
|
||||||
|
".filter-sort",
|
||||||
|
)
|
||||||
|
|
||||||
|
open fun filterParse(response: Response) {
|
||||||
|
val document = Jsoup.parseBodyFragment(response.peekBody(Long.MAX_VALUE).string())
|
||||||
|
filterList = filterListSelector.mapNotNull { selector -> parseSelection(document, selector) }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected data class Tag(val name: String = "", val value: String = "", val query: String = "")
|
||||||
|
|
||||||
|
private open class SelectionList(displayName: String, private val vals: List<Tag>, state: Int = 0) :
|
||||||
|
Filter.Select<String>(displayName, vals.map { it.name }.toTypedArray(), state) {
|
||||||
|
fun selected() = vals[state]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================= Utils ==============================
|
||||||
|
|
||||||
|
private fun String.containsIn(array: Array<String>): Boolean {
|
||||||
|
return this.lowercase() in array.map { it.lowercase() }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PREFIX_SEARCH = "id:"
|
||||||
|
val URL_REGEX = """^(https?://[^\s/$.?#].[^\s]*)${'$'}""".toRegex()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package eu.kanade.tachiyomi.multisrc.etoshore
|
||||||
|
|
||||||
|
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 EtoshoreUrlActivity : Activity() {
|
||||||
|
|
||||||
|
private val tag = javaClass.simpleName
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val pathSegments = intent?.data?.pathSegments
|
||||||
|
if (pathSegments != null && pathSegments.size > 1) {
|
||||||
|
val item = pathSegments[1]
|
||||||
|
val mainIntent = Intent().apply {
|
||||||
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
|
putExtra("query", "${Etoshore.PREFIX_SEARCH}$item")
|
||||||
|
putExtra("filter", packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivity(mainIntent)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Log.e(tag, e.toString())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(tag, "could not parse uri from intent $intent")
|
||||||
|
}
|
||||||
|
|
||||||
|
finish()
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
}
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 30
|
baseVersionCode = 28
|
||||||
|
@ -129,7 +129,7 @@ abstract class GroupLe(
|
|||||||
infoElement.select(".info-icon").attr("data-content").substringBeforeLast("/5</b><br/>")
|
infoElement.select(".info-icon").attr("data-content").substringBeforeLast("/5</b><br/>")
|
||||||
.substringAfterLast(": <b>").replace(",", ".").toFloat() * 2
|
.substringAfterLast(": <b>").replace(",", ".").toFloat() * 2
|
||||||
val ratingVotes =
|
val ratingVotes =
|
||||||
infoElement.select(".col-sm-6 .user-rating meta[itemprop=\"ratingCount\"]")
|
infoElement.select(".col-sm-7 .user-rating meta[itemprop=\"ratingCount\"]")
|
||||||
.attr("content")
|
.attr("content")
|
||||||
val ratingStar = when {
|
val ratingStar = when {
|
||||||
ratingValue > 9.5 -> "★★★★★"
|
ratingValue > 9.5 -> "★★★★★"
|
||||||
@ -209,16 +209,14 @@ abstract class GroupLe(
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected open fun getChapterSearchParams(document: Document): String {
|
protected open fun getChapterSearchParams(document: Document): String {
|
||||||
val scriptContent = document.selectFirst("script:containsData(user_hash)")?.data()
|
return "?mtr=true"
|
||||||
val userHash = scriptContent?.let { USER_HASH_REGEX.find(it)?.groupValues?.get(1) }
|
|
||||||
return userHash?.let { "?d=$it&mtr=true" } ?: "?mtr=true"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
|
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
|
||||||
if (document.select(".user-avatar").isEmpty() &&
|
if (document.select(".user-avatar").isEmpty() &&
|
||||||
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") || contains("RuMix") }
|
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
|
||||||
) {
|
) {
|
||||||
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
||||||
}
|
}
|
||||||
@ -311,7 +309,7 @@ abstract class GroupLe(
|
|||||||
val html = document.html()
|
val html = document.html()
|
||||||
|
|
||||||
if (document.select(".user-avatar").isEmpty() &&
|
if (document.select(".user-avatar").isEmpty() &&
|
||||||
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") || contains("RuMix") }
|
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
|
||||||
|
|
||||||
) {
|
) {
|
||||||
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
||||||
@ -324,9 +322,6 @@ abstract class GroupLe(
|
|||||||
throw Exception("Не удалось загрузить главу. Url: ${response.request.url}")
|
throw Exception("Не удалось загрузить главу. Url: ${response.request.url}")
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
if (document.selectFirst("div.alert") != null || document.selectFirst("form.purchase-form") != null) {
|
|
||||||
throw Exception("Эта глава платная. Используйте сайт, чтобы купить и прочитать ее.")
|
|
||||||
}
|
|
||||||
throw Exception("Дизайн сайта обновлен, для дальнейшей работы необходимо обновление дополнения")
|
throw Exception("Дизайн сайта обновлен, для дальнейшей работы необходимо обновление дополнения")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -441,6 +436,5 @@ abstract class GroupLe(
|
|||||||
private const val UAGENT_TITLE = "User-Agent(для некоторых стран)"
|
private const val UAGENT_TITLE = "User-Agent(для некоторых стран)"
|
||||||
private const val UAGENT_DEFAULT = "arora"
|
private const val UAGENT_DEFAULT = "arora"
|
||||||
const val PREFIX_SLUG_SEARCH = "slug:"
|
const val PREFIX_SLUG_SEARCH = "slug:"
|
||||||
private val USER_HASH_REGEX = "user_hash.+'(.+)'".toRegex()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 28
|
baseVersionCode = 27
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
api(project(":lib:i18n"))
|
||||||
|
@ -105,7 +105,7 @@ class HeanCmsChapterDto(
|
|||||||
@SerialName("chapter_name") private val name: String,
|
@SerialName("chapter_name") private val name: String,
|
||||||
@SerialName("chapter_title") private val title: String? = null,
|
@SerialName("chapter_title") private val title: String? = null,
|
||||||
@SerialName("chapter_slug") private val slug: String,
|
@SerialName("chapter_slug") private val slug: String,
|
||||||
@SerialName("created_at") private val createdAt: String? = null,
|
@SerialName("created_at") private val createdAt: String,
|
||||||
val price: Int? = null,
|
val price: Int? = null,
|
||||||
) {
|
) {
|
||||||
fun toSChapter(
|
fun toSChapter(
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 18
|
baseVersionCode = 16
|
||||||
|
@ -107,7 +107,7 @@ open class Kemono(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var mangas = mangasCache
|
var mangas = mangasCache
|
||||||
if (page == 1 || mangasCache.isEmpty()) {
|
if (page == 1) {
|
||||||
var favourites: List<KemonoFavouritesDto> = emptyList()
|
var favourites: List<KemonoFavouritesDto> = emptyList()
|
||||||
if (fav != null) {
|
if (fav != null) {
|
||||||
val favores = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute()
|
val favores = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute()
|
||||||
@ -132,7 +132,7 @@ open class Kemono(
|
|||||||
|
|
||||||
includeType && !excludeType && isFavourited &&
|
includeType && !excludeType && isFavourited &&
|
||||||
regularSearch
|
regularSearch
|
||||||
}.also { mangasCache = it }
|
}.also { mangasCache = mangas }
|
||||||
}
|
}
|
||||||
|
|
||||||
val sorted = when (sort.first) {
|
val sorted = when (sort.first) {
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 13
|
baseVersionCode = 12
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:i18n"))
|
api(project(":lib:i18n"))
|
||||||
|
@ -290,7 +290,7 @@ abstract class Keyoapp(
|
|||||||
.firstOrNull { CDN_HOST_REGEX.containsMatchIn(it.html()) }
|
.firstOrNull { CDN_HOST_REGEX.containsMatchIn(it.html()) }
|
||||||
?.let {
|
?.let {
|
||||||
val cdnHost = CDN_HOST_REGEX.find(it.html())
|
val cdnHost = CDN_HOST_REGEX.find(it.html())
|
||||||
?.groups?.get(1)?.value
|
?.groups?.get("host")?.value
|
||||||
?.replace(CDN_CLEAN_REGEX, "")
|
?.replace(CDN_CLEAN_REGEX, "")
|
||||||
"https://$cdnHost/uploads"
|
"https://$cdnHost/uploads"
|
||||||
}
|
}
|
||||||
@ -314,7 +314,7 @@ abstract class Keyoapp(
|
|||||||
|
|
||||||
protected open fun Element.getImageUrl(selector: String): String? {
|
protected open fun Element.getImageUrl(selector: String): String? {
|
||||||
return this.selectFirst(selector)?.let { element ->
|
return this.selectFirst(selector)?.let { element ->
|
||||||
IMG_REGEX.find(element.attr("style"))?.groups?.get(1)?.value
|
IMG_REGEX.find(element.attr("style"))?.groups?.get("url")?.value
|
||||||
?.toHttpUrlOrNull()?.let {
|
?.toHttpUrlOrNull()?.let {
|
||||||
it.newBuilder()
|
it.newBuilder()
|
||||||
.setQueryParameter("w", "480") // Keyoapp returns the dynamic size of the thumbnail to any size
|
.setQueryParameter("w", "480") // Keyoapp returns the dynamic size of the thumbnail to any size
|
||||||
@ -376,8 +376,8 @@ abstract class Keyoapp(
|
|||||||
companion object {
|
companion object {
|
||||||
private const val SHOW_PAID_CHAPTERS_PREF = "pref_show_paid_chap"
|
private const val SHOW_PAID_CHAPTERS_PREF = "pref_show_paid_chap"
|
||||||
private const val SHOW_PAID_CHAPTERS_DEFAULT = false
|
private const val SHOW_PAID_CHAPTERS_DEFAULT = false
|
||||||
val CDN_HOST_REGEX = """realUrl\s*=\s*`[^`]+//([^/]+)""".toRegex()
|
val CDN_HOST_REGEX = """realUrl\s*=\s*`[^`]+//(?<host>[^/]+)""".toRegex()
|
||||||
val CDN_CLEAN_REGEX = """\$\{[^}]*\}""".toRegex()
|
val CDN_CLEAN_REGEX = """\$\{[^}]*\}""".toRegex()
|
||||||
val IMG_REGEX = """url\(['"]?([^(['"\)])]+)""".toRegex()
|
val IMG_REGEX = """url\(['"]?(?<url>[^(['"\)])]+)""".toRegex()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 35
|
baseVersionCode = 34
|
||||||
|
@ -313,33 +313,37 @@ abstract class LibGroup(
|
|||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val slugUrl = response.request.url.toString().substringAfter("manga/").substringBefore("/chapters")
|
val slugUrl = response.request.url.toString().substringAfter("manga/").substringBefore("/chapters")
|
||||||
val chaptersData = response.parseAs<Data<List<Chapter>>>()
|
val chaptersData = response.parseAs<Data<List<Chapter>>>()
|
||||||
.also { if (it.data.isEmpty()) return emptyList() }
|
if (chaptersData.data.isEmpty()) {
|
||||||
|
throw Exception("Нет глав")
|
||||||
|
}
|
||||||
|
|
||||||
val sortingList = preferences.getString(SORTING_PREF, "ms_mixing")
|
val sortingList = preferences.getString(SORTING_PREF, "ms_mixing")
|
||||||
val defaultBranchId = if (sortingList == "ms_mixing" && chaptersData.data.getBranchCount() > 1) {
|
val defaultBranchId = if (chaptersData.data.getBranchCount() > 1) { // excess request if branchesCount is only alone = slow update library witch rateLimitHost(apiDomain.toHttpUrl(), 1)
|
||||||
runCatching { getDefaultBranch(slugUrl.substringBefore("-")).first().id }.getOrNull()
|
runCatching { getDefaultBranch(slugUrl.substringBefore("-")).first().id }.getOrNull()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
return chaptersData.data.flatMap { chapter ->
|
val chapters = mutableListOf<SChapter>()
|
||||||
when {
|
for (it in chaptersData.data.withIndex()) {
|
||||||
chapter.branchesCount > 1 && sortingList == "ms_mixing" -> {
|
if (it.value.branchesCount > 1) {
|
||||||
val branch = chapter.branches
|
for (currentBranch in it.value.branches.withIndex()) {
|
||||||
.firstOrNull { it.branchId == defaultBranchId }?.branchId
|
if (currentBranch.value.branchId == defaultBranchId && sortingList == "ms_mixing") { // ms_mixing with default branch from api
|
||||||
?: chapter.branches.first().branchId
|
chapters.add(it.value.toSChapter(slugUrl, defaultBranchId, isScanUser()))
|
||||||
|
} else if (defaultBranchId == null && sortingList == "ms_mixing") { // ms_mixing with first branch in chapter
|
||||||
listOf(
|
if (chapters.any { chpIt -> chpIt.chapter_number == it.value.number.toFloat() }) {
|
||||||
chapter.toSChapter(slugUrl, branch, isScanUser()),
|
chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser()))
|
||||||
)
|
}
|
||||||
}
|
} else if (sortingList == "ms_combining") { // ms_combining
|
||||||
chapter.branchesCount > 1 && sortingList == "ms_combining" -> {
|
chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser()))
|
||||||
chapter.branches.map { branch ->
|
|
||||||
chapter.toSChapter(slugUrl, branch.branchId, isScanUser())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> listOf(chapter.toSChapter(slugUrl, isScanUser = isScanUser()))
|
} else {
|
||||||
|
chapters.add(it.value.toSChapter(slugUrl, isScanUser = isScanUser()))
|
||||||
}
|
}
|
||||||
}.reversed()
|
}
|
||||||
|
|
||||||
|
return chapters.reversed()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
font_size_title=Font size
|
|
||||||
font_size_summary=Font changes will not be applied to downloaded or cached chapters. The font size will be adjusted according to the size of the dialog box.
|
|
||||||
font_size_message=Font size changed to %s
|
|
||||||
default_font_size=Default
|
|
||||||
disable_website_setting_title=Disable source settings
|
|
||||||
disable_website_setting_summary=Site fonts will be disabled and your device's fonts will be applied. This does not apply to downloaded or cached chapters.
|
|
@ -1 +0,0 @@
|
|||||||
font_size_title=Tamaño de letra
|
|
@ -1 +0,0 @@
|
|||||||
font_size_title=Taille de la police
|
|
@ -1 +0,0 @@
|
|||||||
font_size_title=Ukuran font
|
|
@ -1 +0,0 @@
|
|||||||
font_size_title=Dimensione del carattere
|
|
@ -1,6 +0,0 @@
|
|||||||
font_size_title=Tamanho da fonte
|
|
||||||
font_size_summary=As alterações de fonte não serão aplicadas aos capítulos baixados ou armazenados em cache. O tamanho da fonte será ajustado de acordo com o tamanho da caixa de diálogo.
|
|
||||||
font_size_message=Tamanho da fonte foi alterada para %s
|
|
||||||
default_font_size=Padrão
|
|
||||||
disable_website_setting_title=Desativar configurações do site
|
|
||||||
disable_website_setting_summary=As fontes do site serão desativadas e as fontes de seu dispositivo serão aplicadas. Isso não se aplica a capítulos baixados ou armazenados em cache.
|
|
@ -2,8 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 4
|
baseVersionCode = 2
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api(project(":lib:i18n"))
|
|
||||||
}
|
|
||||||
|
@ -1,18 +1,9 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
import eu.kanade.tachiyomi.lib.i18n.Intl
|
|
||||||
import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor
|
import eu.kanade.tachiyomi.multisrc.machinetranslations.interceptors.ComposedImageInterceptor
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
@ -24,26 +15,21 @@ import kotlinx.serialization.decodeFromString
|
|||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
abstract class MachineTranslations(
|
abstract class MachineTranslations(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
override val baseUrl: String,
|
override val baseUrl: String,
|
||||||
private val language: Language,
|
val language: Language,
|
||||||
) : ParsedHttpSource(), ConfigurableSource {
|
) : ParsedHttpSource() {
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
@ -51,66 +37,9 @@ abstract class MachineTranslations(
|
|||||||
|
|
||||||
override val lang = language.lang
|
override val lang = language.lang
|
||||||
|
|
||||||
protected val preferences: SharedPreferences by lazy {
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
.addInterceptor(ComposedImageInterceptor(baseUrl, language))
|
||||||
}
|
.build()
|
||||||
|
|
||||||
/**
|
|
||||||
* A flag that tracks whether the settings have been changed. It is used to indicate if
|
|
||||||
* any configuration change has occurred. Once the value is accessed, it resets to `false`.
|
|
||||||
* This is useful for tracking whether a preference has been modified, and ensures that
|
|
||||||
* the change status is cleared after it has been accessed, to prevent multiple triggers.
|
|
||||||
*/
|
|
||||||
private var isSettingsChanged: Boolean = false
|
|
||||||
get() {
|
|
||||||
val current = field
|
|
||||||
field = false
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
|
|
||||||
protected var fontSize: Int
|
|
||||||
get() = preferences.getString(FONT_SIZE_PREF, DEFAULT_FONT_SIZE)!!.toInt()
|
|
||||||
set(value) = preferences.edit().putString(FONT_SIZE_PREF, value.toString()).apply()
|
|
||||||
|
|
||||||
protected var disableSourceSettings: Boolean
|
|
||||||
get() = preferences.getBoolean(DISABLE_SOURCE_SETTINGS_PREF, language.disableSourceSettings)
|
|
||||||
set(value) = preferences.edit().putBoolean(DISABLE_SOURCE_SETTINGS_PREF, value).apply()
|
|
||||||
|
|
||||||
private val intl = Intl(
|
|
||||||
language = language.lang,
|
|
||||||
baseLanguage = "en",
|
|
||||||
availableLanguages = setOf("en", "es", "fr", "id", "it", "pt-BR"),
|
|
||||||
classLoader = this::class.java.classLoader!!,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val settings get() = language.apply {
|
|
||||||
fontSize = this@MachineTranslations.fontSize
|
|
||||||
}
|
|
||||||
|
|
||||||
open val useDefaultComposedImageInterceptor: Boolean = true
|
|
||||||
|
|
||||||
override val client: OkHttpClient get() = clientInstance!!
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This ensures that the `OkHttpClient` instance is only created when required, and it is rebuilt
|
|
||||||
* when there are configuration changes to ensure that the client uses the most up-to-date settings.
|
|
||||||
*/
|
|
||||||
private var clientInstance: OkHttpClient? = null
|
|
||||||
get() {
|
|
||||||
if (field == null || isSettingsChanged) {
|
|
||||||
field = clientBuilder().build()
|
|
||||||
}
|
|
||||||
return field
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun clientBuilder() = network.cloudflareClient.newBuilder()
|
|
||||||
.connectTimeout(1, TimeUnit.MINUTES)
|
|
||||||
.readTimeout(2, TimeUnit.MINUTES)
|
|
||||||
.addInterceptorIf(useDefaultComposedImageInterceptor, ComposedImageInterceptor(baseUrl, settings))
|
|
||||||
|
|
||||||
private fun OkHttpClient.Builder.addInterceptorIf(condition: Boolean, interceptor: Interceptor): OkHttpClient.Builder {
|
|
||||||
return this.takeIf { condition.not() } ?: this.addInterceptor(interceptor)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
@ -274,76 +203,9 @@ abstract class MachineTranslations(
|
|||||||
return FilterList(filters)
|
return FilterList(filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
// Some libreoffice font sizes
|
|
||||||
val sizes = arrayOf(
|
|
||||||
"24", "26", "28",
|
|
||||||
"32", "36", "40",
|
|
||||||
"42", "44", "48",
|
|
||||||
"54", "60", "72",
|
|
||||||
"80", "88", "96",
|
|
||||||
)
|
|
||||||
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = FONT_SIZE_PREF
|
|
||||||
title = intl["font_size_title"]
|
|
||||||
entries = sizes.map {
|
|
||||||
"${it}pt" + if (it == DEFAULT_FONT_SIZE) " - ${intl["default_font_size"]}" else ""
|
|
||||||
}.toTypedArray()
|
|
||||||
entryValues = sizes
|
|
||||||
summary = intl["font_size_summary"]
|
|
||||||
|
|
||||||
setOnPreferenceChange { _, newValue ->
|
|
||||||
val selected = newValue as String
|
|
||||||
val index = this.findIndexOfValue(selected)
|
|
||||||
val entry = entries[index] as String
|
|
||||||
|
|
||||||
fontSize = selected.toInt()
|
|
||||||
|
|
||||||
Toast.makeText(
|
|
||||||
screen.context,
|
|
||||||
intl["font_size_message"].format(entry),
|
|
||||||
Toast.LENGTH_LONG,
|
|
||||||
).show()
|
|
||||||
|
|
||||||
true // It's necessary to update the user interface
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
if (language.disableSourceSettings.not()) {
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = DISABLE_SOURCE_SETTINGS_PREF
|
|
||||||
title = "⚠ ${intl["disable_website_setting_title"]}"
|
|
||||||
summary = intl["disable_website_setting_summary"]
|
|
||||||
setDefaultValue(false)
|
|
||||||
setOnPreferenceChange { _, newValue ->
|
|
||||||
disableSourceSettings = newValue as Boolean
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets an `OnPreferenceChangeListener` for the preference, and before triggering the original listener,
|
|
||||||
* marks that the configuration has changed by setting `isSettingsChanged` to `true`.
|
|
||||||
* This behavior is useful for applying runtime configurations in the HTTP client,
|
|
||||||
* ensuring that the preference change is registered before invoking the original listener.
|
|
||||||
*/
|
|
||||||
protected fun Preference.setOnPreferenceChange(onPreferenceChangeListener: Preference.OnPreferenceChangeListener) {
|
|
||||||
setOnPreferenceChangeListener { preference, newValue ->
|
|
||||||
isSettingsChanged = true
|
|
||||||
onPreferenceChangeListener.onPreferenceChange(preference, newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val PAGE_REGEX = Regex(".*?\\.(webp|png|jpg|jpeg)#\\[.*?]", RegexOption.IGNORE_CASE)
|
val PAGE_REGEX = Regex(".*?\\.(webp|png|jpg|jpeg)#\\[.*?]", RegexOption.IGNORE_CASE)
|
||||||
const val PREFIX_SEARCH = "id:"
|
const val PREFIX_SEARCH = "id:"
|
||||||
private const val FONT_SIZE_PREF = "fontSizePref"
|
|
||||||
private const val DISABLE_SOURCE_SETTINGS_PREF = "disableSourceSettingsPref"
|
|
||||||
private const val DEFAULT_FONT_SIZE = "24"
|
|
||||||
|
|
||||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.US)
|
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.US)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,18 +2,4 @@ package eu.kanade.tachiyomi.multisrc.machinetranslations
|
|||||||
|
|
||||||
class MachineTranslationsFactoryUtils
|
class MachineTranslationsFactoryUtils
|
||||||
|
|
||||||
interface Language {
|
data class Language(val lang: String, val target: String = lang, val origin: String = "en")
|
||||||
val lang: String
|
|
||||||
val target: String
|
|
||||||
val origin: String
|
|
||||||
var fontSize: Int
|
|
||||||
var disableSourceSettings: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
data class LanguageImpl(
|
|
||||||
override val lang: String,
|
|
||||||
override val target: String = lang,
|
|
||||||
override val origin: String = "en",
|
|
||||||
override var fontSize: Int = 24,
|
|
||||||
override var disableSourceSettings: Boolean = false,
|
|
||||||
) : Language
|
|
||||||
|
@ -26,13 +26,16 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
// The Interceptor joins the dialogues and pages of the manga.
|
// The Interceptor joins the dialogues and pages of the manga.
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
class ComposedImageInterceptor(
|
class ComposedImageInterceptor(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
var language: Language,
|
val language: Language,
|
||||||
) : Interceptor {
|
) : Interceptor {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
@ -52,7 +55,7 @@ class ComposedImageInterceptor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val dialogues = request.url.fragment?.parseAs<List<Dialog>>()
|
val dialogues = request.url.fragment?.parseAs<List<Dialog>>()
|
||||||
?: emptyList()
|
?: throw IOException("Dialogues not found")
|
||||||
|
|
||||||
val imageRequest = request.newBuilder()
|
val imageRequest = request.newBuilder()
|
||||||
.url(url)
|
.url(url)
|
||||||
@ -60,9 +63,7 @@ class ComposedImageInterceptor(
|
|||||||
|
|
||||||
// Load the fonts before opening the connection to load the image,
|
// Load the fonts before opening the connection to load the image,
|
||||||
// so there aren't two open connections inside the interceptor.
|
// so there aren't two open connections inside the interceptor.
|
||||||
if (language.disableSourceSettings.not()) {
|
loadAllFont(chain)
|
||||||
loadAllFont(chain)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = chain.proceed(imageRequest)
|
val response = chain.proceed(imageRequest)
|
||||||
|
|
||||||
@ -77,9 +78,9 @@ class ComposedImageInterceptor(
|
|||||||
|
|
||||||
dialogues.forEach { dialog ->
|
dialogues.forEach { dialog ->
|
||||||
val textPaint = createTextPaint(selectFontFamily(dialog.type))
|
val textPaint = createTextPaint(selectFontFamily(dialog.type))
|
||||||
val dialogBox = createDialogBox(dialog, textPaint)
|
val dialogBox = createDialogBox(dialog, textPaint, bitmap)
|
||||||
val y = getYAxis(textPaint, dialog, dialogBox)
|
val y = getYAxis(textPaint, dialog, dialogBox)
|
||||||
canvas.draw(textPaint, dialogBox, dialog, dialog.x1, y)
|
canvas.draw(dialogBox, dialog, dialog.x1, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
val output = ByteArrayOutputStream()
|
val output = ByteArrayOutputStream()
|
||||||
@ -103,7 +104,7 @@ class ComposedImageInterceptor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createTextPaint(font: Typeface?): TextPaint {
|
private fun createTextPaint(font: Typeface?): TextPaint {
|
||||||
val defaultTextSize = language.fontSize.pt
|
val defaultTextSize = 24.pt // arbitrary
|
||||||
return TextPaint().apply {
|
return TextPaint().apply {
|
||||||
color = Color.BLACK
|
color = Color.BLACK
|
||||||
textSize = defaultTextSize
|
textSize = defaultTextSize
|
||||||
@ -115,10 +116,6 @@ class ComposedImageInterceptor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun selectFontFamily(type: String): Typeface? {
|
private fun selectFontFamily(type: String): Typeface? {
|
||||||
if (language.disableSourceSettings) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type in fontFamily) {
|
if (type in fontFamily) {
|
||||||
return fontFamily[type]?.second
|
return fontFamily[type]?.second
|
||||||
}
|
}
|
||||||
@ -209,7 +206,7 @@ class ComposedImageInterceptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDialogBox(dialog: Dialog, textPaint: TextPaint): StaticLayout {
|
private fun createDialogBox(dialog: Dialog, textPaint: TextPaint, bitmap: Bitmap): StaticLayout {
|
||||||
var dialogBox = createBoxLayout(dialog, textPaint)
|
var dialogBox = createBoxLayout(dialog, textPaint)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -220,8 +217,18 @@ class ComposedImageInterceptor(
|
|||||||
dialogBox = createBoxLayout(dialog, textPaint)
|
dialogBox = createBoxLayout(dialog, textPaint)
|
||||||
}
|
}
|
||||||
|
|
||||||
textPaint.color = Color.BLACK
|
// Use source setup
|
||||||
textPaint.bgColor = Color.WHITE
|
if (dialog.isNewApi) {
|
||||||
|
textPaint.color = dialog.foregroundColor
|
||||||
|
textPaint.bgColor = dialog.backgroundColor
|
||||||
|
textPaint.style = if (dialog.isBold) Paint.Style.FILL_AND_STROKE else Paint.Style.FILL
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces font color correction if the background color of the dialog box and the font color are too similar.
|
||||||
|
* It's a source configuration problem.
|
||||||
|
*/
|
||||||
|
textPaint.adjustTextColor(dialog, bitmap)
|
||||||
|
|
||||||
return dialogBox
|
return dialogBox
|
||||||
}
|
}
|
||||||
@ -234,46 +241,59 @@ class ComposedImageInterceptor(
|
|||||||
setIncludePad(false)
|
setIncludePad(false)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED)
|
setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED)
|
||||||
setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
|
|
||||||
}
|
}
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invert color in black dialog box.
|
||||||
|
private fun TextPaint.adjustTextColor(dialog: Dialog, bitmap: Bitmap) {
|
||||||
|
val pixelColor = bitmap.getPixel(dialog.centerX.toInt(), dialog.centerY.toInt())
|
||||||
|
val inverseColor = (Color.WHITE - pixelColor) or Color.BLACK
|
||||||
|
|
||||||
|
val minDistance = 80f // arbitrary
|
||||||
|
if (colorDistance(pixelColor, dialog.foregroundColor) > minDistance) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
color = inverseColor
|
||||||
|
}
|
||||||
|
|
||||||
private inline fun <reified T> String.parseAs(): T {
|
private inline fun <reified T> String.parseAs(): T {
|
||||||
return json.decodeFromString(this)
|
return json.decodeFromString(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Canvas.draw(textPaint: TextPaint, layout: StaticLayout, dialog: Dialog, x: Float, y: Float) {
|
private fun Canvas.draw(layout: StaticLayout, dialog: Dialog, x: Float, y: Float) {
|
||||||
save()
|
save()
|
||||||
translate(x, y)
|
translate(x, y)
|
||||||
rotate(dialog.angle)
|
rotate(dialog.angle)
|
||||||
drawTextOutline(textPaint, layout)
|
layout.draw(this)
|
||||||
drawText(textPaint, layout)
|
|
||||||
restore()
|
restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Canvas.drawText(textPaint: TextPaint, layout: StaticLayout) {
|
|
||||||
textPaint.style = Paint.Style.FILL
|
|
||||||
layout.draw(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Canvas.drawTextOutline(textPaint: TextPaint, layout: StaticLayout) {
|
|
||||||
val foregroundColor = textPaint.color
|
|
||||||
val style = textPaint.style
|
|
||||||
|
|
||||||
textPaint.strokeWidth = 5F
|
|
||||||
textPaint.color = textPaint.bgColor
|
|
||||||
textPaint.style = Paint.Style.FILL_AND_STROKE
|
|
||||||
|
|
||||||
layout.draw(this)
|
|
||||||
|
|
||||||
textPaint.color = foregroundColor
|
|
||||||
textPaint.style = style
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://pixelsconverter.com/pt-to-px
|
// https://pixelsconverter.com/pt-to-px
|
||||||
private val Int.pt: Float get() = this / SCALED_DENSITY
|
private val Int.pt: Float get() = this / SCALED_DENSITY
|
||||||
|
|
||||||
|
// ============================= Utils ======================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the Euclidean distance between two colors in RGB space.
|
||||||
|
*
|
||||||
|
* This function takes two integer values representing hexadecimal colors,
|
||||||
|
* converts them to their RGB components, and calculates the Euclidean distance
|
||||||
|
* between the two colors. The distance provides a measure of how similar or
|
||||||
|
* different the two colors are.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private fun colorDistance(colorA: Int, colorB: Int): Double {
|
||||||
|
val a = Color.valueOf(colorA)
|
||||||
|
val b = Color.valueOf(colorB)
|
||||||
|
|
||||||
|
return sqrt(
|
||||||
|
(b.red() - a.red()).toDouble().pow(2) +
|
||||||
|
(b.green() - a.green()).toDouble().pow(2) +
|
||||||
|
(b.blue() - a.blue()).toDouble().pow(2),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// w3: Absolute Lengths [...](https://www.w3.org/TR/css3-values/#absolute-lengths)
|
// w3: Absolute Lengths [...](https://www.w3.org/TR/css3-values/#absolute-lengths)
|
||||||
const val SCALED_DENSITY = 0.75f // 1px = 0.75pt
|
const val SCALED_DENSITY = 0.75f // 1px = 0.75pt
|
||||||
|
@ -2,7 +2,7 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 40
|
baseVersionCode = 37
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(project(":lib:cryptoaes"))
|
api(project(":lib:cryptoaes"))
|
||||||
|
@ -82,12 +82,7 @@ abstract class Madara(
|
|||||||
/**
|
/**
|
||||||
* Automatically fetched genres from the source to be used in the filters.
|
* Automatically fetched genres from the source to be used in the filters.
|
||||||
*/
|
*/
|
||||||
protected open var genresList: List<Genre> = emptyList()
|
private var genresList: List<Genre> = emptyList()
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether genres have been fetched
|
|
||||||
*/
|
|
||||||
private var genresFetched: Boolean = false
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inner variable to control how much tries the genres request was called.
|
* Inner variable to control how much tries the genres request was called.
|
||||||
@ -242,14 +237,11 @@ abstract class Madara(
|
|||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
if (query.startsWith(URL_SEARCH_PREFIX)) {
|
if (query.startsWith(URL_SEARCH_PREFIX)) {
|
||||||
val mangaUrl = baseUrl.toHttpUrl().newBuilder().apply {
|
val mangaUrl = "/$mangaSubString/${query.substringAfter(URL_SEARCH_PREFIX)}/"
|
||||||
addPathSegment(mangaSubString)
|
return client.newCall(GET("$baseUrl$mangaUrl", headers))
|
||||||
addPathSegment(query.substringAfter(URL_SEARCH_PREFIX))
|
|
||||||
}.build()
|
|
||||||
return client.newCall(GET(mangaUrl, headers))
|
|
||||||
.asObservableSuccess().map { response ->
|
.asObservableSuccess().map { response ->
|
||||||
val manga = mangaDetailsParse(response).apply {
|
val manga = mangaDetailsParse(response).apply {
|
||||||
setUrlWithoutDomain(mangaUrl.toString())
|
url = mangaUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
MangasPage(listOf(manga), false)
|
MangasPage(listOf(manga), false)
|
||||||
@ -586,13 +578,11 @@ abstract class Madara(
|
|||||||
|
|
||||||
override fun searchMangaSelector() = "div.c-tabs-item__content"
|
override fun searchMangaSelector() = "div.c-tabs-item__content"
|
||||||
|
|
||||||
protected open val searchMangaUrlSelector = "div.post-title a"
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga {
|
override fun searchMangaFromElement(element: Element): SManga {
|
||||||
val manga = SManga.create()
|
val manga = SManga.create()
|
||||||
|
|
||||||
with(element) {
|
with(element) {
|
||||||
selectFirst(searchMangaUrlSelector)!!.let {
|
selectFirst("div.post-title a")!!.let {
|
||||||
manga.setUrlWithoutDomain(it.attr("abs:href"))
|
manga.setUrlWithoutDomain(it.attr("abs:href"))
|
||||||
manga.title = it.ownText()
|
manga.title = it.ownText()
|
||||||
}
|
}
|
||||||
@ -633,7 +623,7 @@ abstract class Madara(
|
|||||||
"Em Andamento", "En cours", "En Cours", "En cours de publication", "Ativo", "Lançando", "Đang Tiến Hành", "Devam Ediyor",
|
"Em Andamento", "En cours", "En Cours", "En cours de publication", "Ativo", "Lançando", "Đang Tiến Hành", "Devam Ediyor",
|
||||||
"Devam ediyor", "In Corso", "In Arrivo", "مستمرة", "مستمر", "En Curso", "En curso", "Emision",
|
"Devam ediyor", "In Corso", "In Arrivo", "مستمرة", "مستمر", "En Curso", "En curso", "Emision",
|
||||||
"Curso", "En marcha", "Publicandose", "Publicándose", "En emision", "连载中", "Em Lançamento", "Devam Ediyo",
|
"Curso", "En marcha", "Publicandose", "Publicándose", "En emision", "连载中", "Em Lançamento", "Devam Ediyo",
|
||||||
"Đang làm", "Em postagem", "Devam Eden", "Em progresso", "Em curso", "Atualizações Semanais",
|
"Đang làm", "Em postagem", "Devam Eden", "Em progresso", "Em curso",
|
||||||
)
|
)
|
||||||
|
|
||||||
protected val hiatusStatusList: Array<String> = arrayOf(
|
protected val hiatusStatusList: Array<String> = arrayOf(
|
||||||
@ -688,7 +678,7 @@ abstract class Madara(
|
|||||||
manga.thumbnail_url = imageFromElement(it)
|
manga.thumbnail_url = imageFromElement(it)
|
||||||
}
|
}
|
||||||
select(mangaDetailsSelectorStatus).last()?.let {
|
select(mangaDetailsSelectorStatus).last()?.let {
|
||||||
manga.status = with(it.text().filter { ch -> ch.isLetterOrDigit() || ch.isWhitespace() }.trim()) {
|
manga.status = with(it.text()) {
|
||||||
when {
|
when {
|
||||||
containsIn(completedStatusList) -> SManga.COMPLETED
|
containsIn(completedStatusList) -> SManga.COMPLETED
|
||||||
containsIn(ongoingStatusList) -> SManga.ONGOING
|
containsIn(ongoingStatusList) -> SManga.ONGOING
|
||||||
@ -752,7 +742,7 @@ abstract class Madara(
|
|||||||
|
|
||||||
// Manga Details Selector
|
// Manga Details Selector
|
||||||
open val mangaDetailsSelectorTitle = "div.post-title h3, div.post-title h1, #manga-title > h1"
|
open val mangaDetailsSelectorTitle = "div.post-title h3, div.post-title h1, #manga-title > h1"
|
||||||
open val mangaDetailsSelectorAuthor = "div.author-content > a, div.manga-authors > a"
|
open val mangaDetailsSelectorAuthor = "div.author-content > a"
|
||||||
open val mangaDetailsSelectorArtist = "div.artist-content > a"
|
open val mangaDetailsSelectorArtist = "div.artist-content > a"
|
||||||
open val mangaDetailsSelectorStatus = "div.summary-content"
|
open val mangaDetailsSelectorStatus = "div.summary-content"
|
||||||
open val mangaDetailsSelectorDescription = "div.description-summary div.summary__content, div.summary_content div.post-content_item > h5 + div, div.summary_content div.manga-excerpt"
|
open val mangaDetailsSelectorDescription = "div.description-summary div.summary__content, div.summary_content div.post-content_item > h5 + div, div.summary_content div.manga-excerpt"
|
||||||
@ -786,7 +776,7 @@ abstract class Madara(
|
|||||||
/**
|
/**
|
||||||
* Get the best image quality available from srcset
|
* Get the best image quality available from srcset
|
||||||
*/
|
*/
|
||||||
protected fun String.getSrcSetImage(): String? {
|
private fun String.getSrcSetImage(): String? {
|
||||||
return this.split(" ")
|
return this.split(" ")
|
||||||
.filter(URL_REGEX::matches)
|
.filter(URL_REGEX::matches)
|
||||||
.maxOfOrNull(String::toString)
|
.maxOfOrNull(String::toString)
|
||||||
@ -930,10 +920,6 @@ abstract class Madara(
|
|||||||
WordSet("hace").startsWith(date) -> {
|
WordSet("hace").startsWith(date) -> {
|
||||||
parseRelativeDate(date)
|
parseRelativeDate(date)
|
||||||
}
|
}
|
||||||
// Handle "jour" with a number before it
|
|
||||||
date.contains(Regex("""\b\d+ jour""")) -> {
|
|
||||||
parseRelativeDate(date)
|
|
||||||
}
|
|
||||||
date.contains(Regex("""\d(st|nd|rd|th)""")) -> {
|
date.contains(Regex("""\d(st|nd|rd|th)""")) -> {
|
||||||
// Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it
|
// Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it
|
||||||
date.split(" ").map {
|
date.split(" ").map {
|
||||||
@ -1077,17 +1063,10 @@ abstract class Madara(
|
|||||||
* Fetch the genres from the source to be used in the filters.
|
* Fetch the genres from the source to be used in the filters.
|
||||||
*/
|
*/
|
||||||
protected fun fetchGenres() {
|
protected fun fetchGenres() {
|
||||||
if (fetchGenres && fetchGenresAttempts < 3 && !genresFetched) {
|
if (fetchGenres && fetchGenresAttempts < 3 && genresList.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
client.newCall(genresRequest()).execute()
|
genresList = client.newCall(genresRequest()).execute()
|
||||||
.use { parseGenres(it.asJsoup()) }
|
.use { parseGenres(it.asJsoup()) }
|
||||||
.also {
|
|
||||||
genresFetched = true
|
|
||||||
}
|
|
||||||
.takeIf { it.isNotEmpty() }
|
|
||||||
?.also {
|
|
||||||
genresList = it
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
} finally {
|
} finally {
|
||||||
fetchGenresAttempts++
|
fetchGenresAttempts++
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 17
|
baseVersionCode = 14
|
||||||
|
@ -11,8 +11,11 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@ -22,6 +25,7 @@ import org.jsoup.Jsoup
|
|||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
@ -51,6 +55,8 @@ abstract class MadTheme(
|
|||||||
add("Referer", "$baseUrl/")
|
add("Referer", "$baseUrl/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private var genreKey = "genre[]"
|
private var genreKey = "genre[]"
|
||||||
|
|
||||||
// Popular
|
// Popular
|
||||||
@ -171,53 +177,19 @@ abstract class MadTheme(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
if (response.request.url.fragment == "idFound") {
|
if (response.code in 200..299) {
|
||||||
return super.chapterListParse(response)
|
return super.chapterListParse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
val document = response.asJsoup()
|
// Try to show message/error from site
|
||||||
|
response.body.let { body ->
|
||||||
// Need the total chapters to check against the request
|
json.decodeFromString<JsonObject>(body.string())["message"]
|
||||||
val totalChapters = document.selectFirst(".title span:containsOwn(CHAPTERS \\()")?.text()
|
?.jsonPrimitive
|
||||||
?.substringAfter("(")
|
?.content
|
||||||
?.substringBefore(")")
|
?.let { throw Exception(it) }
|
||||||
?.toIntOrNull()
|
|
||||||
|
|
||||||
val script = document.selectFirst("script:containsData(bookId)")
|
|
||||||
?: throw Exception("Cannot find script")
|
|
||||||
val bookId = script.data().substringAfter("bookId = ").substringBefore(";")
|
|
||||||
val bookSlug = script.data().substringAfter("bookSlug = \"").substringBefore("\";")
|
|
||||||
|
|
||||||
// Use slug search by default
|
|
||||||
val slugRequest = chapterClient.newCall(GET(buildChapterUrl(bookSlug), headers)).execute()
|
|
||||||
if (!slugRequest.isSuccessful) {
|
|
||||||
throw Exception("HTTP error ${slugRequest.code}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var finalDocument = slugRequest.asJsoup().select(chapterListSelector())
|
throw Exception("HTTP error ${response.code}")
|
||||||
|
|
||||||
if (totalChapters != null && finalDocument.size < totalChapters) {
|
|
||||||
val idRequest = chapterClient.newCall(GET(buildChapterUrl(bookId), headers)).execute()
|
|
||||||
finalDocument = idRequest.asJsoup().select(chapterListSelector())
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalDocument.map {
|
|
||||||
SChapter.create().apply {
|
|
||||||
url = it.selectFirst("a")!!.absUrl("href").removePrefix(baseUrl)
|
|
||||||
name = it.selectFirst(".chapter-title")!!.text()
|
|
||||||
date_upload = parseChapterDate(it.selectFirst(".chapter-update")?.text())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildChapterUrl(fetchByParam: String): HttpUrl {
|
|
||||||
return baseUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
addPathSegment("api")
|
|
||||||
addPathSegment("manga")
|
|
||||||
addPathSegment(fetchByParam)
|
|
||||||
addPathSegment("chapters")
|
|
||||||
addQueryParameter("source", "detail")
|
|
||||||
}.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request =
|
override fun chapterListRequest(manga: SManga): Request =
|
||||||
@ -225,11 +197,10 @@ abstract class MadTheme(
|
|||||||
val url = "$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
|
val url = "$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
|
||||||
.addQueryParameter("manga_id", mangaId)
|
.addQueryParameter("manga_id", mangaId)
|
||||||
.addQueryParameter("manga_name", manga.title)
|
.addQueryParameter("manga_name", manga.title)
|
||||||
.fragment("idFound")
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
GET(url, headers)
|
GET(url, headers)
|
||||||
} ?: GET("$baseUrl${manga.url}", headers)
|
} ?: GET("$baseUrl/api/manga${manga.url}/chapters?source=detail", headers)
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
if (genresList == null) {
|
if (genresList == null) {
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 14
|
baseVersionCode = 13
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 3
|
baseVersionCode = 2
|
||||||
|
@ -1,356 +1,129 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.mangareader
|
package eu.kanade.tachiyomi.multisrc.mangareader
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import android.app.Application
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.nodes.TextNode
|
import org.jsoup.select.Evaluator
|
||||||
import uy.kohesive.injekt.injectLazy
|
import rx.Observable
|
||||||
import java.net.URLEncoder
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
abstract class MangaReader(
|
abstract class MangaReader : HttpSource(), ConfigurableSource {
|
||||||
override val name: String,
|
|
||||||
override val baseUrl: String,
|
|
||||||
final override val lang: String,
|
|
||||||
) : HttpSource() {
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
final override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
open fun addPage(page: Int, builder: HttpUrl.Builder) {
|
|
||||||
builder.addQueryParameter("page", page.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
|
||||||
|
|
||||||
protected open val sortPopularValue = "most-viewed"
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
return searchMangaRequest(
|
|
||||||
page,
|
|
||||||
"",
|
|
||||||
FilterList(SortFilter(sortFilterName, sortFilterParam, sortFilterValues(), sortPopularValue)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
final override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
final override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
final override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
|
||||||
protected open val sortLatestValue = "latest-updated"
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
return searchMangaRequest(
|
|
||||||
page,
|
|
||||||
"",
|
|
||||||
FilterList(SortFilter(sortFilterName, sortFilterParam, sortFilterValues(), sortLatestValue)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
final override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
|
||||||
|
|
||||||
// =============================== Search ===============================
|
|
||||||
|
|
||||||
protected open val searchPathSegment = "search"
|
|
||||||
protected open val searchKeyword = "keyword"
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
if (query.isNotBlank()) {
|
|
||||||
addPathSegment(searchPathSegment)
|
|
||||||
addQueryParameter(searchKeyword, query)
|
|
||||||
} else {
|
|
||||||
addPathSegment("filter")
|
|
||||||
val filterList = filters.ifEmpty { getFilterList() }
|
|
||||||
filterList.filterIsInstance<UriFilter>().forEach {
|
|
||||||
it.addToUri(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addPage(page, this)
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun searchMangaSelector(): String = ".manga_list-sbs .manga-poster"
|
|
||||||
|
|
||||||
open fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
|
||||||
element.selectFirst("img")!!.let {
|
|
||||||
title = it.attr("alt")
|
|
||||||
thumbnail_url = it.imgAttr()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun searchMangaNextPageSelector(): String = "ul.pagination > li.active + li"
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
val entries = document.select(searchMangaSelector())
|
var entries = document.select(searchMangaSelector()).map(::searchMangaFromElement)
|
||||||
.map(::searchMangaFromElement)
|
if (preferences.getBoolean(SHOW_VOLUME_PREF, false)) {
|
||||||
|
entries = entries.flatMapTo(ArrayList(entries.size * 2)) { manga ->
|
||||||
|
val volume = SManga.create().apply {
|
||||||
|
url = manga.url + VOLUME_URL_SUFFIX
|
||||||
|
title = VOLUME_TITLE_PREFIX + manga.title
|
||||||
|
thumbnail_url = manga.thumbnail_url
|
||||||
|
}
|
||||||
|
listOf(manga, volume)
|
||||||
|
}
|
||||||
|
}
|
||||||
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
|
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
|
||||||
return MangasPage(entries, hasNextPage)
|
return MangasPage(entries, hasNextPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================== Manga Details ============================
|
final override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX)
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
|
abstract fun searchMangaSelector(): String
|
||||||
|
|
||||||
private val authorText: String = when (lang) {
|
abstract fun searchMangaNextPageSelector(): String
|
||||||
"ja" -> "著者"
|
|
||||||
else -> "Authors"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val statusText: String = when (lang) {
|
abstract fun searchMangaFromElement(element: Element): SManga
|
||||||
"ja" -> "地位"
|
|
||||||
else -> "Status"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
abstract fun mangaDetailsParse(document: Document): SManga
|
||||||
|
|
||||||
|
final override fun mangaDetailsParse(response: Response): SManga {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
val manga = mangaDetailsParse(document)
|
||||||
return SManga.create().apply {
|
if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
|
||||||
document.selectFirst("#ani_detail")!!.run {
|
manga.title = VOLUME_TITLE_PREFIX + manga.title
|
||||||
title = selectFirst(".manga-name")!!.ownText()
|
|
||||||
thumbnail_url = selectFirst("img")?.imgAttr()
|
|
||||||
genre = select(".genres > a").joinToString { it.ownText() }
|
|
||||||
|
|
||||||
description = buildString {
|
|
||||||
selectFirst(".description")?.ownText()?.let { append(it) }
|
|
||||||
append("\n\n")
|
|
||||||
selectFirst(".manga-name-or")?.ownText()?.let {
|
|
||||||
if (it.isNotEmpty() && it != title) {
|
|
||||||
append("Alternative Title: ")
|
|
||||||
append(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.trim()
|
|
||||||
|
|
||||||
select(".anisc-info > .item").forEach { info ->
|
|
||||||
when (info.selectFirst(".item-head")?.ownText()) {
|
|
||||||
"$authorText:" -> info.parseAuthorsTo(this@apply)
|
|
||||||
"$statusText:" -> info.parseStatus(this@apply)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun Element.parseAuthorsTo(manga: SManga): SManga {
|
|
||||||
val authors = select("a")
|
|
||||||
val text = authors.map { it.ownText().replace(",", "") }
|
|
||||||
|
|
||||||
val count = authors.size
|
|
||||||
when (count) {
|
|
||||||
0 -> return manga
|
|
||||||
1 -> {
|
|
||||||
manga.author = text.first()
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val authorList = ArrayList<String>(count)
|
|
||||||
val artistList = ArrayList<String>(count)
|
|
||||||
for ((index, author) in authors.withIndex()) {
|
|
||||||
val textNode = author.nextSibling() as? TextNode
|
|
||||||
val list = if (textNode?.wholeText?.contains("(Art)") == true) artistList else authorList
|
|
||||||
list.add(text[index])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authorList.isNotEmpty()) manga.author = authorList.joinToString()
|
|
||||||
if (artistList.isNotEmpty()) manga.artist = artistList.joinToString()
|
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Element.parseStatus(manga: SManga): SManga {
|
abstract val chapterType: String
|
||||||
manga.status = this.selectFirst(".name")?.text().getStatus()
|
abstract val volumeType: String
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun String?.getStatus(): Int = when (this?.lowercase()) {
|
abstract fun chapterListRequest(mangaUrl: String, type: String): Request
|
||||||
"ongoing", "publishing", "releasing" -> SManga.ONGOING
|
|
||||||
"completed", "finished" -> SManga.COMPLETED
|
|
||||||
"on-hold", "on_hiatus" -> SManga.ON_HIATUS
|
|
||||||
"canceled", "discontinued" -> SManga.CANCELLED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================== Chapters ==============================
|
abstract fun parseChapterElements(response: Response, isVolume: Boolean): List<Element>
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter): String {
|
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
|
||||||
return baseUrl + chapter.url.substringBeforeLast('#')
|
|
||||||
}
|
|
||||||
|
|
||||||
open val chapterIdSelect = "en-chapters"
|
open fun updateChapterList(manga: SManga, chapters: List<SChapter>) = Unit
|
||||||
|
|
||||||
open fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
|
||||||
element.selectFirst("a")!!.run {
|
val path = manga.url
|
||||||
setUrlWithoutDomain(attr("href") + "#${element.attr("data-id")}")
|
val isVolume = path.endsWith(VOLUME_URL_SUFFIX)
|
||||||
name = selectFirst(".name")?.text() ?: text()
|
val type = if (isVolume) volumeType else chapterType
|
||||||
}
|
val request = chapterListRequest(path.removeSuffix(VOLUME_URL_SUFFIX), type)
|
||||||
}
|
val response = client.newCall(request).execute()
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
val abbrPrefix = if (isVolume) "Vol" else "Chap"
|
||||||
val document = response.asJsoup()
|
val fullPrefix = if (isVolume) "Volume" else "Chapter"
|
||||||
return document.select("#$chapterIdSelect > li.chapter-item").map(::chapterFromElement)
|
val linkSelector = Evaluator.Tag("a")
|
||||||
}
|
parseChapterElements(response, isVolume).map { element ->
|
||||||
|
SChapter.create().apply {
|
||||||
|
val number = element.attr("data-number")
|
||||||
|
chapter_number = number.toFloatOrNull() ?: -1f
|
||||||
|
|
||||||
// =============================== Pages ================================
|
val link = element.selectFirst(linkSelector)!!
|
||||||
|
name = run {
|
||||||
open fun getChapterId(chapter: SChapter): String {
|
val name = link.text()
|
||||||
val document = client.newCall(GET(baseUrl + chapter.url, headers)).execute().asJsoup()
|
val prefix = "$abbrPrefix $number: "
|
||||||
return document.selectFirst("div[data-reading-id]")
|
if (!name.startsWith(prefix)) return@run name
|
||||||
?.attr("data-reading-id")
|
val realName = name.removePrefix(prefix)
|
||||||
.orEmpty()
|
if (realName.contains(number)) realName else "$fullPrefix $number: $realName"
|
||||||
.ifEmpty {
|
|
||||||
throw Exception("Unable to retrieve chapter id")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun getAjaxUrl(id: String): String {
|
|
||||||
return "$baseUrl//ajax/image/list/$id?mode=vertical"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
val chapterId = chapter.url.substringAfterLast('#').ifEmpty {
|
|
||||||
getChapterId(chapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
val ajaxHeaders = super.headersBuilder().apply {
|
|
||||||
add("Accept", "application/json, text/javascript, */*; q=0.01")
|
|
||||||
add("Referer", URLEncoder.encode(baseUrl + chapter.url.substringBeforeLast("#"), "utf-8"))
|
|
||||||
add("X-Requested-With", "XMLHttpRequest")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(getAjaxUrl(chapterId), ajaxHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun pageListParseSelector(): String = ".container-reader-chapter > div > img"
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val document = response.parseHtmlProperty()
|
|
||||||
|
|
||||||
val pageList = document.select(pageListParseSelector()).mapIndexed { index, element ->
|
|
||||||
val imgUrl = element.imgAttr().ifEmpty {
|
|
||||||
element.selectFirst("img")!!.imgAttr()
|
|
||||||
}
|
|
||||||
|
|
||||||
Page(index, imageUrl = imgUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageList
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
|
||||||
|
|
||||||
open fun Element.imgAttr(): String = when {
|
|
||||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
|
||||||
hasAttr("data-src") -> attr("abs:data-src")
|
|
||||||
hasAttr("data-url") -> attr("abs:data-url")
|
|
||||||
else -> attr("abs:src")
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun Response.parseHtmlProperty(): Document {
|
|
||||||
val html = json.parseToJsonElement(body.string()).jsonObject["html"]!!.jsonPrimitive.content
|
|
||||||
return Jsoup.parseBodyFragment(html)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================== Filters ==============================
|
|
||||||
|
|
||||||
object Note : Filter.Header("NOTE: Ignored if using text search!")
|
|
||||||
|
|
||||||
interface UriFilter {
|
|
||||||
fun addToUri(builder: HttpUrl.Builder)
|
|
||||||
}
|
|
||||||
|
|
||||||
open class UriPartFilter(
|
|
||||||
name: String,
|
|
||||||
private val param: String,
|
|
||||||
private val vals: Array<Pair<String, String>>,
|
|
||||||
defaultValue: String? = null,
|
|
||||||
) : Filter.Select<String>(
|
|
||||||
name,
|
|
||||||
vals.map { it.first }.toTypedArray(),
|
|
||||||
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
|
|
||||||
),
|
|
||||||
UriFilter {
|
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
|
||||||
builder.addQueryParameter(param, vals[state].second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
|
|
||||||
|
|
||||||
open class UriMultiSelectFilter(
|
|
||||||
name: String,
|
|
||||||
private val param: String,
|
|
||||||
private val vals: Array<Pair<String, String>>,
|
|
||||||
private val join: String? = null,
|
|
||||||
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
|
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
|
||||||
val checked = state.filter { it.state }
|
|
||||||
if (join == null) {
|
|
||||||
checked.forEach {
|
|
||||||
builder.addQueryParameter(param, it.value)
|
|
||||||
}
|
}
|
||||||
} else {
|
setUrlWithoutDomain(link.attr("href") + '#' + type + '/' + element.attr("data-id"))
|
||||||
builder.addQueryParameter(param, checked.joinToString(join) { it.value })
|
|
||||||
}
|
}
|
||||||
}
|
}.also { if (!isVolume && it.isNotEmpty()) updateChapterList(manga, it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
open class SortFilter(
|
final override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast('#')
|
||||||
title: String,
|
|
||||||
param: String,
|
|
||||||
values: Array<Pair<String, String>>,
|
|
||||||
default: String? = null,
|
|
||||||
) : UriPartFilter(title, param, values, default)
|
|
||||||
|
|
||||||
private val sortFilterName: String = when (lang) {
|
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||||
"ja" -> "選別"
|
|
||||||
else -> "Sort"
|
val preferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open val sortFilterParam: String = "sort"
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
protected open fun sortFilterValues(): Array<Pair<String, String>> {
|
key = SHOW_VOLUME_PREF
|
||||||
return arrayOf(
|
title = "Show volume entries in search result"
|
||||||
Pair("Default", "default"),
|
setDefaultValue(false)
|
||||||
Pair("Latest Updated", sortLatestValue),
|
}.let(screen::addPreference)
|
||||||
Pair("Score", "score"),
|
|
||||||
Pair("Name A-Z", "name-az"),
|
|
||||||
Pair("Release Date", "release-date"),
|
|
||||||
Pair("Most Viewed", sortPopularValue),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getSortFilter() = SortFilter(sortFilterName, sortFilterParam, sortFilterValues())
|
companion object {
|
||||||
|
private const val SHOW_VOLUME_PREF = "show_volume"
|
||||||
|
|
||||||
override fun getFilterList(): FilterList = FilterList(
|
private const val VOLUME_URL_FRAGMENT = "vol"
|
||||||
getSortFilter(),
|
private const val VOLUME_URL_SUFFIX = "#" + VOLUME_URL_FRAGMENT
|
||||||
)
|
private const val VOLUME_TITLE_PREFIX = "[VOL] "
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 3
|
baseVersionCode = 2
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.multisrc.mangaworld
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
|
||||||
|
|
||||||
class CookieRedirectInterceptor(private val client: OkHttpClient) : Interceptor {
|
|
||||||
private val cookieRegex = Regex("""document\.cookie="(MWCookie[^"]+)""")
|
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
|
||||||
val request = chain.request()
|
|
||||||
val response = chain.proceed(request)
|
|
||||||
// ignore requests that already have completed the JS challenge
|
|
||||||
if (response.headers["vary"] != null) return response
|
|
||||||
|
|
||||||
val content = response.body.string()
|
|
||||||
val results = cookieRegex.find(content)
|
|
||||||
?: return response.newBuilder().body(content.toResponseBody(response.body.contentType())).build()
|
|
||||||
val (cookieString) = results.destructured
|
|
||||||
return chain.proceed(loadCookie(request, cookieString))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadCookie(request: Request, cookieString: String): Request {
|
|
||||||
val cookie = Cookie.parse(request.url, cookieString)!!
|
|
||||||
client.cookieJar.saveFromResponse(request.url, listOf(cookie))
|
|
||||||
val headers = request.headers.newBuilder()
|
|
||||||
.add("Cookie", cookie.toString())
|
|
||||||
.build()
|
|
||||||
return GET(request.url, headers)
|
|
||||||
}
|
|
||||||
}
|
|
@ -28,11 +28,7 @@ abstract class MangaWorld(
|
|||||||
) : ParsedHttpSource() {
|
) : ParsedHttpSource() {
|
||||||
|
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
override val client: OkHttpClient = network.cloudflareClient
|
||||||
// CookieRedirectInterceptor extracts MWCookie from the page's JS code, applies it and then redirects to the page
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
|
||||||
.addInterceptor(CookieRedirectInterceptor(network.cloudflareClient))
|
|
||||||
.build()
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
protected val CHAPTER_NUMBER_REGEX by lazy { Regex("""(?i)capitolo\s([0-9]+)""") }
|
protected val CHAPTER_NUMBER_REGEX by lazy { Regex("""(?i)capitolo\s([0-9]+)""") }
|
||||||
|
@ -2,4 +2,4 @@ plugins {
|
|||||||
id("lib-multisrc")
|
id("lib-multisrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseVersionCode = 4
|
baseVersionCode = 3
|
||||||
|
@ -267,7 +267,7 @@ abstract class SlimeReadTheme(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val PREFIX_SEARCH = "id:"
|
const val PREFIX_SEARCH = "id:"
|
||||||
val FUNCTION_REGEX = """(\[""\.concat\("[^,]+,"\."\)\.concat\(([^,]+),":\d+"\)\])""".toRegex(RegexOption.DOT_MATCHES_ALL)
|
val FUNCTION_REGEX = """(?<script>\[""\.concat\("[^,]+,"\."\)\.concat\((?<infix>[^,]+),":\d+"\)\])""".toRegex(RegexOption.DOT_MATCHES_ALL)
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
include(":core")
|
include(":core")
|
||||||
include(":utils")
|
|
||||||
|
|
||||||
// Load all modules under /lib
|
// Load all modules under /lib
|
||||||
File(rootDir, "lib").eachDir { include("lib:${it.name}") }
|
File(rootDir, "lib").eachDir { include("lib:${it.name}") }
|
||||||
|
@ -14,12 +14,8 @@
|
|||||||
|
|
||||||
<data android:host="*.bato.to" />
|
<data android:host="*.bato.to" />
|
||||||
<data android:host="bato.to" />
|
<data android:host="bato.to" />
|
||||||
<data android:host="*.batocomic.com" />
|
<data android:host="*.batocc.com" />
|
||||||
<data android:host="batocomic.com" />
|
<data android:host="batocc.com" />
|
||||||
<data android:host="*.batocomic.net" />
|
|
||||||
<data android:host="batocomic.net" />
|
|
||||||
<data android:host="*.batocomic.org" />
|
|
||||||
<data android:host="batocomic.org" />
|
|
||||||
<data android:host="*.batotoo.com" />
|
<data android:host="*.batotoo.com" />
|
||||||
<data android:host="batotoo.com" />
|
<data android:host="batotoo.com" />
|
||||||
<data android:host="*.batotwo.com" />
|
<data android:host="*.batotwo.com" />
|
||||||
@ -28,40 +24,18 @@
|
|||||||
<data android:host="battwo.com" />
|
<data android:host="battwo.com" />
|
||||||
<data android:host="*.comiko.net" />
|
<data android:host="*.comiko.net" />
|
||||||
<data android:host="comiko.net" />
|
<data android:host="comiko.net" />
|
||||||
<data android:host="*.comiko.org" />
|
|
||||||
<data android:host="comiko.org" />
|
|
||||||
<data android:host="*.mangatoto.com" />
|
<data android:host="*.mangatoto.com" />
|
||||||
<data android:host="mangatoto.com" />
|
<data android:host="mangatoto.com" />
|
||||||
<data android:host="*.mangatoto.net" />
|
<data android:host="*.mangatoto.net" />
|
||||||
<data android:host="mangatoto.net" />
|
<data android:host="mangatoto.net" />
|
||||||
<data android:host="*.mangatoto.org" />
|
<data android:host="*.mangatoto.org" />
|
||||||
<data android:host="mangatoto.org" />
|
<data android:host="mangatoto.org" />
|
||||||
<data android:host="*.readtoto.com" />
|
<data android:host="*.mycordant.co.uk" />
|
||||||
<data android:host="readtoto.com" />
|
<data android:host="mycordant.co.uk" />
|
||||||
<data android:host="*.readtoto.net" />
|
|
||||||
<data android:host="readtoto.net" />
|
|
||||||
<data android:host="*.readtoto.org" />
|
|
||||||
<data android:host="readtoto.org" />
|
|
||||||
<data android:host="*.xbato.com" />
|
|
||||||
<data android:host="xbato.com" />
|
|
||||||
<data android:host="*.xbato.net" />
|
|
||||||
<data android:host="xbato.net" />
|
|
||||||
<data android:host="*.xbato.org" />
|
|
||||||
<data android:host="xbato.org" />
|
|
||||||
<data android:host="*.zbato.com" />
|
|
||||||
<data android:host="zbato.com" />
|
|
||||||
<data android:host="*.zbato.net" />
|
|
||||||
<data android:host="zbato.net" />
|
|
||||||
<data android:host="*.zbato.org" />
|
|
||||||
<data android:host="zbato.org" />
|
|
||||||
<data android:host="*.dto.to" />
|
<data android:host="*.dto.to" />
|
||||||
<data android:host="dto.to" />
|
<data android:host="dto.to" />
|
||||||
<data android:host="*.fto.to" />
|
|
||||||
<data android:host="fto.to" />
|
|
||||||
<data android:host="*.hto.to" />
|
<data android:host="*.hto.to" />
|
||||||
<data android:host="hto.to" />
|
<data android:host="hto.to" />
|
||||||
<data android:host="*.jto.to" />
|
|
||||||
<data android:host="jto.to" />
|
|
||||||
<data android:host="*.mto.to" />
|
<data android:host="*.mto.to" />
|
||||||
<data android:host="mto.to" />
|
<data android:host="mto.to" />
|
||||||
<data android:host="*.wto.to" />
|
<data android:host="*.wto.to" />
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Bato.to'
|
extName = 'Bato.to'
|
||||||
extClass = '.BatoToFactory'
|
extClass = '.BatoToFactory'
|
||||||
extVersionCode = 48
|
extVersionCode = 46
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import android.content.SharedPreferences
|
|||||||
import androidx.preference.CheckBoxPreference
|
import androidx.preference.CheckBoxPreference
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.extension.BuildConfig
|
|
||||||
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
||||||
import eu.kanade.tachiyomi.lib.cryptoaes.Deobfuscator
|
import eu.kanade.tachiyomi.lib.cryptoaes.Deobfuscator
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
@ -101,25 +100,11 @@ open class BatoTo(
|
|||||||
if (current.isNotEmpty()) {
|
if (current.isNotEmpty()) {
|
||||||
return current
|
return current
|
||||||
}
|
}
|
||||||
field = getMirrorPref()
|
field = getMirrorPref()!!
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMirrorPref(): String {
|
private fun getMirrorPref(): String? = preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
|
||||||
return preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
|
|
||||||
?.takeUnless { it == MIRROR_PREF_DEFAULT_VALUE }
|
|
||||||
?: let {
|
|
||||||
val seed = runCatching {
|
|
||||||
val pm = Injekt.get<Application>().packageManager
|
|
||||||
pm.getPackageInfo(BuildConfig.APPLICATION_ID, 0).lastUpdateTime
|
|
||||||
}.getOrElse {
|
|
||||||
BuildConfig.VERSION_NAME.hashCode().toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
MIRROR_PREF_ENTRY_VALUES[1 + (seed % (MIRROR_PREF_ENTRIES.size - 1)).toInt()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAltChapterListPref(): Boolean = preferences.getBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE)
|
private fun getAltChapterListPref(): Boolean = preferences.getBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE)
|
||||||
private fun isRemoveTitleVersion(): Boolean {
|
private fun isRemoveTitleVersion(): Boolean {
|
||||||
return preferences.getBoolean("${REMOVE_TITLE_VERSION_PREF}_$lang", false)
|
return preferences.getBoolean("${REMOVE_TITLE_VERSION_PREF}_$lang", false)
|
||||||
@ -342,12 +327,6 @@ open class BatoTo(
|
|||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
if (manga.url.startsWith("http")) {
|
if (manga.url.startsWith("http")) {
|
||||||
// Check if trying to use a deprecated mirror, force current mirror
|
|
||||||
val httpUrl = manga.url.toHttpUrl()
|
|
||||||
if ("https://${httpUrl.host}" in DEPRECATED_MIRRORS) {
|
|
||||||
val newHttpUrl = httpUrl.newBuilder().host(getMirrorPref().toHttpUrl().host)
|
|
||||||
return GET(newHttpUrl.build(), headers)
|
|
||||||
}
|
|
||||||
return GET(manga.url, headers)
|
return GET(manga.url, headers)
|
||||||
}
|
}
|
||||||
return super.mangaDetailsRequest(manga)
|
return super.mangaDetailsRequest(manga)
|
||||||
@ -358,8 +337,8 @@ open class BatoTo(
|
|||||||
override fun mangaDetailsParse(document: Document): SManga {
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
val infoElement = document.selectFirst("div#mainer div.container-fluid")!!
|
val infoElement = document.selectFirst("div#mainer div.container-fluid")!!
|
||||||
val manga = SManga.create()
|
val manga = SManga.create()
|
||||||
val workStatus = infoElement.selectFirst("div.attr-item:contains(original work) span")?.text()
|
val workStatus = infoElement.select("div.attr-item:contains(original work) span").text()
|
||||||
val uploadStatus = infoElement.selectFirst("div.attr-item:contains(upload status) span")?.text()
|
val uploadStatus = infoElement.select("div.attr-item:contains(upload status) span").text()
|
||||||
val originalTitle = infoElement.select("h3").text().removeEntities()
|
val originalTitle = infoElement.select("h3").text().removeEntities()
|
||||||
val description = buildString {
|
val description = buildString {
|
||||||
append(infoElement.select("div.limit-html").text())
|
append(infoElement.select("div.limit-html").text())
|
||||||
@ -367,13 +346,13 @@ open class BatoTo(
|
|||||||
append("\n\n${it.text()}")
|
append("\n\n${it.text()}")
|
||||||
}
|
}
|
||||||
infoElement.selectFirst("h5:containsOwn(Extra Info:) + div")?.also {
|
infoElement.selectFirst("h5:containsOwn(Extra Info:) + div")?.also {
|
||||||
append("\n\nExtra Info:\n${it.wholeText()}")
|
append("\n\nExtra Info:\n${it.text()}")
|
||||||
}
|
}
|
||||||
document.selectFirst("div.pb-2.alias-set.line-b-f")?.takeIf { it.hasText() }?.also {
|
document.selectFirst("div.pb-2.alias-set.line-b-f")?.also {
|
||||||
append("\n\nAlternative Titles:\n")
|
append("\n\nAlternative Titles:\n")
|
||||||
append(it.text().split('/').joinToString("\n") { "• ${it.trim()}" })
|
append(it.text().split('/').joinToString("\n") { "• ${it.trim()}" })
|
||||||
}
|
}
|
||||||
}.trim()
|
}
|
||||||
|
|
||||||
val cleanedTitle = if (isRemoveTitleVersion()) {
|
val cleanedTitle = if (isRemoveTitleVersion()) {
|
||||||
originalTitle.replace(titleRegex, "").trim()
|
originalTitle.replace(titleRegex, "").trim()
|
||||||
@ -390,19 +369,16 @@ open class BatoTo(
|
|||||||
manga.thumbnail_url = document.select("div.attr-cover img").attr("abs:src")
|
manga.thumbnail_url = document.select("div.attr-cover img").attr("abs:src")
|
||||||
return manga
|
return manga
|
||||||
}
|
}
|
||||||
private fun parseStatus(workStatus: String?, uploadStatus: String?): Int {
|
private fun parseStatus(workStatus: String?, uploadStatus: String?) = when {
|
||||||
val status = workStatus ?: uploadStatus
|
workStatus == null -> SManga.UNKNOWN
|
||||||
return when {
|
workStatus.contains("Ongoing") -> SManga.ONGOING
|
||||||
status == null -> SManga.UNKNOWN
|
workStatus.contains("Cancelled") -> SManga.CANCELLED
|
||||||
status.contains("Ongoing") -> SManga.ONGOING
|
workStatus.contains("Hiatus") -> SManga.ON_HIATUS
|
||||||
status.contains("Cancelled") -> SManga.CANCELLED
|
workStatus.contains("Completed") -> when {
|
||||||
status.contains("Hiatus") -> SManga.ON_HIATUS
|
uploadStatus?.contains("Ongoing") == true -> SManga.PUBLISHING_FINISHED
|
||||||
status.contains("Completed") -> when {
|
else -> SManga.COMPLETED
|
||||||
uploadStatus?.contains("Ongoing") == true -> SManga.PUBLISHING_FINISHED
|
|
||||||
else -> SManga.COMPLETED
|
|
||||||
}
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
}
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun altChapterParse(response: Response): List<SChapter> {
|
private fun altChapterParse(response: Response): List<SChapter> {
|
||||||
@ -435,12 +411,6 @@ open class BatoTo(
|
|||||||
|
|
||||||
GET("$baseUrl/rss/series/$id.xml", headers)
|
GET("$baseUrl/rss/series/$id.xml", headers)
|
||||||
} else if (manga.url.startsWith("http")) {
|
} else if (manga.url.startsWith("http")) {
|
||||||
// Check if trying to use a deprecated mirror, force current mirror
|
|
||||||
val httpUrl = manga.url.toHttpUrl()
|
|
||||||
if ("https://${httpUrl.host}" in DEPRECATED_MIRRORS) {
|
|
||||||
val newHttpUrl = httpUrl.newBuilder().host(getMirrorPref().toHttpUrl().host)
|
|
||||||
return GET(newHttpUrl.build(), headers)
|
|
||||||
}
|
|
||||||
GET(manga.url, headers)
|
GET(manga.url, headers)
|
||||||
} else {
|
} else {
|
||||||
super.chapterListRequest(manga)
|
super.chapterListRequest(manga)
|
||||||
@ -537,12 +507,6 @@ open class BatoTo(
|
|||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
if (chapter.url.startsWith("http")) {
|
if (chapter.url.startsWith("http")) {
|
||||||
// Check if trying to use a deprecated mirror, force current mirror
|
|
||||||
val httpUrl = chapter.url.toHttpUrl()
|
|
||||||
if ("https://${httpUrl.host}" in DEPRECATED_MIRRORS) {
|
|
||||||
val newHttpUrl = httpUrl.newBuilder().host(getMirrorPref().toHttpUrl().host)
|
|
||||||
return GET(newHttpUrl.build(), headers)
|
|
||||||
}
|
|
||||||
return GET(chapter.url, headers)
|
return GET(chapter.url, headers)
|
||||||
}
|
}
|
||||||
return super.pageListRequest(chapter)
|
return super.pageListRequest(chapter)
|
||||||
@ -1037,7 +1001,7 @@ open class BatoTo(
|
|||||||
private const val MIRROR_PREF_TITLE = "Mirror"
|
private const val MIRROR_PREF_TITLE = "Mirror"
|
||||||
private const val REMOVE_TITLE_VERSION_PREF = "REMOVE_TITLE_VERSION"
|
private const val REMOVE_TITLE_VERSION_PREF = "REMOVE_TITLE_VERSION"
|
||||||
private val MIRROR_PREF_ENTRIES = arrayOf(
|
private val MIRROR_PREF_ENTRIES = arrayOf(
|
||||||
"Auto",
|
"zbato.org",
|
||||||
"batocomic.com",
|
"batocomic.com",
|
||||||
"batocomic.net",
|
"batocomic.net",
|
||||||
"batocomic.org",
|
"batocomic.org",
|
||||||
@ -1049,25 +1013,23 @@ open class BatoTo(
|
|||||||
"readtoto.com",
|
"readtoto.com",
|
||||||
"readtoto.net",
|
"readtoto.net",
|
||||||
"readtoto.org",
|
"readtoto.org",
|
||||||
|
"dto.to",
|
||||||
|
"fto.to",
|
||||||
|
"jto.to",
|
||||||
|
"hto.to",
|
||||||
|
"mto.to",
|
||||||
|
"wto.to",
|
||||||
"xbato.com",
|
"xbato.com",
|
||||||
"xbato.net",
|
"xbato.net",
|
||||||
"xbato.org",
|
"xbato.org",
|
||||||
"zbato.com",
|
"zbato.com",
|
||||||
"zbato.net",
|
"zbato.net",
|
||||||
"zbato.org",
|
|
||||||
"dto.to",
|
|
||||||
"fto.to",
|
|
||||||
"hto.to",
|
|
||||||
"jto.to",
|
|
||||||
"mto.to",
|
|
||||||
"wto.to",
|
|
||||||
)
|
)
|
||||||
private val MIRROR_PREF_ENTRY_VALUES = MIRROR_PREF_ENTRIES.map { "https://$it" }.toTypedArray()
|
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 val MIRROR_PREF_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES[0]
|
||||||
|
|
||||||
private val DEPRECATED_MIRRORS = listOf(
|
private val DEPRECATED_MIRRORS = listOf(
|
||||||
"https://bato.to",
|
"https://bato.to",
|
||||||
"https://batocc.com", // parked
|
|
||||||
"https://mangatoto.com",
|
"https://mangatoto.com",
|
||||||
"https://mangatoto.net",
|
"https://mangatoto.net",
|
||||||
"https://mangatoto.org",
|
"https://mangatoto.org",
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
ext {
|
|
||||||
extName = 'Comic Growl'
|
|
||||||
extClass = '.ComicGrowl'
|
|
||||||
themePkg = 'gigaviewer'
|
|
||||||
baseUrl = 'https://comic-growl.com'
|
|
||||||
overrideVersionCode = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 18 KiB |
@ -1,63 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.comicgrowl
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
|
|
||||||
// TODO: get manga status
|
|
||||||
// TODO: filter by status
|
|
||||||
// TODO: change cdnUrl as a array(upstream)
|
|
||||||
class ComicGrowl : GigaViewer(
|
|
||||||
"コミックグロウル",
|
|
||||||
"https://comic-growl.com",
|
|
||||||
"all",
|
|
||||||
"https://cdn-img.comic-growl.com/public/page",
|
|
||||||
) {
|
|
||||||
|
|
||||||
override val publisher = "BUSHIROAD WORKS"
|
|
||||||
|
|
||||||
override val chapterListMode = CHAPTER_LIST_LOCKED
|
|
||||||
|
|
||||||
override val supportsLatest: Boolean = true
|
|
||||||
|
|
||||||
override val client: OkHttpClient =
|
|
||||||
super.client.newBuilder().addInterceptor(::imageIntercept).build()
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
|
|
||||||
|
|
||||||
// Show only ongoing works
|
|
||||||
override fun popularMangaSelector(): String = "ul[class=\"lineup-list ongoing\"] > li > div > a"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
|
||||||
title = element.select("h5").text()
|
|
||||||
thumbnail_url = element.select("div > img").attr("data-src")
|
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector() =
|
|
||||||
"div[class=\"update latest\"] > div.card-board > " + "div[class~=card]:not([class~=ad]) > div > a"
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
|
|
||||||
title = element.select("div.data h3").text()
|
|
||||||
thumbnail_url = element.select("div.thumb-container img").attr("data-src")
|
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCollections(): List<Collection> = listOf(
|
|
||||||
Collection("連載作品", ""),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
if (query.isNotEmpty()) {
|
|
||||||
val url = "$baseUrl/search".toHttpUrl().newBuilder().addQueryParameter("q", query)
|
|
||||||
|
|
||||||
return GET(url.build(), headers)
|
|
||||||
}
|
|
||||||
return GET(baseUrl, headers) // Currently just get all ongoing works
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Comick'
|
extName = 'Comick'
|
||||||
extClass = '.ComickFactory'
|
extClass = '.ComickFactory'
|
||||||
extVersionCode = 52
|
extVersionCode = 51
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Comico'
|
extName = 'Comico'
|
||||||
extClass = '.ComicoFactory'
|
extClass = '.ComicoFactory'
|
||||||
extVersionCode = 6
|
extVersionCode = 5
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ import okhttp3.Headers
|
|||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.lang.Exception
|
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
@ -160,7 +159,7 @@ open class Comico(
|
|||||||
if (!chapter.name.endsWith(LOCK)) {
|
if (!chapter.name.endsWith(LOCK)) {
|
||||||
super.fetchPageList(chapter)
|
super.fetchPageList(chapter)
|
||||||
} else {
|
} else {
|
||||||
throw Exception("You are not authorized to view this!")
|
throw Error("You are not authorized to view this!")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun search(query: String, page: Int) =
|
private fun search(query: String, page: Int) =
|
||||||
@ -177,7 +176,7 @@ open class Comico(
|
|||||||
private val Response.data: JsonElement?
|
private val Response.data: JsonElement?
|
||||||
get() = json.parseToJsonElement(body.string()).jsonObject.also {
|
get() = json.parseToJsonElement(body.string()).jsonObject.also {
|
||||||
val code = it["result"]["code"].jsonPrimitive.int
|
val code = it["result"]["code"].jsonPrimitive.int
|
||||||
if (code != 200) throw Exception(status(code))
|
if (code != 200) throw Error(status(code))
|
||||||
}["data"]
|
}["data"]
|
||||||
|
|
||||||
private operator fun JsonElement?.get(key: String) =
|
private operator fun JsonElement?.get(key: String) =
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'CosplayTele'
|
extName = 'CosplayTele'
|
||||||
extClass = '.CosplayTele'
|
extClass = '.CosplayTele'
|
||||||
extVersionCode = 4
|
extVersionCode = 3
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ class CosplayTele : ParsedHttpSource() {
|
|||||||
override fun latestUpdatesNextPageSelector() = ".next.page-number"
|
override fun latestUpdatesNextPageSelector() = ".next.page-number"
|
||||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page/")
|
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page/")
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = "main div.box"
|
override fun latestUpdatesSelector() = "div.box"
|
||||||
|
|
||||||
// Popular
|
// Popular
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'DeviantArt'
|
extName = 'DeviantArt'
|
||||||
extClass = '.DeviantArt'
|
extClass = '.DeviantArt'
|
||||||
extVersionCode = 6
|
extVersionCode = 3
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.deviantart
|
package eu.kanade.tachiyomi.extension.all.deviantart
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
@ -20,22 +15,16 @@ import okhttp3.Response
|
|||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.parser.Parser
|
import org.jsoup.parser.Parser
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class DeviantArt : HttpSource(), ConfigurableSource {
|
class DeviantArt : HttpSource() {
|
||||||
override val name = "DeviantArt"
|
override val name = "DeviantArt"
|
||||||
override val baseUrl = "https://www.deviantart.com"
|
override val baseUrl = "https://deviantart.com"
|
||||||
override val lang = "all"
|
override val lang = "all"
|
||||||
override val supportsLatest = false
|
override val supportsLatest = false
|
||||||
|
|
||||||
private val preferences: SharedPreferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun headersBuilder() = Headers.Builder().apply {
|
override fun headersBuilder() = Headers.Builder().apply {
|
||||||
add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0")
|
add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0")
|
||||||
}
|
}
|
||||||
@ -87,32 +76,28 @@ class DeviantArt : HttpSource(), ConfigurableSource {
|
|||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
val gallery = document.selectFirst("#sub-folder-gallery")
|
val subFolderGallery = document.selectFirst("#sub-folder-gallery")
|
||||||
|
val manga = SManga.create().apply {
|
||||||
// If manga is sub-gallery then use sub-gallery name, else use gallery name
|
// If manga is sub-gallery then use sub-gallery name, else use gallery name
|
||||||
val galleryName = gallery?.selectFirst("._2vMZg + ._2vMZg")?.text()?.substringBeforeLast(" ")
|
title = subFolderGallery?.selectFirst("._2vMZg + ._2vMZg")?.text()?.substringBeforeLast(" ")
|
||||||
?: gallery?.selectFirst("[aria-haspopup=listbox] > div")!!.ownText()
|
?: subFolderGallery?.selectFirst("[aria-haspopup=listbox] > div")!!.ownText()
|
||||||
val artistInTitle = preferences.artistInTitle == ArtistInTitle.ALWAYS.name ||
|
|
||||||
preferences.artistInTitle == ArtistInTitle.ONLY_ALL_GALLERIES.name && galleryName == "All"
|
|
||||||
|
|
||||||
return SManga.create().apply {
|
|
||||||
setUrlWithoutDomain(response.request.url.toString())
|
|
||||||
author = document.title().substringBefore(" ")
|
author = document.title().substringBefore(" ")
|
||||||
title = when (artistInTitle) {
|
description = subFolderGallery?.selectFirst(".legacy-journal")?.wholeText()
|
||||||
true -> "$author - $galleryName"
|
thumbnail_url = subFolderGallery?.selectFirst("img[property=contentUrl]")?.absUrl("src")
|
||||||
false -> galleryName
|
|
||||||
}
|
|
||||||
description = gallery?.selectFirst(".legacy-journal")?.wholeText()
|
|
||||||
thumbnail_url = gallery?.selectFirst("img[property=contentUrl]")?.absUrl("src")
|
|
||||||
}
|
}
|
||||||
|
manga.setUrlWithoutDomain(response.request.url.toString())
|
||||||
|
return manga
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
val pathSegments = getMangaUrl(manga).toHttpUrl().pathSegments
|
val pathSegments = getMangaUrl(manga).toHttpUrl().pathSegments
|
||||||
val username = pathSegments[0]
|
val username = pathSegments[0]
|
||||||
val query = when (val folderId = pathSegments[2]) {
|
val folderId = pathSegments[2]
|
||||||
"all" -> "gallery:$username"
|
|
||||||
else -> "gallery:$username/$folderId"
|
val query = if (folderId == "all") {
|
||||||
|
"gallery:$username"
|
||||||
|
} else {
|
||||||
|
"gallery:$username/$folderId"
|
||||||
}
|
}
|
||||||
|
|
||||||
val url = backendBuilder()
|
val url = backendBuilder()
|
||||||
@ -138,14 +123,15 @@ class DeviantArt : HttpSource(), ConfigurableSource {
|
|||||||
nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href")
|
nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href")
|
||||||
}
|
}
|
||||||
|
|
||||||
return chapterList.toList().also(::indexChapterList)
|
return indexChapterList(chapterList.toList())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseToChapterList(document: Document): List<SChapter> {
|
private fun parseToChapterList(document: Document): List<SChapter> {
|
||||||
val items = document.select("item")
|
val items = document.select("item")
|
||||||
return items.map {
|
return items.map {
|
||||||
SChapter.create().apply {
|
val chapter = SChapter.create()
|
||||||
setUrlWithoutDomain(it.selectFirst("link")!!.text())
|
chapter.setUrlWithoutDomain(it.selectFirst("link")!!.text())
|
||||||
|
chapter.apply {
|
||||||
name = it.selectFirst("title")!!.text()
|
name = it.selectFirst("title")!!.text()
|
||||||
date_upload = parseDate(it.selectFirst("pubDate")?.text())
|
date_upload = parseDate(it.selectFirst("pubDate")?.text())
|
||||||
scanlator = it.selectFirst("media|credit")?.text()
|
scanlator = it.selectFirst("media|credit")?.text()
|
||||||
@ -153,34 +139,24 @@ class DeviantArt : HttpSource(), ConfigurableSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun indexChapterList(chapterList: List<SChapter>) {
|
private fun indexChapterList(chapterList: List<SChapter>): List<SChapter> {
|
||||||
// DeviantArt allows users to arrange galleries arbitrarily so we will
|
// DeviantArt allows users to arrange galleries arbitrarily so we will
|
||||||
// primitively index the list by checking the first and last dates
|
// primitively index the list by checking the first and last dates
|
||||||
if (chapterList.first().date_upload > chapterList.last().date_upload) {
|
return if (chapterList.first().date_upload > chapterList.last().date_upload) {
|
||||||
chapterList.forEachIndexed { i, chapter ->
|
chapterList.mapIndexed { i, chapter ->
|
||||||
chapter.chapter_number = chapterList.size - i.toFloat()
|
chapter.apply { chapter_number = chapterList.size - i.toFloat() }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
chapterList.forEachIndexed { i, chapter ->
|
chapterList.mapIndexed { i, chapter ->
|
||||||
chapter.chapter_number = i.toFloat() + 1
|
chapter.apply { chapter_number = i.toFloat() + 1 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
val firstImageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src")
|
val imageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src")
|
||||||
return when (val buttons = document.selectFirst("[draggable=false]")?.children()) {
|
return listOf(Page(0, imageUrl = imageUrl))
|
||||||
null -> listOf(Page(0, imageUrl = firstImageUrl))
|
|
||||||
else -> buttons.mapIndexed { i, button ->
|
|
||||||
// Remove everything past "/v1/" to get original instead of thumbnail
|
|
||||||
val imageUrl = button.selectFirst("img")?.absUrl("src")?.substringBefore("/v1/")
|
|
||||||
Page(i, imageUrl = imageUrl)
|
|
||||||
}.also {
|
|
||||||
// First image needs token to get original, which is included in firstImageUrl
|
|
||||||
it[0].imageUrl = firstImageUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
override fun imageUrlParse(response: Response): String {
|
||||||
@ -191,38 +167,7 @@ class DeviantArt : HttpSource(), ConfigurableSource {
|
|||||||
return Jsoup.parse(body.string(), request.url.toString(), Parser.xmlParser())
|
return Jsoup.parse(body.string(), request.url.toString(), Parser.xmlParser())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
val artistInTitlePref = ListPreference(screen.context).apply {
|
|
||||||
key = ArtistInTitle.PREF_KEY
|
|
||||||
title = "Artist name in manga title"
|
|
||||||
entries = ArtistInTitle.values().map { it.text }.toTypedArray()
|
|
||||||
entryValues = ArtistInTitle.values().map { it.name }.toTypedArray()
|
|
||||||
summary = "Current: %s\n\n" +
|
|
||||||
"Changing this preference will not automatically apply to manga in Library " +
|
|
||||||
"and History, so refresh all DeviantArt manga and/or clear database in Settings " +
|
|
||||||
"> Advanced after doing so."
|
|
||||||
setDefaultValue(ArtistInTitle.defaultValue.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
screen.addPreference(artistInTitlePref)
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum class ArtistInTitle(val text: String) {
|
|
||||||
NEVER("Never"),
|
|
||||||
ALWAYS("Always"),
|
|
||||||
ONLY_ALL_GALLERIES("Only in \"All\" galleries"),
|
|
||||||
;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val PREF_KEY = "artistInTitlePref"
|
|
||||||
val defaultValue = ONLY_ALL_GALLERIES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val SharedPreferences.artistInTitle
|
|
||||||
get() = getString(ArtistInTitle.PREF_KEY, ArtistInTitle.defaultValue.name)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val SEARCH_FORMAT_MSG = "Please enter a query in the format of gallery:{username} or gallery:{username}/{folderId}"
|
const val SEARCH_FORMAT_MSG = "Please enter a query in the format of gallery:{username} or gallery:{username}/{folderId}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Manga18Free'
|
extName = 'Eromanhwa'
|
||||||
extClass = '.manga18free'
|
extClass = '.Eromanhwa'
|
||||||
themePkg = 'madara'
|
themePkg = 'madara'
|
||||||
baseUrl = 'https://manga18free.com'
|
baseUrl = 'https://eromanhwa.org'
|
||||||
overrideVersionCode = 1
|
overrideVersionCode = 1
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
BIN
src/all/eromanhwa/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
src/all/eromanhwa/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/all/eromanhwa/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
src/all/eromanhwa/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/all/eromanhwa/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1,12 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.eromanhwa
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
||||||
|
|
||||||
|
class Eromanhwa : Madara(
|
||||||
|
"Eromanhwa",
|
||||||
|
"https://eromanhwa.org",
|
||||||
|
"all",
|
||||||
|
) {
|
||||||
|
override val id = 3597355706480775153 // accidently set lang to en...
|
||||||
|
override val useNewChapterEndpoint = true
|
||||||
|
}
|
8
src/all/freleinbooks/build.gradle
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'Frelein Books'
|
||||||
|
extClass = '.FreleinBooks'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = false
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/all/freleinbooks/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
src/all/freleinbooks/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
src/all/freleinbooks/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
src/all/freleinbooks/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
src/all/freleinbooks/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
@ -0,0 +1,271 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.freleinbooks
|
||||||
|
|
||||||
|
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.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class FreleinBooks() : ParsedHttpSource() {
|
||||||
|
override val baseUrl = "https://books.frelein.my.id"
|
||||||
|
override val lang = "all"
|
||||||
|
override val name = "Frelein Books"
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
private val Element.imgSrc: String
|
||||||
|
get() = attr("data-lazy-src")
|
||||||
|
.ifEmpty { attr("data-src") }
|
||||||
|
.ifEmpty { attr("src") }
|
||||||
|
|
||||||
|
// Latest
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
manga.thumbnail_url = element.selectFirst("img")!!.imgSrc
|
||||||
|
manga.title = element.select(".postTitle").text()
|
||||||
|
manga.setUrlWithoutDomain(element.select(".postTitle > a").attr("abs:href"))
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = ".olderLink"
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
return if (page == 1) {
|
||||||
|
GET(baseUrl)
|
||||||
|
} else {
|
||||||
|
val dateParam = page * 7 * 2
|
||||||
|
// Calendar set to the current date
|
||||||
|
val calendar: Calendar = Calendar.getInstance()
|
||||||
|
// rollback 14 days
|
||||||
|
calendar.add(Calendar.DAY_OF_YEAR, -dateParam)
|
||||||
|
val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
|
// now the date is 14 days back
|
||||||
|
GET("$baseUrl/search?updated-max=${formatter.format(calendar.time)}T12:38:00%2B07:00&max-results=12&start=12&by-date=false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = ".blogPosts > article"
|
||||||
|
|
||||||
|
// Popular
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
manga.thumbnail_url = element.selectFirst("img")!!.imgSrc
|
||||||
|
manga.title = element.select("h3").text()
|
||||||
|
manga.setUrlWithoutDomain(element.select("h3 > a").attr("abs:href"))
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector(): String? = null
|
||||||
|
override fun popularMangaRequest(page: Int) = latestUpdatesRequest(page)
|
||||||
|
override fun popularMangaSelector() = ".itemPopulars article"
|
||||||
|
|
||||||
|
// Search
|
||||||
|
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
|
||||||
|
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||||
|
val tagFilter = filterList.findInstance<TagFilter>()!!
|
||||||
|
val groupFilter = filterList.findInstance<GroupFilter>()!!
|
||||||
|
val magazineFilter = filterList.findInstance<MagazineFilter>()!!
|
||||||
|
val fashionMagazineFilter = filterList.findInstance<FashionMagazineFilter>()!!
|
||||||
|
return when {
|
||||||
|
query.isEmpty() && groupFilter.state != 0 -> GET("$baseUrl/search/label/${groupFilter.toUriPart()}")
|
||||||
|
query.isEmpty() && magazineFilter.state != 0 -> GET("$baseUrl/search/label/${magazineFilter.toUriPart()}")
|
||||||
|
query.isEmpty() && fashionMagazineFilter.state != 0 -> GET("$baseUrl/search/label/${fashionMagazineFilter.toUriPart()}")
|
||||||
|
query.isEmpty() && tagFilter.state.isNotEmpty() -> GET("$baseUrl/search/label/${tagFilter.state}")
|
||||||
|
query.isNotEmpty() -> GET("$baseUrl/search?q=$query")
|
||||||
|
else -> latestUpdatesRequest(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = latestUpdatesSelector()
|
||||||
|
|
||||||
|
// Details
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
manga.title = document.select(".postTitle").text()
|
||||||
|
manga.description = "Read ${document.select(".postTitle").text()} \n \nNote: If you encounters error when opening the magazine, please press the WebView button then leave a comment on our web so we can update it soon."
|
||||||
|
manga.genre = document.select(".labelLink > a")
|
||||||
|
.joinToString(", ") { it.text() }
|
||||||
|
manga.status = SManga.COMPLETED
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
|
val chapter = SChapter.create()
|
||||||
|
chapter.setUrlWithoutDomain(element.select("link[rel=\"canonical\"]").attr("href"))
|
||||||
|
chapter.name = "Gallery"
|
||||||
|
chapter.date_upload = getDate(element.select("link[rel=\"canonical\"]").attr("href"))
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "html"
|
||||||
|
|
||||||
|
// Pages
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
val pages = mutableListOf<Page>()
|
||||||
|
document.select("noscript").remove()
|
||||||
|
document.select(".gallerybox a > img").forEachIndexed { i, it ->
|
||||||
|
// format new img/b/
|
||||||
|
if (it.imgSrc.contains("img/b/")) {
|
||||||
|
if (it.imgSrc.contains("/w768-rw/")) {
|
||||||
|
val itUrl = it.imgSrc.replace("/w768-rw/", "/s0/")
|
||||||
|
pages.add(Page(i, itUrl, itUrl))
|
||||||
|
}
|
||||||
|
if (it.imgSrc.contains("/w480-rw/")) {
|
||||||
|
val itUrl = it.imgSrc.replace("/w480-rw/", "/s0/")
|
||||||
|
pages.add(Page(i, itUrl, itUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// format new img/b/
|
||||||
|
else {
|
||||||
|
if (it.imgSrc.contains("=w768-rw")) {
|
||||||
|
val itUrl = it.imgSrc.replace("=w768-rw", "")
|
||||||
|
pages.add(Page(i, itUrl, itUrl))
|
||||||
|
} else if (it.imgSrc.contains("=w480-rw")) {
|
||||||
|
val itUrl = it.imgSrc.replace("=w480-rw", "")
|
||||||
|
pages.add(Page(i, itUrl, itUrl))
|
||||||
|
} else {
|
||||||
|
val itUrl = it.imgSrc
|
||||||
|
pages.add(Page(i, itUrl, itUrl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList = FilterList(
|
||||||
|
Filter.Header("NOTE: Only one filter will be applied!"),
|
||||||
|
Filter.Separator(),
|
||||||
|
GroupFilter(),
|
||||||
|
MagazineFilter(),
|
||||||
|
FashionMagazineFilter(),
|
||||||
|
TagFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
open class UriPartFilter(
|
||||||
|
displayName: String,
|
||||||
|
private val valuePair: Array<Pair<String, String>>,
|
||||||
|
) : Filter.Select<String>(displayName, valuePair.map { it.first }.toTypedArray()) {
|
||||||
|
fun toUriPart() = valuePair[state].second
|
||||||
|
}
|
||||||
|
|
||||||
|
class MagazineFilter : UriPartFilter(
|
||||||
|
"Magazine",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Any", ""),
|
||||||
|
Pair("B.L.T.", "B.L.T."),
|
||||||
|
Pair("BIG ONE GIRLS", "BIG ONE GIRLS"),
|
||||||
|
Pair("BOMB!", "BOMB!"),
|
||||||
|
Pair("BRODY", "BRODY"),
|
||||||
|
Pair("BUBKA", "BUBKA"),
|
||||||
|
Pair("ENTAME", "ENTAME"),
|
||||||
|
Pair("EX Taishu", "EX Taishu"),
|
||||||
|
Pair("FINEBOYS", "FINEBOYS"),
|
||||||
|
Pair("FLASH", "FLASH"),
|
||||||
|
Pair("Fine", "Fine"),
|
||||||
|
Pair("Friday", "Friday"),
|
||||||
|
Pair("HINA_SATSU", "HINA_SATSU"),
|
||||||
|
Pair("IDOL AND READ", "IDOL AND READ"),
|
||||||
|
Pair("Kadokawa Scene 07", "Kadokawa Scene 07"),
|
||||||
|
Pair("Monthly Basketball", "Monthly Basketball"),
|
||||||
|
Pair("Monthly Young Magazine", "Monthly Young Magazine"),
|
||||||
|
Pair("NOGI_SATSU", "NOGI_SATSU"),
|
||||||
|
Pair("Nylon Japan", "Nylon Japan"),
|
||||||
|
Pair("Platinum FLASH", "Platinum FLASH"),
|
||||||
|
Pair("Shonen Magazine", "Shonen Magazine"),
|
||||||
|
Pair("Shukan Post", "Shukan Post"),
|
||||||
|
Pair("TOKYO NEWS MOOK", "TOKYO NEWS MOOK"),
|
||||||
|
Pair("TV LIFE,Tarzan", "TV LIFE,Tarzan"),
|
||||||
|
Pair("Tokyo Calendar", "Tokyo Calendar"),
|
||||||
|
Pair("Top Yell NEO", "Top Yell NEO"),
|
||||||
|
Pair("UTB", "UTB"),
|
||||||
|
Pair("Weekly Playboy", "Weekly Playboy"),
|
||||||
|
Pair("Weekly SPA", "Weekly SPA"),
|
||||||
|
Pair("Weekly SPA!", "Weekly SPA!"),
|
||||||
|
Pair("Weekly Shonen Champion", "Weekly Shonen Champion"),
|
||||||
|
Pair("Weekly Shonen Magazine", "Weekly Shonen Magazine"),
|
||||||
|
Pair("Weekly Shonen Sunday", "Weekly Shonen Sunday"),
|
||||||
|
Pair("Weekly Shounen Magazine", "Weekly Shounen Magazine"),
|
||||||
|
Pair("Weekly The Television Plus", "Weekly The Television Plus"),
|
||||||
|
Pair("Weekly Zero Jump", "Weekly Zero Jump"),
|
||||||
|
Pair("Yanmaga Web", "Yanmaga Web"),
|
||||||
|
Pair("Young Animal", "Young Animal"),
|
||||||
|
Pair("Young Champion", "Young Champion"),
|
||||||
|
Pair("Young Gangan", "Young Gangan"),
|
||||||
|
Pair("Young Jump", "Young Jump"),
|
||||||
|
Pair("Young Magazine", "Young Magazine"),
|
||||||
|
Pair("blt graph.", "blt graph."),
|
||||||
|
Pair("mini", "mini"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class FashionMagazineFilter : UriPartFilter(
|
||||||
|
"Fashion Magazine",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Any", ""),
|
||||||
|
Pair("BAILA", "BAILA"),
|
||||||
|
Pair("Biteki", "Biteki"),
|
||||||
|
Pair("CLASSY", "CLASSY"),
|
||||||
|
Pair("CanCam", "CanCam"),
|
||||||
|
Pair("JJ", "JJ"),
|
||||||
|
Pair("LARME", "LARME"),
|
||||||
|
Pair("MARQUEE", "MARQUEE"),
|
||||||
|
Pair("Maquia", "Maquia"),
|
||||||
|
Pair("Men's non-no", "Men's non-no"),
|
||||||
|
Pair("More", "More"),
|
||||||
|
Pair("Oggi", "Oggi"),
|
||||||
|
Pair("Ray", "Ray"),
|
||||||
|
Pair("Seventeen", "Seventeen"),
|
||||||
|
Pair("Sweet", "Sweet"),
|
||||||
|
Pair("VOCE", "VOCE"),
|
||||||
|
Pair("ViVi", "ViVi"),
|
||||||
|
Pair("With", "With"),
|
||||||
|
Pair("aR", "aR"),
|
||||||
|
Pair("anan", "anan"),
|
||||||
|
Pair("bis", "bis"),
|
||||||
|
Pair("non-no", "non-no"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class GroupFilter : UriPartFilter(
|
||||||
|
"Group",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Any", ""),
|
||||||
|
Pair("Hinatazaka46", "Hinatazaka46"),
|
||||||
|
Pair("Nogizaka46", "Nogizaka46"),
|
||||||
|
Pair("Sakurazaka46", "Sakurazaka46"),
|
||||||
|
Pair("Keyakizaka46", "Keyakizaka46"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class TagFilter : Filter.Text("Tag")
|
||||||
|
|
||||||
|
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||||
|
|
||||||
|
private fun getDate(str: String): Long {
|
||||||
|
val regex = "[0-9]{4}\\/[0-9]{2}\\/[0-9]{2}".toRegex()
|
||||||
|
val match = regex.find(str)
|
||||||
|
return runCatching { DATE_FORMAT.parse(match!!.value)?.time }.getOrNull() ?: 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DATE_FORMAT by lazy {
|
||||||
|
SimpleDateFormat("yyyy/MM/dd", Locale.US)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Hitomi'
|
extName = 'Hitomi'
|
||||||
extClass = '.HitomiFactory'
|
extClass = '.HitomiFactory'
|
||||||
extVersionCode = 36
|
extVersionCode = 35
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,11 +64,8 @@ class Hitomi(
|
|||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val REGEX_IMAGE_URL = """https://.*?a\.$domain/(jxl|avif|webp)/\d+?/\d+/([0-9a-f]{64})\.\1""".toRegex()
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
.addInterceptor(::jxlContentTypeInterceptor)
|
.addInterceptor(::jxlContentTypeInterceptor)
|
||||||
.addInterceptor(::updateImageUrlInterceptor)
|
|
||||||
.apply {
|
.apply {
|
||||||
interceptors().add(0, ::streamResetRetry)
|
interceptors().add(0, ::streamResetRetry)
|
||||||
}
|
}
|
||||||
@ -751,25 +748,6 @@ class Hitomi(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateImageUrlInterceptor(chain: Interceptor.Chain): Response {
|
|
||||||
val request = chain.request()
|
|
||||||
|
|
||||||
val cleanUrl = request.url.run { "$scheme://$host$encodedPath" }
|
|
||||||
REGEX_IMAGE_URL.matchEntire(cleanUrl)?.let { match ->
|
|
||||||
val (ext, hash) = match.destructured
|
|
||||||
|
|
||||||
val commonId = runBlocking { commonImageId() }
|
|
||||||
val imageId = imageIdFromHash(hash)
|
|
||||||
val subDomain = 'a' + runBlocking { subdomainOffset(imageId) }
|
|
||||||
|
|
||||||
val newUrl = "https://${subDomain}a.$domain/$ext/$commonId$imageId/$hash.$ext"
|
|
||||||
val newRequest = request.newBuilder().url(newUrl).build()
|
|
||||||
return chain.proceed(newRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
return chain.proceed(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||||
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
||||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Little Garden'
|
extName = 'Little Garden'
|
||||||
extClass = '.LittleGarden'
|
extClass = '.LittleGarden'
|
||||||
extVersionCode = 3
|
extVersionCode = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -31,7 +31,7 @@ class LittleGarden : ParsedHttpSource() {
|
|||||||
private const val cdnUrl = "https://littlexgarden.com/static/images/webp/"
|
private const val cdnUrl = "https://littlexgarden.com/static/images/webp/"
|
||||||
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||||
private val slugRegex = Regex("\\\\\"slug\\\\\":\\\\\"(.*?(?=\\\\\"))")
|
private val slugRegex = Regex("\\\\\"slug\\\\\":\\\\\"(.*?(?=\\\\\"))")
|
||||||
private val oricolPageRegex = Regex("\\{colored:(.*?(?=,)),original:(.*?(?=,))")
|
private val oricolPageRegex = Regex("\\{colored:(?<colored>.*?(?=,)),original:(?<original>.*?(?=,))")
|
||||||
private val oriPageRegex = Regex("""original:"(.*?(?="))""")
|
private val oriPageRegex = Regex("""original:"(.*?(?="))""")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,10 +176,10 @@ class LittleGarden : ParsedHttpSource() {
|
|||||||
val engChaps: IntArray = intArrayOf(970, 987, 992)
|
val engChaps: IntArray = intArrayOf(970, 987, 992)
|
||||||
if (document.selectFirst("div.manga-name")!!.text().trim() == "One Piece" && (engChaps.contains(chapNb) || chapNb > 1004)) { // Permits to get French pages rather than English pages for some chapters
|
if (document.selectFirst("div.manga-name")!!.text().trim() == "One Piece" && (engChaps.contains(chapNb) || chapNb > 1004)) { // Permits to get French pages rather than English pages for some chapters
|
||||||
oricolPageRegex.findAll(document.select("script:containsData(pages)").toString()).asIterable().mapIndexed { i, it ->
|
oricolPageRegex.findAll(document.select("script:containsData(pages)").toString()).asIterable().mapIndexed { i, it ->
|
||||||
if (it.groups[1]?.value?.contains("\"") == true) { // Their JS dict has " " around the link only when available. Also uses colored pages rather than B&W as it's the main strength of this site
|
if (it.groups["colored"]?.value?.contains("\"") == true) { // Their JS dict has " " around the link only when available. Also uses colored pages rather than B&W as it's the main strength of this site
|
||||||
pages.add(Page(i, "", cdnUrl + it.groups[1]?.value?.replace("\"", "") + ".webp"))
|
pages.add(Page(i, "", cdnUrl + it.groups["colored"]?.value?.replace("\"", "") + ".webp"))
|
||||||
} else {
|
} else {
|
||||||
pages.add(Page(i, "", cdnUrl + it.groups[2]?.value?.replace("\"", "") + ".webp"))
|
pages.add(Page(i, "", cdnUrl + it.groups["original"]?.value?.replace("\"", "") + ".webp"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -23,9 +23,6 @@ data_saver_summary=Enables smaller, more compressed images
|
|||||||
excluded_tags_mode=Excluded tags mode
|
excluded_tags_mode=Excluded tags mode
|
||||||
filter_original_languages=Filter original languages
|
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
|
filter_original_languages_summary=Only show content that was originally published in the selected languages in both latest and browse
|
||||||
final_chapter=Final chapter:
|
|
||||||
final_chapter_in_description=Final chapter in description
|
|
||||||
final_chapter_in_description_summary=Include a manga's final chapter number at the end of its description
|
|
||||||
format=Format
|
format=Format
|
||||||
format_adaptation=Adaptation
|
format_adaptation=Adaptation
|
||||||
format_anthology=Anthology
|
format_anthology=Anthology
|
||||||
@ -79,8 +76,8 @@ original_language=Original language
|
|||||||
original_language_filter_chinese=%s (Manhua)
|
original_language_filter_chinese=%s (Manhua)
|
||||||
original_language_filter_japanese=%s (Manga)
|
original_language_filter_japanese=%s (Manga)
|
||||||
original_language_filter_korean=%s (Manhwa)
|
original_language_filter_korean=%s (Manhwa)
|
||||||
prefer_title_in_extension_language=Use alternative titles
|
prefer_title_in_extension_language=Use Alternate Titles
|
||||||
prefer_title_in_extension_language_summary=If there is an alternative title available which matches the extension language, it will be used
|
prefer_title_in_extension_language_summary=If there is an alternate title available which matches the extension language, it will be used
|
||||||
publication_demographic=Publication demographic
|
publication_demographic=Publication demographic
|
||||||
publication_demographic_josei=Josei
|
publication_demographic_josei=Josei
|
||||||
publication_demographic_none=None
|
publication_demographic_none=None
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'MangaDex'
|
extName = 'MangaDex'
|
||||||
extClass = '.MangaDexFactory'
|
extClass = '.MangaDexFactory'
|
||||||
extVersionCode = 199
|
extVersionCode = 196
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,11 +143,6 @@ object MDConstants {
|
|||||||
return "${preferExtensionLangTitlePref}_$dexLang"
|
return "${preferExtensionLangTitlePref}_$dexLang"
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val finalChapterInDescPref = "finalChapterInDesc"
|
|
||||||
fun getFinalChapterInDescPrefKey(dexLang: String): String {
|
|
||||||
return "${finalChapterInDescPref}_$dexLang"
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val tagGroupContent = "content"
|
private const val tagGroupContent = "content"
|
||||||
private const val tagGroupFormat = "format"
|
private const val tagGroupFormat = "format"
|
||||||
private const val tagGroupGenre = "genre"
|
private const val tagGroupGenre = "genre"
|
||||||
|
@ -424,7 +424,6 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
|
|||||||
preferences.coverQuality,
|
preferences.coverQuality,
|
||||||
preferences.altTitlesInDesc,
|
preferences.altTitlesInDesc,
|
||||||
preferences.preferExtensionLangTitle,
|
preferences.preferExtensionLangTitle,
|
||||||
preferences.finalChapterInDesc,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -774,28 +773,12 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val finalChapterInDescPref = SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = MDConstants.getFinalChapterInDescPrefKey(dexLang)
|
|
||||||
title = helper.intl["final_chapter_in_description"]
|
|
||||||
summary = helper.intl["final_chapter_in_description_summary"]
|
|
||||||
setDefaultValue(true)
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
val checkValue = newValue as Boolean
|
|
||||||
|
|
||||||
preferences.edit()
|
|
||||||
.putBoolean(MDConstants.getFinalChapterInDescPrefKey(dexLang), checkValue)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
screen.addPreference(coverQualityPref)
|
screen.addPreference(coverQualityPref)
|
||||||
screen.addPreference(tryUsingFirstVolumeCoverPref)
|
screen.addPreference(tryUsingFirstVolumeCoverPref)
|
||||||
screen.addPreference(dataSaverPref)
|
screen.addPreference(dataSaverPref)
|
||||||
screen.addPreference(standardHttpsPortPref)
|
screen.addPreference(standardHttpsPortPref)
|
||||||
screen.addPreference(altTitlesInDescPref)
|
screen.addPreference(altTitlesInDescPref)
|
||||||
screen.addPreference(preferExtensionLangTitlePref)
|
screen.addPreference(preferExtensionLangTitlePref)
|
||||||
screen.addPreference(finalChapterInDescPref)
|
|
||||||
screen.addPreference(contentRatingPref)
|
screen.addPreference(contentRatingPref)
|
||||||
screen.addPreference(originalLanguagePref)
|
screen.addPreference(originalLanguagePref)
|
||||||
screen.addPreference(blockedGroupsPref)
|
screen.addPreference(blockedGroupsPref)
|
||||||
@ -877,9 +860,6 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
|
|||||||
private val SharedPreferences.preferExtensionLangTitle
|
private val SharedPreferences.preferExtensionLangTitle
|
||||||
get() = getBoolean(MDConstants.getPreferExtensionLangTitlePrefKey(dexLang), true)
|
get() = getBoolean(MDConstants.getPreferExtensionLangTitlePrefKey(dexLang), true)
|
||||||
|
|
||||||
private val SharedPreferences.finalChapterInDesc
|
|
||||||
get() = getBoolean(MDConstants.getFinalChapterInDescPrefKey(dexLang), true)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Previous versions of the extension allowed invalid UUID values to be stored in the
|
* 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
|
* preferences. This method clear invalid UUIDs in case the user have updated from
|
||||||
|
@ -156,7 +156,7 @@ class MangaDexHelper(lang: String) {
|
|||||||
*/
|
*/
|
||||||
private fun String.removeEntitiesAndMarkdown(): String {
|
private fun String.removeEntitiesAndMarkdown(): String {
|
||||||
return removeEntities()
|
return removeEntities()
|
||||||
.substringBefore("\n---")
|
.substringBefore("---")
|
||||||
.replace(markdownLinksRegex, "$1")
|
.replace(markdownLinksRegex, "$1")
|
||||||
.replace(markdownItalicBoldRegex, "$1")
|
.replace(markdownItalicBoldRegex, "$1")
|
||||||
.replace(markdownItalicRegex, "$1")
|
.replace(markdownItalicRegex, "$1")
|
||||||
@ -324,7 +324,6 @@ class MangaDexHelper(lang: String) {
|
|||||||
coverSuffix: String?,
|
coverSuffix: String?,
|
||||||
altTitlesInDesc: Boolean,
|
altTitlesInDesc: Boolean,
|
||||||
preferExtensionLangTitle: Boolean,
|
preferExtensionLangTitle: Boolean,
|
||||||
finalChapterInDesc: Boolean,
|
|
||||||
): SManga {
|
): SManga {
|
||||||
val attr = mangaDataDto.attributes!!
|
val attr = mangaDataDto.attributes!!
|
||||||
|
|
||||||
@ -366,12 +365,9 @@ class MangaDexHelper(lang: String) {
|
|||||||
|
|
||||||
val genreList = MDConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } + nonGenres
|
val genreList = MDConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } + nonGenres
|
||||||
|
|
||||||
// Build description
|
var desc = (attr.description[lang] ?: attr.description["en"])
|
||||||
val desc = mutableListOf<String>()
|
|
||||||
|
|
||||||
(attr.description[lang] ?: attr.description["en"])
|
|
||||||
?.removeEntitiesAndMarkdown()
|
?.removeEntitiesAndMarkdown()
|
||||||
?.let { desc.add(it) }
|
.orEmpty()
|
||||||
|
|
||||||
if (altTitlesInDesc) {
|
if (altTitlesInDesc) {
|
||||||
val romanizedOriginalLang = MDConstants.romanizedLangCodes[attr.originalLanguage].orEmpty()
|
val romanizedOriginalLang = MDConstants.romanizedLangCodes[attr.originalLanguage].orEmpty()
|
||||||
@ -383,24 +379,12 @@ class MangaDexHelper(lang: String) {
|
|||||||
if (altTitles.isNotEmpty()) {
|
if (altTitles.isNotEmpty()) {
|
||||||
val altTitlesDesc = altTitles
|
val altTitlesDesc = altTitles
|
||||||
.joinToString("\n", "${intl["alternative_titles"]}\n") { "• $it" }
|
.joinToString("\n", "${intl["alternative_titles"]}\n") { "• $it" }
|
||||||
desc.add(altTitlesDesc.removeEntities())
|
desc += (if (desc.isBlank()) "" else "\n\n") + altTitlesDesc.removeEntities()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finalChapterInDesc) {
|
|
||||||
val finalChapter = mutableListOf<String>()
|
|
||||||
attr.lastVolume?.takeIf { it.isNotEmpty() }?.let { finalChapter.add("Vol.$it") }
|
|
||||||
attr.lastChapter?.takeIf { it.isNotEmpty() }?.let { finalChapter.add("Ch.$it") }
|
|
||||||
|
|
||||||
if (finalChapter.isNotEmpty()) {
|
|
||||||
val finalChapterDesc = finalChapter
|
|
||||||
.joinToString(" ", "${intl["final_chapter"]}\n")
|
|
||||||
desc.add(finalChapterDesc.removeEntities())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang, preferExtensionLangTitle).apply {
|
return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang, preferExtensionLangTitle).apply {
|
||||||
description = desc.joinToString("\n\n")
|
description = desc
|
||||||
author = authors.joinToString()
|
author = authors.joinToString()
|
||||||
artist = artists.joinToString()
|
artist = artists.joinToString()
|
||||||
status = getPublicationStatus(attr, chapters)
|
status = getPublicationStatus(attr, chapters)
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'MangaFire'
|
extName = 'MangaFire'
|
||||||
extClass = '.MangaFireFactory'
|
extClass = '.MangaFireFactory'
|
||||||
extVersionCode = 10
|
themePkg = 'mangareader'
|
||||||
|
baseUrl = 'https://mangafire.to'
|
||||||
|
overrideVersionCode = 5
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,190 +1,166 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.mangafire
|
package eu.kanade.tachiyomi.extension.all.mangafire
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
interface UriFilter {
|
class Entry(name: String, val id: String) : Filter.CheckBox(name) {
|
||||||
fun addToUri(builder: HttpUrl.Builder)
|
constructor(name: String) : this(name, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
open class UriPartFilter(
|
sealed class Group(
|
||||||
name: String,
|
name: String,
|
||||||
private val param: String,
|
val param: String,
|
||||||
private val vals: Array<Pair<String, String>>,
|
values: List<Entry>,
|
||||||
defaultValue: String? = null,
|
) : Filter.Group<Entry>(name, values)
|
||||||
) : Filter.Select<String>(
|
|
||||||
name,
|
|
||||||
vals.map { it.first }.toTypedArray(),
|
|
||||||
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
|
|
||||||
),
|
|
||||||
UriFilter {
|
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
|
||||||
builder.addQueryParameter(param, vals[state].second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
|
sealed class Select(
|
||||||
|
|
||||||
open class UriMultiSelectFilter(
|
|
||||||
name: String,
|
name: String,
|
||||||
private val param: String,
|
val param: String,
|
||||||
private val vals: Array<Pair<String, String>>,
|
private val valuesMap: Map<String, String>,
|
||||||
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
|
) : Filter.Select<String>(name, valuesMap.keys.toTypedArray()) {
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
open val selection: String
|
||||||
val checked = state.filter { it.state }
|
get() = valuesMap[values[state]]!!
|
||||||
|
|
||||||
checked.forEach {
|
|
||||||
builder.addQueryParameter(param, it.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open class UriTriSelectOption(name: String, val value: String) : Filter.TriState(name)
|
class TypeFilter : Group("Type", "type[]", types)
|
||||||
|
|
||||||
open class UriTriSelectFilter(
|
private val types: List<Entry>
|
||||||
name: String,
|
get() = listOf(
|
||||||
private val param: String,
|
Entry("Manga", "manga"),
|
||||||
private val vals: Array<Pair<String, String>>,
|
Entry("One-Shot", "one_shot"),
|
||||||
) : Filter.Group<UriTriSelectOption>(name, vals.map { UriTriSelectOption(it.first, it.second) }), UriFilter {
|
Entry("Doujinshi", "doujinshi"),
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
Entry("Light-Novel", "light_novel"),
|
||||||
state.forEach { s ->
|
Entry("Novel", "novel"),
|
||||||
when (s.state) {
|
Entry("Manhwa", "manhwa"),
|
||||||
TriState.STATE_INCLUDE -> builder.addQueryParameter(param, s.value)
|
Entry("Manhua", "manhua"),
|
||||||
TriState.STATE_EXCLUDE -> builder.addQueryParameter(param, "-${s.value}")
|
)
|
||||||
}
|
|
||||||
}
|
class Genre(name: String, val id: String) : Filter.TriState(name) {
|
||||||
}
|
val selection: String
|
||||||
|
get() = (if (isExcluded()) "-" else "") + id
|
||||||
}
|
}
|
||||||
|
|
||||||
class TypeFilter : UriMultiSelectFilter(
|
class GenresFilter : Filter.Group<Genre>("Genre", genres) {
|
||||||
"Type",
|
val param = "genre[]"
|
||||||
"type",
|
|
||||||
arrayOf(
|
|
||||||
Pair("Manga", "manga"),
|
|
||||||
Pair("One-Shot", "one_shot"),
|
|
||||||
Pair("Doujinshi", "doujinshi"),
|
|
||||||
Pair("Novel", "novel"),
|
|
||||||
Pair("Manhwa", "manhwa"),
|
|
||||||
Pair("Manhua", "manhua"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class GenreFilter : UriTriSelectFilter(
|
val combineMode: Boolean
|
||||||
"Genres",
|
get() = state.filter { !it.isIgnored() }.size > 1
|
||||||
"genre[]",
|
|
||||||
arrayOf(
|
|
||||||
Pair("Action", "1"),
|
|
||||||
Pair("Adventure", "78"),
|
|
||||||
Pair("Avant Garde", "3"),
|
|
||||||
Pair("Boys Love", "4"),
|
|
||||||
Pair("Comedy", "5"),
|
|
||||||
Pair("Demons", "77"),
|
|
||||||
Pair("Drama", "6"),
|
|
||||||
Pair("Ecchi", "7"),
|
|
||||||
Pair("Fantasy", "79"),
|
|
||||||
Pair("Girls Love", "9"),
|
|
||||||
Pair("Gourmet", "10"),
|
|
||||||
Pair("Harem", "11"),
|
|
||||||
Pair("Horror", "530"),
|
|
||||||
Pair("Isekai", "13"),
|
|
||||||
Pair("Iyashikei", "531"),
|
|
||||||
Pair("Josei", "15"),
|
|
||||||
Pair("Kids", "532"),
|
|
||||||
Pair("Magic", "539"),
|
|
||||||
Pair("Mahou Shoujo", "533"),
|
|
||||||
Pair("Martial Arts", "534"),
|
|
||||||
Pair("Mecha", "19"),
|
|
||||||
Pair("Military", "535"),
|
|
||||||
Pair("Music", "21"),
|
|
||||||
Pair("Mystery", "22"),
|
|
||||||
Pair("Parody", "23"),
|
|
||||||
Pair("Psychological", "536"),
|
|
||||||
Pair("Reverse Harem", "25"),
|
|
||||||
Pair("Romance", "26"),
|
|
||||||
Pair("School", "73"),
|
|
||||||
Pair("Sci-Fi", "28"),
|
|
||||||
Pair("Seinen", "537"),
|
|
||||||
Pair("Shoujo", "30"),
|
|
||||||
Pair("Shounen", "31"),
|
|
||||||
Pair("Slice of Life", "538"),
|
|
||||||
Pair("Space", "33"),
|
|
||||||
Pair("Sports", "34"),
|
|
||||||
Pair("Super Power", "75"),
|
|
||||||
Pair("Supernatural", "76"),
|
|
||||||
Pair("Suspense", "37"),
|
|
||||||
Pair("Thriller", "38"),
|
|
||||||
Pair("Vampire", "39"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class GenreModeFilter : Filter.CheckBox("Must have all the selected genres"), UriFilter {
|
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
|
||||||
if (state) {
|
|
||||||
builder.addQueryParameter("genre_mode", "and")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class StatusFilter : UriMultiSelectFilter(
|
private val genres: List<Genre>
|
||||||
"Status",
|
get() = listOf(
|
||||||
"status[]",
|
Genre("Action", "1"),
|
||||||
arrayOf(
|
Genre("Adventure", "78"),
|
||||||
Pair("Completed", "completed"),
|
Genre("Avant Garde", "3"),
|
||||||
Pair("Releasing", "releasing"),
|
Genre("Boys Love", "4"),
|
||||||
Pair("On Hiatus", "on_hiatus"),
|
Genre("Comedy", "5"),
|
||||||
Pair("Discontinued", "discontinued"),
|
Genre("Demons", "77"),
|
||||||
Pair("Not Yet Published", "info"),
|
Genre("Drama", "6"),
|
||||||
),
|
Genre("Ecchi", "7"),
|
||||||
)
|
Genre("Fantasy", "79"),
|
||||||
|
Genre("Girls Love", "9"),
|
||||||
|
Genre("Gourmet", "10"),
|
||||||
|
Genre("Harem", "11"),
|
||||||
|
Genre("Horror", "530"),
|
||||||
|
Genre("Isekai", "13"),
|
||||||
|
Genre("Iyashikei", "531"),
|
||||||
|
Genre("Josei", "15"),
|
||||||
|
Genre("Kids", "532"),
|
||||||
|
Genre("Magic", "539"),
|
||||||
|
Genre("Mahou Shoujo", "533"),
|
||||||
|
Genre("Martial Arts", "534"),
|
||||||
|
Genre("Mecha", "19"),
|
||||||
|
Genre("Military", "535"),
|
||||||
|
Genre("Music", "21"),
|
||||||
|
Genre("Mystery", "22"),
|
||||||
|
Genre("Parody", "23"),
|
||||||
|
Genre("Psychological", "536"),
|
||||||
|
Genre("Reverse Harem", "25"),
|
||||||
|
Genre("Romance", "26"),
|
||||||
|
Genre("School", "73"),
|
||||||
|
Genre("Sci-Fi", "28"),
|
||||||
|
Genre("Seinen", "537"),
|
||||||
|
Genre("Shoujo", "30"),
|
||||||
|
Genre("Shounen", "31"),
|
||||||
|
Genre("Slice of Life", "538"),
|
||||||
|
Genre("Space", "33"),
|
||||||
|
Genre("Sports", "34"),
|
||||||
|
Genre("Super Power", "75"),
|
||||||
|
Genre("Supernatural", "76"),
|
||||||
|
Genre("Suspense", "37"),
|
||||||
|
Genre("Thriller", "38"),
|
||||||
|
Genre("Vampire", "39"),
|
||||||
|
)
|
||||||
|
|
||||||
class YearFilter : UriMultiSelectFilter(
|
class StatusFilter : Group("Status", "status[]", statuses)
|
||||||
"Year",
|
|
||||||
"year[]",
|
|
||||||
years,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
private val currentYear by lazy {
|
|
||||||
Calendar.getInstance()[Calendar.YEAR]
|
|
||||||
}
|
|
||||||
|
|
||||||
private val years: Array<Pair<String, String>> = buildList(29) {
|
private val statuses: List<Entry>
|
||||||
addAll(
|
get() = listOf(
|
||||||
(currentYear downTo (currentYear - 20)).map(Int::toString),
|
Entry("Completed", "completed"),
|
||||||
)
|
Entry("Releasing", "releasing"),
|
||||||
|
Entry("On Hiatus", "on_hiatus"),
|
||||||
|
Entry("Discontinued", "discontinued"),
|
||||||
|
Entry("Not Yet Published", "info"),
|
||||||
|
)
|
||||||
|
|
||||||
addAll(
|
class YearFilter : Group("Year", "year[]", years)
|
||||||
(2000 downTo 1930 step 10).map { "${it}s" },
|
|
||||||
)
|
|
||||||
}.map { Pair(it, it) }.toTypedArray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MinChapterFilter : Filter.Text("Minimum chapter length"), UriFilter {
|
private val years: List<Entry>
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
get() = listOf(
|
||||||
if (state.isNotEmpty()) {
|
Entry("2023"),
|
||||||
val value = state.toIntOrNull()?.takeIf { it > 0 }
|
Entry("2022"),
|
||||||
?: throw IllegalArgumentException("Minimum chapter length must be a positive integer greater than 0")
|
Entry("2021"),
|
||||||
|
Entry("2020"),
|
||||||
|
Entry("2019"),
|
||||||
|
Entry("2018"),
|
||||||
|
Entry("2017"),
|
||||||
|
Entry("2016"),
|
||||||
|
Entry("2015"),
|
||||||
|
Entry("2014"),
|
||||||
|
Entry("2013"),
|
||||||
|
Entry("2012"),
|
||||||
|
Entry("2011"),
|
||||||
|
Entry("2010"),
|
||||||
|
Entry("2009"),
|
||||||
|
Entry("2008"),
|
||||||
|
Entry("2007"),
|
||||||
|
Entry("2006"),
|
||||||
|
Entry("2005"),
|
||||||
|
Entry("2004"),
|
||||||
|
Entry("2003"),
|
||||||
|
Entry("2000s"),
|
||||||
|
Entry("1990s"),
|
||||||
|
Entry("1980s"),
|
||||||
|
Entry("1970s"),
|
||||||
|
Entry("1960s"),
|
||||||
|
Entry("1950s"),
|
||||||
|
Entry("1940s"),
|
||||||
|
)
|
||||||
|
|
||||||
builder.addQueryParameter("minchap", value.toString())
|
class ChapterCountFilter : Select("Chapter Count", "minchap", chapterCounts)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SortFilter(defaultValue: String? = null) : UriPartFilter(
|
private val chapterCounts
|
||||||
"Sort",
|
get() = mapOf(
|
||||||
"sort",
|
"Any" to "",
|
||||||
arrayOf(
|
"At least 1 chapter" to "1",
|
||||||
Pair("Most relevance", "most_relevance"),
|
"At least 3 chapters" to "3",
|
||||||
Pair("Recently updated", "recently_updated"),
|
"At least 5 chapters" to "5",
|
||||||
Pair("Recently added", "recently_added"),
|
"At least 10 chapters" to "10",
|
||||||
Pair("Release date", "release_date"),
|
"At least 20 chapters" to "20",
|
||||||
Pair("Trending", "trending"),
|
"At least 30 chapters" to "30",
|
||||||
Pair("Name A-Z", "title_az"),
|
"At least 50 chapters" to "50",
|
||||||
Pair("Scores", "scores"),
|
)
|
||||||
Pair("MAL scores", "mal_scores"),
|
|
||||||
Pair("Most viewed", "most_viewed"),
|
class SortFilter : Select("Sort", "sort", orders)
|
||||||
Pair("Most favourited", "most_favourited"),
|
|
||||||
),
|
private val orders
|
||||||
defaultValue,
|
get() = mapOf(
|
||||||
)
|
"Trending" to "trending",
|
||||||
|
"Recently updated" to "recently_updated",
|
||||||
|
"Recently added" to "recently_added",
|
||||||
|
"Release date" to "release_date",
|
||||||
|
"Name A-Z" to "title_az",
|
||||||
|
"Score" to "scores",
|
||||||
|
"MAL score" to "mal_scores",
|
||||||
|
"Most viewed" to "most_viewed",
|
||||||
|
"Most favourited" to "most_favourited",
|
||||||
|
)
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.mangafire
|
package eu.kanade.tachiyomi.extension.all.mangafire
|
||||||
|
|
||||||
import android.app.Application
|
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
|
||||||
import androidx.preference.PreferenceScreen
|
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
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.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@ -23,245 +18,182 @@ import okhttp3.Response
|
|||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import rx.Observable
|
import org.jsoup.select.Evaluator
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.ParseException
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class MangaFire(
|
open class MangaFire(
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
private val langCode: String = lang,
|
private val langCode: String = lang,
|
||||||
) : ConfigurableSource, HttpSource() {
|
) : MangaReader() {
|
||||||
override val name = "MangaFire"
|
override val name = "MangaFire"
|
||||||
|
|
||||||
override val baseUrl = "https://mangafire.to"
|
override val baseUrl = "https://mangafire.to"
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val preferences by lazy {
|
override val client = super.client.newBuilder()
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
|
.addInterceptor(ImageInterceptor)
|
||||||
}
|
.build()
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build()
|
override fun latestUpdatesRequest(page: Int) =
|
||||||
|
GET("$baseUrl/filter?sort=recently_updated&language[]=$langCode&page=$page", headers)
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
override fun popularMangaRequest(page: Int) =
|
||||||
|
GET("$baseUrl/filter?sort=most_viewed&language[]=$langCode&page=$page", headers)
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
return searchMangaRequest(
|
|
||||||
page,
|
|
||||||
"",
|
|
||||||
FilterList(SortFilter(defaultValue = "most_viewed")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
return searchMangaRequest(
|
|
||||||
page,
|
|
||||||
"",
|
|
||||||
FilterList(SortFilter(defaultValue = "recently_updated")),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
|
||||||
|
|
||||||
// =============================== Search ===============================
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
|
||||||
addPathSegment("filter")
|
if (query.isNotBlank()) {
|
||||||
|
urlBuilder.addPathSegment("filter").apply {
|
||||||
if (query.isNotBlank()) {
|
|
||||||
addQueryParameter("keyword", query)
|
addQueryParameter("keyword", query)
|
||||||
|
addQueryParameter("page", page.toString())
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
val filterList = filters.ifEmpty { getFilterList() }
|
urlBuilder.addPathSegment("filter").apply {
|
||||||
filterList.filterIsInstance<UriFilter>().forEach {
|
addQueryParameter("language[]", langCode)
|
||||||
it.addToUri(this)
|
addQueryParameter("page", page.toString())
|
||||||
}
|
filters.ifEmpty(::getFilterList).forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
addQueryParameter("language[]", langCode)
|
is Group -> {
|
||||||
addQueryParameter("page", page.toString())
|
filter.state.forEach {
|
||||||
}.build()
|
if (it.state) {
|
||||||
|
addQueryParameter(filter.param, it.id)
|
||||||
return GET(url, headers)
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
is Select -> {
|
||||||
val document = response.asJsoup()
|
addQueryParameter(filter.param, filter.selection)
|
||||||
var entries = document.select(searchMangaSelector()).map(::searchMangaFromElement)
|
}
|
||||||
if (preferences.getBoolean(SHOW_VOLUME_PREF, false)) {
|
is GenresFilter -> {
|
||||||
entries = entries.flatMapTo(ArrayList(entries.size * 2)) { manga ->
|
filter.state.forEach {
|
||||||
val volume = SManga.create().apply {
|
if (it.state != 0) {
|
||||||
url = manga.url + VOLUME_URL_SUFFIX
|
addQueryParameter(filter.param, it.selection)
|
||||||
title = VOLUME_TITLE_PREFIX + manga.title
|
}
|
||||||
thumbnail_url = manga.thumbnail_url
|
}
|
||||||
|
if (filter.combineMode) {
|
||||||
|
addQueryParameter("genre_mode", "and")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
listOf(manga, volume)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
|
return GET(urlBuilder.build(), headers)
|
||||||
return MangasPage(entries, hasNextPage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
|
override fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
|
||||||
|
|
||||||
private fun searchMangaSelector() = ".original.card-lg .unit .inner"
|
override fun searchMangaSelector() = ".original.card-lg .unit .inner"
|
||||||
|
|
||||||
private fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
override fun searchMangaFromElement(element: Element) =
|
||||||
element.selectFirst(".info > a")!!.let {
|
SManga.create().apply {
|
||||||
setUrlWithoutDomain(it.attr("href"))
|
element.selectFirst(".info > a")!!.let {
|
||||||
title = it.ownText()
|
setUrlWithoutDomain(it.attr("href"))
|
||||||
}
|
title = it.ownText()
|
||||||
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
|
}
|
||||||
}
|
element.selectFirst(Evaluator.Tag("img"))!!.let {
|
||||||
|
thumbnail_url = it.attr("src")
|
||||||
// =============================== Filters ==============================
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
TypeFilter(),
|
|
||||||
GenreFilter(),
|
|
||||||
GenreModeFilter(),
|
|
||||||
StatusFilter(),
|
|
||||||
YearFilter(),
|
|
||||||
MinChapterFilter(),
|
|
||||||
SortFilter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// =========================== Manga Details ============================
|
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX)
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
return mangaDetailsParse(response.asJsoup()).apply {
|
|
||||||
if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
|
|
||||||
title = VOLUME_TITLE_PREFIX + title
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
with(document.selectFirst(".main-inner:not(.manga-bottom)")!!) {
|
val root = document.selectFirst(".info")!!
|
||||||
title = selectFirst("h1")!!.text()
|
val mangaTitle = root.child(1).ownText()
|
||||||
thumbnail_url = selectFirst(".poster img")?.attr("src")
|
title = mangaTitle
|
||||||
status = selectFirst(".info > p").parseStatus()
|
description = document.run {
|
||||||
description = buildString {
|
val description = selectFirst(Evaluator.Class("description"))!!.ownText()
|
||||||
document.selectFirst("#synopsis .modal-content")?.textNodes()?.let {
|
when (val altTitle = root.child(2).ownText()) {
|
||||||
append(it.joinToString("\n\n"))
|
"", mangaTitle -> description
|
||||||
}
|
else -> "$description\n\nAlternative Title: $altTitle"
|
||||||
|
|
||||||
selectFirst("h6")?.let {
|
|
||||||
append("\n\nAlternative title: ${it.text()}")
|
|
||||||
}
|
|
||||||
}.trim()
|
|
||||||
|
|
||||||
selectFirst(".meta")?.let {
|
|
||||||
author = it.selectFirst("span:contains(Author:) + span")?.text()
|
|
||||||
val type = it.selectFirst("span:contains(Type:) + span")?.text()
|
|
||||||
val genres = it.selectFirst("span:contains(Genres:) + span")?.text()
|
|
||||||
genre = listOfNotNull(type, genres).joinToString()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
thumbnail_url = document.selectFirst(".poster")!!
|
||||||
|
.selectFirst("img")!!.attr("src")
|
||||||
|
status = when (root.child(0).ownText()) {
|
||||||
|
"Completed" -> SManga.COMPLETED
|
||||||
|
"Releasing" -> SManga.ONGOING
|
||||||
|
"On_hiatus" -> SManga.ON_HIATUS
|
||||||
|
"Discontinued" -> SManga.CANCELLED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
with(document.selectFirst(Evaluator.Class("meta"))!!) {
|
||||||
|
author = selectFirst("span:contains(Author:) + span")?.text()
|
||||||
|
val type = selectFirst("span:contains(Type:) + span")?.text()
|
||||||
|
val genres = selectFirst("span:contains(Genres:) + span")?.text()
|
||||||
|
genre = listOfNotNull(type, genres).joinToString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
override val chapterType get() = "chapter"
|
||||||
"releasing" -> SManga.ONGOING
|
override val volumeType get() = "volume"
|
||||||
"completed" -> SManga.COMPLETED
|
|
||||||
"on_hiatus" -> SManga.ON_HIATUS
|
override fun chapterListRequest(mangaUrl: String, type: String): Request {
|
||||||
"discontinued" -> SManga.CANCELLED
|
val id = mangaUrl.substringAfterLast('.')
|
||||||
else -> SManga.UNKNOWN
|
return GET("$baseUrl/ajax/manga/$id/$type/$langCode", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== Chapters ==============================
|
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
|
||||||
|
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
|
||||||
override fun getChapterUrl(chapter: SChapter): String {
|
val document = Jsoup.parse(result)
|
||||||
return baseUrl + chapter.url.substringBeforeLast("#")
|
val selector = if (isVolume) "div.unit" else "ul li"
|
||||||
}
|
val elements = document.select(selector)
|
||||||
|
if (elements.size > 0) {
|
||||||
private fun getAjaxRequest(ajaxType: String, mangaId: String, chapterType: String): Request {
|
val linkToFirstChapter = elements[0].selectFirst(Evaluator.Tag("a"))!!.attr("href")
|
||||||
return GET("$baseUrl/ajax/$ajaxType/$mangaId/$chapterType/$langCode", headers)
|
val mangaId = linkToFirstChapter.toString().substringAfter('.').substringBefore('/')
|
||||||
|
val type = if (isVolume) volumeType else chapterType
|
||||||
|
val request = GET("$baseUrl/ajax/read/$mangaId/$type/$langCode", headers)
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
val res = json.decodeFromString<ResponseDto<ChapterIdsDto>>(response.body.string()).result.html
|
||||||
|
val chapterInfoDocument = Jsoup.parse(res)
|
||||||
|
val chapters = chapterInfoDocument.select("ul li")
|
||||||
|
for ((i, it) in elements.withIndex()) {
|
||||||
|
it.attr("data-id", chapters[i].select("a").attr("data-id"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return elements.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class AjaxReadDto(
|
class ChapterIdsDto(
|
||||||
val html: String,
|
val html: String,
|
||||||
|
val title_format: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun updateChapterList(manga: SManga, chapters: List<SChapter>) {
|
||||||
throw UnsupportedOperationException()
|
val request = chapterListRequest(manga.url, chapterType)
|
||||||
}
|
val response = client.newCall(request).execute()
|
||||||
|
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
|
||||||
|
val document = Jsoup.parse(result)
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
val elements = document.selectFirst(".scroll-sm")!!.children()
|
||||||
val path = manga.url
|
val chapterCount = chapters.size
|
||||||
val mangaId = path.removeSuffix(VOLUME_URL_SUFFIX).substringAfterLast(".")
|
if (elements.size != chapterCount) throw Exception("Chapter count doesn't match. Try updating again.")
|
||||||
val isVolume = path.endsWith(VOLUME_URL_SUFFIX)
|
val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.US)
|
||||||
|
for (i in 0 until chapterCount) {
|
||||||
val type = if (isVolume) "volume" else "chapter"
|
val chapter = chapters[i]
|
||||||
val abbrPrefix = if (isVolume) "Vol" else "Chap"
|
val element = elements[i]
|
||||||
val fullPrefix = if (isVolume) "Volume" else "Chapter"
|
val number = element.attr("data-number").toFloatOrNull() ?: -1f
|
||||||
|
if (chapter.chapter_number != number) throw Exception("Chapter number doesn't match. Try updating again.")
|
||||||
val ajaxMangaList = client.newCall(getAjaxRequest("manga", mangaId, type))
|
chapter.name = element.select(Evaluator.Tag("span"))[0].ownText()
|
||||||
.execute().parseAs<ResponseDto<String>>().result
|
val date = element.select(Evaluator.Tag("span"))[1].ownText()
|
||||||
.toBodyFragment()
|
chapter.date_upload = try {
|
||||||
.select(if (isVolume) ".vol-list > .item" else "li")
|
dateFormat.parse(date)!!.time
|
||||||
|
} catch (_: Throwable) {
|
||||||
val ajaxReadList = client.newCall(getAjaxRequest("read", mangaId, type))
|
0
|
||||||
.execute().parseAs<ResponseDto<AjaxReadDto>>().result.html
|
|
||||||
.toBodyFragment()
|
|
||||||
.select("ul a")
|
|
||||||
|
|
||||||
val chapterList = ajaxMangaList.zip(ajaxReadList) { m, r ->
|
|
||||||
val link = r.selectFirst("a")!!
|
|
||||||
if (!r.attr("abs:href").toHttpUrl().pathSegments.last().contains(type)) {
|
|
||||||
return Observable.just(emptyList())
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(m.attr("data-number") == r.attr("data-number")) {
|
|
||||||
"Chapter count doesn't match. Try updating again."
|
|
||||||
}
|
|
||||||
|
|
||||||
val number = m.attr("data-number")
|
|
||||||
val dateStr = m.select("span").getOrNull(1)?.text() ?: ""
|
|
||||||
|
|
||||||
SChapter.create().apply {
|
|
||||||
setUrlWithoutDomain("${link.attr("href")}#$type/${r.attr("data-id")}")
|
|
||||||
chapter_number = number.toFloatOrNull() ?: -1f
|
|
||||||
name = run {
|
|
||||||
val name = link.text()
|
|
||||||
val prefix = "$abbrPrefix $number: "
|
|
||||||
if (!name.startsWith(prefix)) return@run name
|
|
||||||
val realName = name.removePrefix(prefix)
|
|
||||||
if (realName.contains(number)) realName else "$fullPrefix $number: $realName"
|
|
||||||
}
|
|
||||||
|
|
||||||
date_upload = try {
|
|
||||||
dateFormat.parse(dateStr)!!.time
|
|
||||||
} catch (_: ParseException) {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Observable.just(chapterList)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================== Pages ================================
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
val typeAndId = chapter.url.substringAfterLast('#')
|
val typeAndId = chapter.url.substringAfterLast('#')
|
||||||
return GET("$baseUrl/ajax/read/$typeAndId", headers)
|
return GET("$baseUrl/ajax/read/$typeAndId", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val result = response.parseAs<ResponseDto<PageListDto>>().result
|
val result = json.decodeFromString<ResponseDto<PageListDto>>(response.body.string()).result
|
||||||
|
|
||||||
return result.pages.mapIndexed { index, image ->
|
return result.pages.mapIndexed { index, image ->
|
||||||
val url = image.url
|
val url = image.url
|
||||||
@ -274,49 +206,27 @@ class MangaFire(
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class PageListDto(private val images: List<List<JsonPrimitive>>) {
|
class PageListDto(private val images: List<List<JsonPrimitive>>) {
|
||||||
val pages
|
val pages get() = images.map {
|
||||||
get() = images.map {
|
Image(it[0].content, it[2].int)
|
||||||
Image(it[0].content, it[2].int)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Image(val url: String, val offset: Int)
|
class Image(val url: String, val offset: Int)
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String {
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================ Preferences =============================
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = SHOW_VOLUME_PREF
|
|
||||||
title = "Show volume entries in search result"
|
|
||||||
setDefaultValue(false)
|
|
||||||
}.let(screen::addPreference)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class ResponseDto<T>(
|
class ResponseDto<T>(
|
||||||
val result: T,
|
val result: T,
|
||||||
|
val status: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T {
|
override fun getFilterList() =
|
||||||
return json.decodeFromString(body.string())
|
FilterList(
|
||||||
}
|
Filter.Header("NOTE: Ignored if using text search!"),
|
||||||
|
Filter.Separator(),
|
||||||
private fun String.toBodyFragment(): Document {
|
TypeFilter(),
|
||||||
return Jsoup.parseBodyFragment(this, baseUrl)
|
GenresFilter(),
|
||||||
}
|
StatusFilter(),
|
||||||
|
YearFilter(),
|
||||||
companion object {
|
ChapterCountFilter(),
|
||||||
private val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.US)
|
SortFilter(),
|
||||||
private const val SHOW_VOLUME_PREF = "show_volume"
|
)
|
||||||
|
|
||||||
private const val VOLUME_URL_FRAGMENT = "vol"
|
|
||||||
private const val VOLUME_URL_SUFFIX = "#$VOLUME_URL_FRAGMENT"
|
|
||||||
private const val VOLUME_TITLE_PREFIX = "[VOL] "
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name=".all.mangahosted.MangaHostedUrlActivity"
|
|
||||||
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:scheme="https"
|
|
||||||
android:host="mangahosted.org"
|
|
||||||
android:pathPattern="/.*/..*" />
|
|
||||||
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 12 KiB |
@ -1,202 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.mangahosted
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
||||||
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 kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
|
|
||||||
|
|
||||||
override val lang = langOption.lang
|
|
||||||
|
|
||||||
override val name: String = "Manga Hosted${langOption.nameSuffix}"
|
|
||||||
|
|
||||||
override val baseUrl: String = "https://mangahosted.org"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
override val client = network.client.newBuilder()
|
|
||||||
.rateLimit(2)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
|
||||||
.set("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
// ================================= Popular ==========================================
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
val maxResult = 24
|
|
||||||
return GET("$apiUrl/${langOption.infix}/HomeTopFllow/$maxResult/${page - 1}")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
val dto = response.parseAs<Pageable<MangaDto>>()
|
|
||||||
val mangas = dto.data.map(::mangaParse)
|
|
||||||
return MangasPage(
|
|
||||||
mangas = mangas,
|
|
||||||
hasNextPage = dto.hasNextPage(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================= Latest ===========================================
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
val maxResult = 24
|
|
||||||
val url = "$apiUrl/${langOption.infix}/HomeLastUpdate".toHttpUrl().newBuilder()
|
|
||||||
.addPathSegment("$maxResult")
|
|
||||||
.addPathSegment("${page - 1}")
|
|
||||||
.build()
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
|
||||||
|
|
||||||
// ================================= Search ===========================================
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val maxResult = 20
|
|
||||||
val url = "$apiUrl/${langOption.infix}/SeachPage/$maxResult/${page - 1}".toHttpUrl().newBuilder()
|
|
||||||
.addPathSegment(query)
|
|
||||||
.build()
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
if (query.startsWith(SEARCH_PREFIX)) {
|
|
||||||
val url = "$baseUrl/${langOption.infix}/${query.substringAfter(SEARCH_PREFIX)}"
|
|
||||||
return client.newCall(GET(url, headers))
|
|
||||||
.asObservableSuccess().map { response ->
|
|
||||||
val mangas = try { listOf(mangaDetailsParse(response)) } catch (_: Exception) { emptyList() }
|
|
||||||
MangasPage(mangas, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.fetchSearchManga(page, query, filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val dto = response.parseAs<SearchDto>()
|
|
||||||
return MangasPage(
|
|
||||||
dto.mangas.map(::mangaParse),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================= Details ==========================================
|
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
|
||||||
val url = "$apiUrl/${langOption.infix}/getInfoManga".toHttpUrl().newBuilder()
|
|
||||||
.addPathSegment(manga.slug())
|
|
||||||
.build()
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
val dto = response.parseAs<MangaDetailsDto>()
|
|
||||||
return mangaParse(dto.details)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga): String {
|
|
||||||
return baseUrl + manga.url.replace(langOption.infix, langOption.mangaSubstring)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================= Chapter ==========================================
|
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
val chapters = mutableListOf<SChapter>()
|
|
||||||
var currentPage = 0
|
|
||||||
do {
|
|
||||||
val chaptersDto = fetchChapterListPageable(manga, currentPage++)
|
|
||||||
chapters += chaptersDto.data.map { chapter ->
|
|
||||||
SChapter.create().apply {
|
|
||||||
name = chapter.name
|
|
||||||
date_upload = chapter.date.toDate()
|
|
||||||
url = chapter.toChapterUrl(langOption.infix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (chaptersDto.hasNextPage())
|
|
||||||
return Observable.just(chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchChapterListPageable(manga: SManga, page: Int): Pageable<ChapterDto> {
|
|
||||||
val maxResult = 100
|
|
||||||
val url = "$apiUrl/${langOption.infix}/GetChapterListFilter/${manga.slug()}/$maxResult/$page/all/${langOption.orderBy}"
|
|
||||||
return client.newCall(GET(url, headers)).execute()
|
|
||||||
.parseAs<Pageable<ChapterDto>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
// ================================= Pages ============================================
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
val chapterSlug = chapter.url.substringAfter(langOption.infix)
|
|
||||||
val url = "$apiUrl/${langOption.infix}/GetImageChapter$chapterSlug"
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageRequest(page: Page): Request {
|
|
||||||
val imageHeaders = headers.newBuilder()
|
|
||||||
.set("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
|
|
||||||
.removeAll("Referer")
|
|
||||||
.build()
|
|
||||||
return super.imageRequest(page).newBuilder()
|
|
||||||
.headers(imageHeaders)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val location = response.request.url.toString()
|
|
||||||
val dto = response.parseAs<PageDto>()
|
|
||||||
return dto.pages.mapIndexed { index, url ->
|
|
||||||
Page(index, location, imageUrl = url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String = ""
|
|
||||||
|
|
||||||
// ================================= Utilities =======================================
|
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T {
|
|
||||||
return json.decodeFromString(body.string())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun SManga.slug() = this.url.split("/").last()
|
|
||||||
|
|
||||||
private fun mangaParse(dto: MangaDto): SManga {
|
|
||||||
return SManga.create().apply {
|
|
||||||
title = dto.title
|
|
||||||
thumbnail_url = dto.thumbnailUrl
|
|
||||||
status = dto.status
|
|
||||||
url = "/${langOption.infix}/${dto.slug}"
|
|
||||||
genre = dto.genres
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.toDate(): Long =
|
|
||||||
try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val SEARCH_PREFIX = "slug:"
|
|
||||||
val baseApiUrl = "https://api.novelfull.us"
|
|
||||||
val apiUrl = "$baseApiUrl/api"
|
|
||||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.mangahosted
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class MangaDetailsDto(private val data: Props) {
|
|
||||||
val details: MangaDto get() = data.details
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Props(
|
|
||||||
@SerialName("infoDoc") val details: MangaDto,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
open class Pageable<T>(
|
|
||||||
var currentPage: Int,
|
|
||||||
var totalPage: Int,
|
|
||||||
val data: List<T>,
|
|
||||||
) {
|
|
||||||
fun hasNextPage() = (currentPage + 1) <= totalPage
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class ChapterDto(
|
|
||||||
val date: String,
|
|
||||||
@SerialName("idDoc") val slugManga: String,
|
|
||||||
@SerialName("idDetail") val id: String,
|
|
||||||
@SerialName("nameChapter") val name: String,
|
|
||||||
) {
|
|
||||||
fun toChapterUrl(lang: String) = "/$lang/${this.slugManga}/$id"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class MangaDto(
|
|
||||||
@SerialName("name") val title: String,
|
|
||||||
@SerialName("image") private val _thumbnailUrl: String,
|
|
||||||
@SerialName("idDoc") val slug: String,
|
|
||||||
@SerialName("genresName") val genres: String,
|
|
||||||
@SerialName("status") val _status: String,
|
|
||||||
) {
|
|
||||||
val thumbnailUrl get() = "${MangaHosted.baseApiUrl}$_thumbnailUrl"
|
|
||||||
|
|
||||||
val status get() = when (_status) {
|
|
||||||
"ongoing" -> SManga.ONGOING
|
|
||||||
"completed" -> SManga.COMPLETED
|
|
||||||
else -> SManga.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class SearchDto(
|
|
||||||
@SerialName("data")
|
|
||||||
val mangas: List<MangaDto>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class PageDto(val `data`: Data) {
|
|
||||||
val pages: List<String> get() = `data`.detailDocuments.source.split("#")
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Data(@SerialName("detail_documents") val detailDocuments: DetailDocuments)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class DetailDocuments(val source: String)
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.mangahosted
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
|
|
||||||
class MangaHostedFactory : SourceFactory {
|
|
||||||
override fun createSources(): List<Source> = languages.map { MangaHosted(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
class LanguageOption(
|
|
||||||
val lang: String,
|
|
||||||
val infix: String = lang,
|
|
||||||
val mangaSubstring: String = infix,
|
|
||||||
val nameSuffix: String = "",
|
|
||||||
val orderBy: String = "DESC",
|
|
||||||
)
|
|
||||||
|
|
||||||
val languages = listOf(
|
|
||||||
LanguageOption("en", "manga", "scan"),
|
|
||||||
LanguageOption("en", "manga-v2", "kaka", " v2"),
|
|
||||||
LanguageOption("en", "comic", "comic-dc", " Comics"),
|
|
||||||
LanguageOption("es", "manga-spanish", "manga-es"),
|
|
||||||
LanguageOption("id", "manga-indo", "id"),
|
|
||||||
LanguageOption("it", "manga-italia", "manga-it"),
|
|
||||||
LanguageOption("ja", "mangaraw", "raw"),
|
|
||||||
LanguageOption("pt-BR", "manga-br", orderBy = "ASC"),
|
|
||||||
LanguageOption("ru", "manga-ru", "mangaru"),
|
|
||||||
LanguageOption("ru", "manga-ru-hentai", "hentai", " +18"),
|
|
||||||
LanguageOption("ru", "manga-ru-yaoi", "yaoi", " +18 Yaoi"),
|
|
||||||
)
|
|
@ -1,7 +1,3 @@
|
|||||||
## 1.4.7
|
|
||||||
|
|
||||||
- Reworked the lib-multisrc theme
|
|
||||||
|
|
||||||
## 1.3.4
|
## 1.3.4
|
||||||
|
|
||||||
- Refactor and make multisrc
|
- Refactor and make multisrc
|
||||||
|
@ -3,7 +3,7 @@ ext {
|
|||||||
extClass = '.MangaReaderFactory'
|
extClass = '.MangaReaderFactory'
|
||||||
themePkg = 'mangareader'
|
themePkg = 'mangareader'
|
||||||
baseUrl = 'https://mangareader.to'
|
baseUrl = 'https://mangareader.to'
|
||||||
overrideVersionCode = 5
|
overrideVersionCode = 4
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,208 +1,247 @@
|
|||||||
package eu.kanade.tachiyomi.extension.all.mangareaderto
|
package eu.kanade.tachiyomi.extension.all.mangareaderto
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader.UriFilter
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader.UriMultiSelectFilter
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader.UriPartFilter
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
|
||||||
class TypeFilter : UriPartFilter(
|
object Note : Filter.Header("NOTE: Ignored if using text search!")
|
||||||
"Type",
|
|
||||||
"type",
|
|
||||||
arrayOf(
|
|
||||||
Pair("All", ""),
|
|
||||||
Pair("Manga", "1"),
|
|
||||||
Pair("One-Shot", "2"),
|
|
||||||
Pair("Doujinshi", "3"),
|
|
||||||
Pair("Light Novel", "4"),
|
|
||||||
Pair("Manhwa", "5"),
|
|
||||||
Pair("Manhua", "6"),
|
|
||||||
Pair("Comic", "7"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class StatusFilter : UriPartFilter(
|
sealed class Select(
|
||||||
"Status",
|
name: String,
|
||||||
"status",
|
val param: String,
|
||||||
arrayOf(
|
values: Array<String>,
|
||||||
Pair("All", ""),
|
) : Filter.Select<String>(name, values) {
|
||||||
Pair("Finished", "1"),
|
open val selection: String
|
||||||
Pair("Publishing", "2"),
|
get() = if (state == 0) "" else state.toString()
|
||||||
Pair("On Hiatus", "3"),
|
}
|
||||||
Pair("Discontinued", "4"),
|
|
||||||
Pair("Not yet published", "5"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class RatingFilter : UriPartFilter(
|
class TypeFilter(
|
||||||
"Rating Type",
|
values: Array<String> = types,
|
||||||
"rating_type",
|
) : Select("Type", "type", values) {
|
||||||
arrayOf(
|
companion object {
|
||||||
Pair("All", ""),
|
private val types: Array<String>
|
||||||
Pair("G - All Ages", "1"),
|
get() = arrayOf(
|
||||||
Pair("PG - Children", "2"),
|
"All",
|
||||||
Pair("PG-13 - Teens 13 or older", "3"),
|
"Manga",
|
||||||
Pair("R - 17+ (violence & profanity)", "4"),
|
"One-Shot",
|
||||||
Pair("R+ - Mild Nudity", "5"),
|
"Doujinshi",
|
||||||
Pair("Rx - Hentai", "6"),
|
"Light Novel",
|
||||||
),
|
"Manhwa",
|
||||||
)
|
"Manhua",
|
||||||
|
"Comic",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ScoreFilter : UriPartFilter(
|
class StatusFilter(
|
||||||
"Score",
|
values: Array<String> = statuses,
|
||||||
"score",
|
) : Select("Status", "status", values) {
|
||||||
arrayOf(
|
companion object {
|
||||||
Pair("All", ""),
|
private val statuses: Array<String>
|
||||||
Pair("(1) Appalling", "1"),
|
get() = arrayOf(
|
||||||
Pair("(2) Horrible", "2"),
|
"All",
|
||||||
Pair("(3) Very Bad", "3"),
|
"Finished",
|
||||||
Pair("(4) Bad", "4"),
|
"Publishing",
|
||||||
Pair("(5) Average", "5"),
|
"On Hiatus",
|
||||||
Pair("(6) Fine", "6"),
|
"Discontinued",
|
||||||
Pair("(7) Good", "7"),
|
"Not yet published",
|
||||||
Pair("(8) Very Good", "8"),
|
)
|
||||||
Pair("(9) Great", "9"),
|
}
|
||||||
Pair("(10) Masterpiece", "10"),
|
}
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class YearFilter(name: String, param: String) : UriPartFilter(
|
class RatingFilter(
|
||||||
name,
|
values: Array<String> = ratings,
|
||||||
param,
|
) : Select("Rating Type", "rating_type", values) {
|
||||||
years,
|
companion object {
|
||||||
) {
|
private val ratings: Array<String>
|
||||||
|
get() = arrayOf(
|
||||||
|
"All",
|
||||||
|
"G - All Ages",
|
||||||
|
"PG - Children",
|
||||||
|
"PG-13 - Teens 13 or older",
|
||||||
|
"R - 17+ (violence & profanity)",
|
||||||
|
"R+ - Mild Nudity",
|
||||||
|
"Rx - Hentai",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScoreFilter(
|
||||||
|
values: Array<String> = scores,
|
||||||
|
) : Select("Score", "score", values) {
|
||||||
|
companion object {
|
||||||
|
private val scores: Array<String>
|
||||||
|
get() = arrayOf(
|
||||||
|
"All",
|
||||||
|
"(1) Appalling",
|
||||||
|
"(2) Horrible",
|
||||||
|
"(3) Very Bad",
|
||||||
|
"(4) Bad",
|
||||||
|
"(5) Average",
|
||||||
|
"(6) Fine",
|
||||||
|
"(7) Good",
|
||||||
|
"(8) Very Good",
|
||||||
|
"(9) Great",
|
||||||
|
"(10) Masterpiece",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class DateSelect(
|
||||||
|
name: String,
|
||||||
|
param: String,
|
||||||
|
values: Array<String>,
|
||||||
|
) : Select(name, param, values) {
|
||||||
|
override val selection: String
|
||||||
|
get() = if (state == 0) "" else values[state]
|
||||||
|
}
|
||||||
|
|
||||||
|
class YearFilter(
|
||||||
|
param: String,
|
||||||
|
values: Array<String> = years,
|
||||||
|
) : DateSelect("Year", param, values) {
|
||||||
companion object {
|
companion object {
|
||||||
private val nextYear by lazy {
|
private val nextYear by lazy {
|
||||||
Calendar.getInstance()[Calendar.YEAR] + 1
|
Calendar.getInstance()[Calendar.YEAR] + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private val years = Array(nextYear - 1916) { year ->
|
private val years: Array<String>
|
||||||
if (year == 0) {
|
get() = Array(nextYear - 1916) {
|
||||||
Pair("Any", "")
|
if (it == 0) "Any" else (nextYear - it).toString()
|
||||||
} else {
|
|
||||||
(nextYear - year).toString().let { Pair(it, it) }
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MonthFilter(name: String, param: String) : UriPartFilter(
|
class MonthFilter(
|
||||||
name,
|
param: String,
|
||||||
param,
|
values: Array<String> = months,
|
||||||
months,
|
) : DateSelect("Month", param, values) {
|
||||||
) {
|
|
||||||
companion object {
|
companion object {
|
||||||
private val months = Array(13) { months ->
|
private val months: Array<String>
|
||||||
if (months == 0) {
|
get() = Array(13) {
|
||||||
Pair("Any", "")
|
if (it == 0) "Any" else "%02d".format(it)
|
||||||
} else {
|
|
||||||
Pair("%02d".format(months), months.toString())
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DayFilter(name: String, param: String) : UriPartFilter(
|
class DayFilter(
|
||||||
name,
|
param: String,
|
||||||
param,
|
values: Array<String> = days,
|
||||||
days,
|
) : DateSelect("Day", param, values) {
|
||||||
) {
|
|
||||||
companion object {
|
companion object {
|
||||||
private val days = Array(32) { day ->
|
private val days: Array<String>
|
||||||
if (day == 0) {
|
get() = Array(32) {
|
||||||
Pair("Any", "")
|
if (it == 0) "Any" else "%02d".format(it)
|
||||||
} else {
|
|
||||||
Pair("%02d".format(day), day.toString())
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class DateFilter(
|
sealed class DateFilter(
|
||||||
type: String,
|
type: String,
|
||||||
private val values: List<UriPartFilter>,
|
values: List<DateSelect>,
|
||||||
) : Filter.Group<UriPartFilter>("$type Date", values), UriFilter {
|
) : Filter.Group<DateSelect>("$type Date", values)
|
||||||
override fun addToUri(builder: HttpUrl.Builder) {
|
|
||||||
values.forEach {
|
|
||||||
it.addToUri(builder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class StartDateFilter(
|
class StartDateFilter(
|
||||||
values: List<UriPartFilter> = parts,
|
values: List<DateSelect> = parts,
|
||||||
) : DateFilter("Start", values) {
|
) : DateFilter("Start", values) {
|
||||||
companion object {
|
companion object {
|
||||||
private val parts = listOf(
|
private val parts: List<DateSelect>
|
||||||
YearFilter("Year", "sy"),
|
get() = listOf(
|
||||||
MonthFilter("Month", "sm"),
|
YearFilter("sy"),
|
||||||
DayFilter("Day", "sd"),
|
MonthFilter("sm"),
|
||||||
)
|
DayFilter("sd"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EndDateFilter(
|
class EndDateFilter(
|
||||||
values: List<UriPartFilter> = parts,
|
values: List<DateSelect> = parts,
|
||||||
) : DateFilter("End", values) {
|
) : DateFilter("End", values) {
|
||||||
companion object {
|
companion object {
|
||||||
private val parts = listOf(
|
private val parts: List<DateSelect>
|
||||||
YearFilter("Year", "ey"),
|
get() = listOf(
|
||||||
MonthFilter("Month", "em"),
|
YearFilter("ey"),
|
||||||
DayFilter("Day", "ed"),
|
MonthFilter("em"),
|
||||||
|
DayFilter("ed"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SortFilter(
|
||||||
|
values: Array<String> = orders.keys.toTypedArray(),
|
||||||
|
) : Select("Sort", "sort", values) {
|
||||||
|
override val selection: String
|
||||||
|
get() = orders[values[state]]!!
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val orders = mapOf(
|
||||||
|
"Default" to "default",
|
||||||
|
"Latest Updated" to "latest-updated",
|
||||||
|
"Score" to "score",
|
||||||
|
"Name A-Z" to "name-az",
|
||||||
|
"Release Date" to "release-date",
|
||||||
|
"Most Viewed" to "most-viewed",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenreFilter : UriMultiSelectFilter(
|
class Genre(name: String, val id: String) : Filter.CheckBox(name)
|
||||||
"Genres",
|
|
||||||
"genres",
|
class GenresFilter(
|
||||||
arrayOf(
|
values: List<Genre> = genres,
|
||||||
Pair("Action", "1"),
|
) : Filter.Group<Genre>("Genres", values) {
|
||||||
Pair("Adventure", "2"),
|
val param = "genres"
|
||||||
Pair("Cars", "3"),
|
|
||||||
Pair("Comedy", "4"),
|
val selection: String
|
||||||
Pair("Dementia", "5"),
|
get() = state.filter { it.state }.joinToString(",") { it.id }
|
||||||
Pair("Demons", "6"),
|
|
||||||
Pair("Doujinshi", "7"),
|
companion object {
|
||||||
Pair("Drama", "8"),
|
private val genres: List<Genre>
|
||||||
Pair("Ecchi", "9"),
|
get() = listOf(
|
||||||
Pair("Fantasy", "10"),
|
Genre("Action", "1"),
|
||||||
Pair("Game", "11"),
|
Genre("Adventure", "2"),
|
||||||
Pair("Gender Bender", "12"),
|
Genre("Cars", "3"),
|
||||||
Pair("Harem", "13"),
|
Genre("Comedy", "4"),
|
||||||
Pair("Hentai", "14"),
|
Genre("Dementia", "5"),
|
||||||
Pair("Historical", "15"),
|
Genre("Demons", "6"),
|
||||||
Pair("Horror", "16"),
|
Genre("Doujinshi", "7"),
|
||||||
Pair("Josei", "17"),
|
Genre("Drama", "8"),
|
||||||
Pair("Kids", "18"),
|
Genre("Ecchi", "9"),
|
||||||
Pair("Magic", "19"),
|
Genre("Fantasy", "10"),
|
||||||
Pair("Martial Arts", "20"),
|
Genre("Game", "11"),
|
||||||
Pair("Mecha", "21"),
|
Genre("Gender Bender", "12"),
|
||||||
Pair("Military", "22"),
|
Genre("Harem", "13"),
|
||||||
Pair("Music", "23"),
|
Genre("Hentai", "14"),
|
||||||
Pair("Mystery", "24"),
|
Genre("Historical", "15"),
|
||||||
Pair("Parody", "25"),
|
Genre("Horror", "16"),
|
||||||
Pair("Police", "26"),
|
Genre("Josei", "17"),
|
||||||
Pair("Psychological", "27"),
|
Genre("Kids", "18"),
|
||||||
Pair("Romance", "28"),
|
Genre("Magic", "19"),
|
||||||
Pair("Samurai", "29"),
|
Genre("Martial Arts", "20"),
|
||||||
Pair("School", "30"),
|
Genre("Mecha", "21"),
|
||||||
Pair("Sci-Fi", "31"),
|
Genre("Military", "22"),
|
||||||
Pair("Seinen", "32"),
|
Genre("Music", "23"),
|
||||||
Pair("Shoujo", "33"),
|
Genre("Mystery", "24"),
|
||||||
Pair("Shoujo Ai", "34"),
|
Genre("Parody", "25"),
|
||||||
Pair("Shounen", "35"),
|
Genre("Police", "26"),
|
||||||
Pair("Shounen Ai", "36"),
|
Genre("Psychological", "27"),
|
||||||
Pair("Slice of Life", "37"),
|
Genre("Romance", "28"),
|
||||||
Pair("Space", "38"),
|
Genre("Samurai", "29"),
|
||||||
Pair("Sports", "39"),
|
Genre("School", "30"),
|
||||||
Pair("Super Power", "40"),
|
Genre("Sci-Fi", "31"),
|
||||||
Pair("Supernatural", "41"),
|
Genre("Seinen", "32"),
|
||||||
Pair("Thriller", "42"),
|
Genre("Shoujo", "33"),
|
||||||
Pair("Vampire", "43"),
|
Genre("Shoujo Ai", "34"),
|
||||||
Pair("Yaoi", "44"),
|
Genre("Shounen", "35"),
|
||||||
Pair("Yuri", "45"),
|
Genre("Shounen Ai", "36"),
|
||||||
),
|
Genre("Slice of Life", "37"),
|
||||||
",",
|
Genre("Space", "38"),
|
||||||
)
|
Genre("Sports", "39"),
|
||||||
|
Genre("Super Power", "40"),
|
||||||
|
Genre("Supernatural", "41"),
|
||||||
|
Genre("Thriller", "42"),
|
||||||
|
Genre("Vampire", "43"),
|
||||||
|
Genre("Yaoi", "44"),
|
||||||
|
Genre("Yuri", "45"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|