Compare commits
180 Commits
dfb448cbff
...
2cfdda0bcf
Author | SHA1 | Date |
---|---|---|
![]() |
2cfdda0bcf | |
![]() |
d4c94d0972 | |
![]() |
027d09ffd9 | |
![]() |
8b86222f80 | |
![]() |
7210f658e2 | |
![]() |
fd194f60f1 | |
![]() |
611e25e161 | |
![]() |
6e646630d1 | |
![]() |
82536cc63a | |
![]() |
574c64db71 | |
![]() |
7bf465ddae | |
![]() |
46b8ceaecf | |
![]() |
2deca0aaeb | |
![]() |
8ac307c66e | |
![]() |
d910910223 | |
![]() |
662da8f9fe | |
![]() |
6149261e7b | |
![]() |
0431f55f9b | |
![]() |
2a707c1072 | |
![]() |
1585a295a3 | |
![]() |
d34316685b | |
![]() |
6c1e55053c | |
![]() |
63558a4cdb | |
![]() |
10aa286c04 | |
![]() |
94603b3843 | |
![]() |
6fa43de2c5 | |
![]() |
b347869525 | |
![]() |
69268fc1e3 | |
![]() |
3a3cdded16 | |
![]() |
165c752b5b | |
![]() |
936331464b | |
![]() |
c9bd7e0825 | |
![]() |
7c55e0b5ae | |
![]() |
b7acdd9390 | |
![]() |
1455398b8f | |
![]() |
e079bf1f88 | |
![]() |
7408c2e05a | |
![]() |
78a055915d | |
![]() |
ebd8d72ada | |
![]() |
dd47360020 | |
![]() |
54ca7ab4a3 | |
![]() |
a670f5d4ec | |
![]() |
86cdc76645 | |
![]() |
1b1ef9274b | |
![]() |
17300b3bd0 | |
![]() |
2d2ba69ee9 | |
![]() |
ade7d90d51 | |
![]() |
cc401ccf80 | |
![]() |
94aeab80fa | |
![]() |
7c3386fcf2 | |
![]() |
5e57723c32 | |
![]() |
bd311d42e8 | |
![]() |
8ae19659f0 | |
![]() |
56d872d023 | |
![]() |
2a44209a2f | |
![]() |
0f4d62d622 | |
![]() |
fa1562218c | |
![]() |
d438400491 | |
![]() |
7926ca6011 | |
![]() |
11d6ca37c3 | |
![]() |
58941d9440 | |
![]() |
732949510a | |
![]() |
72665ad48c | |
![]() |
ad58836d20 | |
![]() |
a4934b8529 | |
![]() |
469e9839e8 | |
![]() |
5c73069818 | |
![]() |
28942d4f8f | |
![]() |
de1914c2fa | |
![]() |
f47d81ba39 | |
![]() |
8bb0acfc1d | |
![]() |
16a8b7f758 | |
![]() |
b5e9d35b0b | |
![]() |
643376e8a0 | |
![]() |
d6e5553084 | |
![]() |
a473bfca82 | |
![]() |
3a10804380 | |
![]() |
db4e03c62e | |
![]() |
40efcf1be8 | |
![]() |
4154954883 | |
![]() |
551b09d0d6 | |
![]() |
b4f3f5f565 | |
![]() |
2d6ded730e | |
![]() |
a622437064 | |
![]() |
a55bcf4996 | |
![]() |
2147b87816 | |
![]() |
7dd76fe0c9 | |
![]() |
4b90bdeb0c | |
![]() |
0d94dabe27 | |
![]() |
9cfd561677 | |
![]() |
42d3545a0a | |
![]() |
6e7fcda20c | |
![]() |
1d24591d65 | |
![]() |
b6194e68b3 | |
![]() |
f6b09f2a03 | |
![]() |
0505a26934 | |
![]() |
e080f6fd1b | |
![]() |
05f5fa8df5 | |
![]() |
b6ed7ac63b | |
![]() |
c4c6fe69f4 | |
![]() |
0658c1926c | |
![]() |
2e45708568 | |
![]() |
e2c7543d3e | |
![]() |
3403b7a0ec | |
![]() |
48ed9621c7 | |
![]() |
831cc82113 | |
![]() |
0a3f608727 | |
![]() |
deb32e6f28 | |
![]() |
9a44b80cb5 | |
![]() |
b9c2949bf6 | |
![]() |
3a8b7c697e | |
![]() |
74145d9a55 | |
![]() |
9b19e756cb | |
![]() |
4867bf18a0 | |
![]() |
86a24181b5 | |
![]() |
5ad751cc20 | |
![]() |
28f152a029 | |
![]() |
7c7553dec1 | |
![]() |
83145f221f | |
![]() |
eb07f68d63 | |
![]() |
c902a0eb3a | |
![]() |
32af52d14f | |
![]() |
bf3e698542 | |
![]() |
1bd2f6cd65 | |
![]() |
bc0b80fc6f | |
![]() |
2596ddea63 | |
![]() |
c9010a0167 | |
![]() |
9a7eaafc20 | |
![]() |
f46b854fad | |
![]() |
3218b4370f | |
![]() |
487ed3fb38 | |
![]() |
343024eb03 | |
![]() |
404bb0dd8c | |
![]() |
f4780ecb11 | |
![]() |
701b46e29a | |
![]() |
a0e3441507 | |
![]() |
7722a6e458 | |
![]() |
25bac4f262 | |
![]() |
43979e6f99 | |
![]() |
1fb7d1c976 | |
![]() |
9a6a106b38 | |
![]() |
e6753d4ea5 | |
![]() |
ed426f5e6b | |
![]() |
5fd5b76527 | |
![]() |
448956e853 | |
![]() |
4500fc3584 | |
![]() |
f632d3636f | |
![]() |
bfde21aa95 | |
![]() |
932ce83b1c | |
![]() |
d65dce62ae | |
![]() |
8057b3f55a | |
![]() |
c15852943e | |
![]() |
07c6de7cf6 | |
![]() |
944586408e | |
![]() |
b0eda7b44d | |
![]() |
bc6e37008b | |
![]() |
0a0ba8ea6c | |
![]() |
8424084c58 | |
![]() |
24aa61ed77 | |
![]() |
ff790752e0 | |
![]() |
48913cde0a | |
![]() |
08ff6b51b3 | |
![]() |
0637c0d0eb | |
![]() |
20b51fa15e | |
![]() |
a25c0555d8 | |
![]() |
4c3c2212e1 | |
![]() |
3f86aa1c40 | |
![]() |
936a7f1fde | |
![]() |
c063dda9f0 | |
![]() |
24772e3262 | |
![]() |
21ceea01e9 | |
![]() |
efdaf2f6f5 | |
![]() |
99c85784e3 | |
![]() |
efc7e0f088 | |
![]() |
c4a3d0c39e | |
![]() |
dd96377d74 | |
![]() |
4193a3e304 | |
![]() |
04fce0106a | |
![]() |
f9996f5921 | |
![]() |
05c0051c32 |
|
@ -10,3 +10,4 @@ repo/
|
|||
apk/
|
||||
gen
|
||||
generated-src/
|
||||
.kotlin
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -38,6 +38,7 @@ kotlinter {
|
|||
|
||||
dependencies {
|
||||
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
|
||||
implementation(project(":utils"))
|
||||
}
|
||||
|
||||
tasks {
|
||||
|
@ -51,3 +52,9 @@ tasks {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("printDependentExtensions") {
|
||||
doLast {
|
||||
project.printDependentExtensions()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,6 +103,7 @@ dependencies {
|
|||
if (theme != null) implementation(theme) // Overrides core launcher icons
|
||||
implementation(project(":core"))
|
||||
compileOnly(libs.bundles.common)
|
||||
implementation(project(":utils"))
|
||||
}
|
||||
|
||||
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-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }
|
||||
|
||||
tachiyomi-lib = { module = "com.github.tachiyomiorg:extensions-lib", version = "1.4.2" }
|
||||
tachiyomi-lib = { module = "com.github.keiyoushi:extensions-lib", version = "v1.4.2.1" }
|
||||
|
||||
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" }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 5
|
||||
baseVersionCode = 7
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:synchrony"))
|
||||
|
|
|
@ -276,14 +276,14 @@ abstract class ColaManga(
|
|||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
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 keyMappingRegex = Regex("""if\s*\(\s*([a-zA-Z0-9_]+)\s*==\s*(\d+)\s*\)\s*\{\s*return\s*'([a-zA-Z0-9_]+)'\s*;""")
|
||||
|
||||
private val keyMapping by lazy {
|
||||
val obfuscatedReadJs = client.newCall(GET("$baseUrl/js/manga.read.js")).execute().body.string()
|
||||
val readJs = Deobfuscator.deobfuscateScript(obfuscatedReadJs)
|
||||
?: throw Exception(intl.couldNotDeobufscateScript)
|
||||
|
||||
keyMappingRegex.findAll(readJs).associate { it.groups["keyType"]!!.value to it.groups["key"]!!.value }
|
||||
keyMappingRegex.findAll(readJs).associate { it.groups[2]!!.value to it.groups[3]!!.value }
|
||||
}
|
||||
|
||||
private fun randomString() = buildString(15) {
|
||||
|
|
|
@ -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="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>
|
|
@ -1,5 +0,0 @@
|
|||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
|
@ -1,242 +0,0 @@
|
|||
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()
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
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")
|
||||
}
|
||||
|
||||
baseVersionCode = 28
|
||||
baseVersionCode = 30
|
||||
|
|
|
@ -129,7 +129,7 @@ abstract class GroupLe(
|
|||
infoElement.select(".info-icon").attr("data-content").substringBeforeLast("/5</b><br/>")
|
||||
.substringAfterLast(": <b>").replace(",", ".").toFloat() * 2
|
||||
val ratingVotes =
|
||||
infoElement.select(".col-sm-7 .user-rating meta[itemprop=\"ratingCount\"]")
|
||||
infoElement.select(".col-sm-6 .user-rating meta[itemprop=\"ratingCount\"]")
|
||||
.attr("content")
|
||||
val ratingStar = when {
|
||||
ratingValue > 9.5 -> "★★★★★"
|
||||
|
@ -209,14 +209,16 @@ abstract class GroupLe(
|
|||
}
|
||||
|
||||
protected open fun getChapterSearchParams(document: Document): String {
|
||||
return "?mtr=true"
|
||||
val scriptContent = document.selectFirst("script:containsData(user_hash)")?.data()
|
||||
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> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
if (document.select(".user-avatar").isEmpty() &&
|
||||
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
|
||||
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") || contains("RuMix") }
|
||||
) {
|
||||
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
||||
}
|
||||
|
@ -309,7 +311,7 @@ abstract class GroupLe(
|
|||
val html = document.html()
|
||||
|
||||
if (document.select(".user-avatar").isEmpty() &&
|
||||
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
|
||||
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") || contains("RuMix") }
|
||||
|
||||
) {
|
||||
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
||||
|
@ -322,6 +324,9 @@ abstract class GroupLe(
|
|||
throw Exception("Не удалось загрузить главу. Url: ${response.request.url}")
|
||||
}
|
||||
else -> {
|
||||
if (document.selectFirst("div.alert") != null || document.selectFirst("form.purchase-form") != null) {
|
||||
throw Exception("Эта глава платная. Используйте сайт, чтобы купить и прочитать ее.")
|
||||
}
|
||||
throw Exception("Дизайн сайта обновлен, для дальнейшей работы необходимо обновление дополнения")
|
||||
}
|
||||
}
|
||||
|
@ -436,5 +441,6 @@ abstract class GroupLe(
|
|||
private const val UAGENT_TITLE = "User-Agent(для некоторых стран)"
|
||||
private const val UAGENT_DEFAULT = "arora"
|
||||
const val PREFIX_SLUG_SEARCH = "slug:"
|
||||
private val USER_HASH_REGEX = "user_hash.+'(.+)'".toRegex()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 27
|
||||
baseVersionCode = 28
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
|
|
@ -105,7 +105,7 @@ class HeanCmsChapterDto(
|
|||
@SerialName("chapter_name") private val name: String,
|
||||
@SerialName("chapter_title") private val title: String? = null,
|
||||
@SerialName("chapter_slug") private val slug: String,
|
||||
@SerialName("created_at") private val createdAt: String,
|
||||
@SerialName("created_at") private val createdAt: String? = null,
|
||||
val price: Int? = null,
|
||||
) {
|
||||
fun toSChapter(
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 16
|
||||
baseVersionCode = 18
|
||||
|
|
|
@ -107,7 +107,7 @@ open class Kemono(
|
|||
}
|
||||
|
||||
var mangas = mangasCache
|
||||
if (page == 1) {
|
||||
if (page == 1 || mangasCache.isEmpty()) {
|
||||
var favourites: List<KemonoFavouritesDto> = emptyList()
|
||||
if (fav != null) {
|
||||
val favores = client.newCall(GET("$baseUrl/$apiPath/account/favorites", headers)).execute()
|
||||
|
@ -132,7 +132,7 @@ open class Kemono(
|
|||
|
||||
includeType && !excludeType && isFavourited &&
|
||||
regularSearch
|
||||
}.also { mangasCache = mangas }
|
||||
}.also { mangasCache = it }
|
||||
}
|
||||
|
||||
val sorted = when (sort.first) {
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 12
|
||||
baseVersionCode = 13
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
|
|
@ -290,7 +290,7 @@ abstract class Keyoapp(
|
|||
.firstOrNull { CDN_HOST_REGEX.containsMatchIn(it.html()) }
|
||||
?.let {
|
||||
val cdnHost = CDN_HOST_REGEX.find(it.html())
|
||||
?.groups?.get("host")?.value
|
||||
?.groups?.get(1)?.value
|
||||
?.replace(CDN_CLEAN_REGEX, "")
|
||||
"https://$cdnHost/uploads"
|
||||
}
|
||||
|
@ -314,7 +314,7 @@ abstract class Keyoapp(
|
|||
|
||||
protected open fun Element.getImageUrl(selector: String): String? {
|
||||
return this.selectFirst(selector)?.let { element ->
|
||||
IMG_REGEX.find(element.attr("style"))?.groups?.get("url")?.value
|
||||
IMG_REGEX.find(element.attr("style"))?.groups?.get(1)?.value
|
||||
?.toHttpUrlOrNull()?.let {
|
||||
it.newBuilder()
|
||||
.setQueryParameter("w", "480") // Keyoapp returns the dynamic size of the thumbnail to any size
|
||||
|
@ -376,8 +376,8 @@ abstract class Keyoapp(
|
|||
companion object {
|
||||
private const val SHOW_PAID_CHAPTERS_PREF = "pref_show_paid_chap"
|
||||
private const val SHOW_PAID_CHAPTERS_DEFAULT = false
|
||||
val CDN_HOST_REGEX = """realUrl\s*=\s*`[^`]+//(?<host>[^/]+)""".toRegex()
|
||||
val CDN_HOST_REGEX = """realUrl\s*=\s*`[^`]+//([^/]+)""".toRegex()
|
||||
val CDN_CLEAN_REGEX = """\$\{[^}]*\}""".toRegex()
|
||||
val IMG_REGEX = """url\(['"]?(?<url>[^(['"\)])]+)""".toRegex()
|
||||
val IMG_REGEX = """url\(['"]?([^(['"\)])]+)""".toRegex()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 34
|
||||
baseVersionCode = 35
|
||||
|
|
|
@ -313,37 +313,33 @@ abstract class LibGroup(
|
|||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val slugUrl = response.request.url.toString().substringAfter("manga/").substringBefore("/chapters")
|
||||
val chaptersData = response.parseAs<Data<List<Chapter>>>()
|
||||
if (chaptersData.data.isEmpty()) {
|
||||
throw Exception("Нет глав")
|
||||
}
|
||||
|
||||
.also { if (it.data.isEmpty()) return emptyList() }
|
||||
val sortingList = preferences.getString(SORTING_PREF, "ms_mixing")
|
||||
val defaultBranchId = if (chaptersData.data.getBranchCount() > 1) { // excess request if branchesCount is only alone = slow update library witch rateLimitHost(apiDomain.toHttpUrl(), 1)
|
||||
val defaultBranchId = if (sortingList == "ms_mixing" && chaptersData.data.getBranchCount() > 1) {
|
||||
runCatching { getDefaultBranch(slugUrl.substringBefore("-")).first().id }.getOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
for (it in chaptersData.data.withIndex()) {
|
||||
if (it.value.branchesCount > 1) {
|
||||
for (currentBranch in it.value.branches.withIndex()) {
|
||||
if (currentBranch.value.branchId == defaultBranchId && sortingList == "ms_mixing") { // ms_mixing with default branch from api
|
||||
chapters.add(it.value.toSChapter(slugUrl, defaultBranchId, isScanUser()))
|
||||
} else if (defaultBranchId == null && sortingList == "ms_mixing") { // ms_mixing with first branch in chapter
|
||||
if (chapters.any { chpIt -> chpIt.chapter_number == it.value.number.toFloat() }) {
|
||||
chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser()))
|
||||
}
|
||||
} else if (sortingList == "ms_combining") { // ms_combining
|
||||
chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser()))
|
||||
return chaptersData.data.flatMap { chapter ->
|
||||
when {
|
||||
chapter.branchesCount > 1 && sortingList == "ms_mixing" -> {
|
||||
val branch = chapter.branches
|
||||
.firstOrNull { it.branchId == defaultBranchId }?.branchId
|
||||
?: chapter.branches.first().branchId
|
||||
|
||||
listOf(
|
||||
chapter.toSChapter(slugUrl, branch, isScanUser()),
|
||||
)
|
||||
}
|
||||
chapter.branchesCount > 1 && sortingList == "ms_combining" -> {
|
||||
chapter.branches.map { branch ->
|
||||
chapter.toSChapter(slugUrl, branch.branchId, isScanUser())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
chapters.add(it.value.toSChapter(slugUrl, isScanUser = isScanUser()))
|
||||
else -> listOf(chapter.toSChapter(slugUrl, isScanUser = isScanUser()))
|
||||
}
|
||||
}
|
||||
|
||||
return chapters.reversed()
|
||||
}.reversed()
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
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.
|
|
@ -0,0 +1 @@
|
|||
font_size_title=Tamaño de letra
|
|
@ -0,0 +1 @@
|
|||
font_size_title=Taille de la police
|
|
@ -0,0 +1 @@
|
|||
font_size_title=Ukuran font
|
|
@ -0,0 +1 @@
|
|||
font_size_title=Dimensione del carattere
|
|
@ -0,0 +1,6 @@
|
|||
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,4 +2,8 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
||||
baseVersionCode = 4
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
}
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
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.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.MangasPage
|
||||
|
@ -15,21 +24,26 @@ import kotlinx.serialization.decodeFromString
|
|||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
abstract class MachineTranslations(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
val language: Language,
|
||||
) : ParsedHttpSource() {
|
||||
private val language: Language,
|
||||
) : ParsedHttpSource(), ConfigurableSource {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
|
@ -37,9 +51,66 @@ abstract class MachineTranslations(
|
|||
|
||||
override val lang = language.lang
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor(ComposedImageInterceptor(baseUrl, language))
|
||||
.build()
|
||||
protected val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ===============================
|
||||
|
||||
|
@ -203,9 +274,76 @@ abstract class MachineTranslations(
|
|||
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 {
|
||||
val PAGE_REGEX = Regex(".*?\\.(webp|png|jpg|jpeg)#\\[.*?]", RegexOption.IGNORE_CASE)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,18 @@ package eu.kanade.tachiyomi.multisrc.machinetranslations
|
|||
|
||||
class MachineTranslationsFactoryUtils
|
||||
|
||||
data class Language(val lang: String, val target: String = lang, val origin: String = "en")
|
||||
interface Language {
|
||||
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,16 +26,13 @@ import uy.kohesive.injekt.injectLazy
|
|||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
// The Interceptor joins the dialogues and pages of the manga.
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
class ComposedImageInterceptor(
|
||||
baseUrl: String,
|
||||
val language: Language,
|
||||
var language: Language,
|
||||
) : Interceptor {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
@ -55,7 +52,7 @@ class ComposedImageInterceptor(
|
|||
}
|
||||
|
||||
val dialogues = request.url.fragment?.parseAs<List<Dialog>>()
|
||||
?: throw IOException("Dialogues not found")
|
||||
?: emptyList()
|
||||
|
||||
val imageRequest = request.newBuilder()
|
||||
.url(url)
|
||||
|
@ -63,7 +60,9 @@ class ComposedImageInterceptor(
|
|||
|
||||
// Load the fonts before opening the connection to load the image,
|
||||
// so there aren't two open connections inside the interceptor.
|
||||
loadAllFont(chain)
|
||||
if (language.disableSourceSettings.not()) {
|
||||
loadAllFont(chain)
|
||||
}
|
||||
|
||||
val response = chain.proceed(imageRequest)
|
||||
|
||||
|
@ -78,9 +77,9 @@ class ComposedImageInterceptor(
|
|||
|
||||
dialogues.forEach { dialog ->
|
||||
val textPaint = createTextPaint(selectFontFamily(dialog.type))
|
||||
val dialogBox = createDialogBox(dialog, textPaint, bitmap)
|
||||
val dialogBox = createDialogBox(dialog, textPaint)
|
||||
val y = getYAxis(textPaint, dialog, dialogBox)
|
||||
canvas.draw(dialogBox, dialog, dialog.x1, y)
|
||||
canvas.draw(textPaint, dialogBox, dialog, dialog.x1, y)
|
||||
}
|
||||
|
||||
val output = ByteArrayOutputStream()
|
||||
|
@ -104,7 +103,7 @@ class ComposedImageInterceptor(
|
|||
}
|
||||
|
||||
private fun createTextPaint(font: Typeface?): TextPaint {
|
||||
val defaultTextSize = 24.pt // arbitrary
|
||||
val defaultTextSize = language.fontSize.pt
|
||||
return TextPaint().apply {
|
||||
color = Color.BLACK
|
||||
textSize = defaultTextSize
|
||||
|
@ -116,6 +115,10 @@ class ComposedImageInterceptor(
|
|||
}
|
||||
|
||||
private fun selectFontFamily(type: String): Typeface? {
|
||||
if (language.disableSourceSettings) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (type in fontFamily) {
|
||||
return fontFamily[type]?.second
|
||||
}
|
||||
|
@ -206,7 +209,7 @@ class ComposedImageInterceptor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun createDialogBox(dialog: Dialog, textPaint: TextPaint, bitmap: Bitmap): StaticLayout {
|
||||
private fun createDialogBox(dialog: Dialog, textPaint: TextPaint): StaticLayout {
|
||||
var dialogBox = createBoxLayout(dialog, textPaint)
|
||||
|
||||
/**
|
||||
|
@ -217,18 +220,8 @@ class ComposedImageInterceptor(
|
|||
dialogBox = createBoxLayout(dialog, textPaint)
|
||||
}
|
||||
|
||||
// Use source setup
|
||||
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)
|
||||
textPaint.color = Color.BLACK
|
||||
textPaint.bgColor = Color.WHITE
|
||||
|
||||
return dialogBox
|
||||
}
|
||||
|
@ -241,59 +234,46 @@ class ComposedImageInterceptor(
|
|||
setIncludePad(false)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED)
|
||||
setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
|
||||
}
|
||||
}.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 {
|
||||
return json.decodeFromString(this)
|
||||
}
|
||||
|
||||
private fun Canvas.draw(layout: StaticLayout, dialog: Dialog, x: Float, y: Float) {
|
||||
private fun Canvas.draw(textPaint: TextPaint, layout: StaticLayout, dialog: Dialog, x: Float, y: Float) {
|
||||
save()
|
||||
translate(x, y)
|
||||
rotate(dialog.angle)
|
||||
layout.draw(this)
|
||||
drawTextOutline(textPaint, layout)
|
||||
drawText(textPaint, layout)
|
||||
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
|
||||
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 {
|
||||
// w3: Absolute Lengths [...](https://www.w3.org/TR/css3-values/#absolute-lengths)
|
||||
const val SCALED_DENSITY = 0.75f // 1px = 0.75pt
|
||||
|
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 37
|
||||
baseVersionCode = 40
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:cryptoaes"))
|
||||
|
|
|
@ -82,7 +82,12 @@ abstract class Madara(
|
|||
/**
|
||||
* Automatically fetched genres from the source to be used in the filters.
|
||||
*/
|
||||
private var genresList: List<Genre> = emptyList()
|
||||
protected open 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.
|
||||
|
@ -237,11 +242,14 @@ abstract class Madara(
|
|||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
if (query.startsWith(URL_SEARCH_PREFIX)) {
|
||||
val mangaUrl = "/$mangaSubString/${query.substringAfter(URL_SEARCH_PREFIX)}/"
|
||||
return client.newCall(GET("$baseUrl$mangaUrl", headers))
|
||||
val mangaUrl = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment(mangaSubString)
|
||||
addPathSegment(query.substringAfter(URL_SEARCH_PREFIX))
|
||||
}.build()
|
||||
return client.newCall(GET(mangaUrl, headers))
|
||||
.asObservableSuccess().map { response ->
|
||||
val manga = mangaDetailsParse(response).apply {
|
||||
url = mangaUrl
|
||||
setUrlWithoutDomain(mangaUrl.toString())
|
||||
}
|
||||
|
||||
MangasPage(listOf(manga), false)
|
||||
|
@ -578,11 +586,13 @@ abstract class Madara(
|
|||
|
||||
override fun searchMangaSelector() = "div.c-tabs-item__content"
|
||||
|
||||
protected open val searchMangaUrlSelector = "div.post-title a"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
|
||||
with(element) {
|
||||
selectFirst("div.post-title a")!!.let {
|
||||
selectFirst(searchMangaUrlSelector)!!.let {
|
||||
manga.setUrlWithoutDomain(it.attr("abs:href"))
|
||||
manga.title = it.ownText()
|
||||
}
|
||||
|
@ -623,7 +633,7 @@ abstract class Madara(
|
|||
"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",
|
||||
"Curso", "En marcha", "Publicandose", "Publicándose", "En emision", "连载中", "Em Lançamento", "Devam Ediyo",
|
||||
"Đang làm", "Em postagem", "Devam Eden", "Em progresso", "Em curso",
|
||||
"Đang làm", "Em postagem", "Devam Eden", "Em progresso", "Em curso", "Atualizações Semanais",
|
||||
)
|
||||
|
||||
protected val hiatusStatusList: Array<String> = arrayOf(
|
||||
|
@ -678,7 +688,7 @@ abstract class Madara(
|
|||
manga.thumbnail_url = imageFromElement(it)
|
||||
}
|
||||
select(mangaDetailsSelectorStatus).last()?.let {
|
||||
manga.status = with(it.text()) {
|
||||
manga.status = with(it.text().filter { ch -> ch.isLetterOrDigit() || ch.isWhitespace() }.trim()) {
|
||||
when {
|
||||
containsIn(completedStatusList) -> SManga.COMPLETED
|
||||
containsIn(ongoingStatusList) -> SManga.ONGOING
|
||||
|
@ -742,7 +752,7 @@ abstract class Madara(
|
|||
|
||||
// Manga Details Selector
|
||||
open val mangaDetailsSelectorTitle = "div.post-title h3, div.post-title h1, #manga-title > h1"
|
||||
open val mangaDetailsSelectorAuthor = "div.author-content > a"
|
||||
open val mangaDetailsSelectorAuthor = "div.author-content > a, div.manga-authors > a"
|
||||
open val mangaDetailsSelectorArtist = "div.artist-content > a"
|
||||
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"
|
||||
|
@ -776,7 +786,7 @@ abstract class Madara(
|
|||
/**
|
||||
* Get the best image quality available from srcset
|
||||
*/
|
||||
private fun String.getSrcSetImage(): String? {
|
||||
protected fun String.getSrcSetImage(): String? {
|
||||
return this.split(" ")
|
||||
.filter(URL_REGEX::matches)
|
||||
.maxOfOrNull(String::toString)
|
||||
|
@ -920,6 +930,10 @@ abstract class Madara(
|
|||
WordSet("hace").startsWith(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)""")) -> {
|
||||
// Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it
|
||||
date.split(" ").map {
|
||||
|
@ -1063,10 +1077,17 @@ abstract class Madara(
|
|||
* Fetch the genres from the source to be used in the filters.
|
||||
*/
|
||||
protected fun fetchGenres() {
|
||||
if (fetchGenres && fetchGenresAttempts < 3 && genresList.isEmpty()) {
|
||||
if (fetchGenres && fetchGenresAttempts < 3 && !genresFetched) {
|
||||
try {
|
||||
genresList = client.newCall(genresRequest()).execute()
|
||||
client.newCall(genresRequest()).execute()
|
||||
.use { parseGenres(it.asJsoup()) }
|
||||
.also {
|
||||
genresFetched = true
|
||||
}
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.also {
|
||||
genresList = it
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
} finally {
|
||||
fetchGenresAttempts++
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 14
|
||||
baseVersionCode = 17
|
||||
|
|
|
@ -11,11 +11,8 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
|||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
|
@ -25,7 +22,6 @@ import org.jsoup.Jsoup
|
|||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
|
@ -55,8 +51,6 @@ abstract class MadTheme(
|
|||
add("Referer", "$baseUrl/")
|
||||
}
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private var genreKey = "genre[]"
|
||||
|
||||
// Popular
|
||||
|
@ -177,19 +171,53 @@ abstract class MadTheme(
|
|||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
if (response.code in 200..299) {
|
||||
if (response.request.url.fragment == "idFound") {
|
||||
return super.chapterListParse(response)
|
||||
}
|
||||
|
||||
// Try to show message/error from site
|
||||
response.body.let { body ->
|
||||
json.decodeFromString<JsonObject>(body.string())["message"]
|
||||
?.jsonPrimitive
|
||||
?.content
|
||||
?.let { throw Exception(it) }
|
||||
val document = response.asJsoup()
|
||||
|
||||
// Need the total chapters to check against the request
|
||||
val totalChapters = document.selectFirst(".title span:containsOwn(CHAPTERS \\()")?.text()
|
||||
?.substringAfter("(")
|
||||
?.substringBefore(")")
|
||||
?.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}")
|
||||
}
|
||||
|
||||
throw Exception("HTTP error ${response.code}")
|
||||
var finalDocument = slugRequest.asJsoup().select(chapterListSelector())
|
||||
|
||||
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 =
|
||||
|
@ -197,10 +225,11 @@ abstract class MadTheme(
|
|||
val url = "$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("manga_id", mangaId)
|
||||
.addQueryParameter("manga_name", manga.title)
|
||||
.fragment("idFound")
|
||||
.build()
|
||||
|
||||
GET(url, headers)
|
||||
} ?: GET("$baseUrl/api/manga${manga.url}/chapters?source=detail", headers)
|
||||
} ?: GET("$baseUrl${manga.url}", headers)
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
if (genresList == null) {
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 13
|
||||
baseVersionCode = 14
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
||||
baseVersionCode = 3
|
||||
|
|
|
@ -1,129 +1,356 @@
|
|||
package eu.kanade.tachiyomi.multisrc.mangareader
|
||||
|
||||
import android.app.Application
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
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.HttpSource
|
||||
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.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Evaluator
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import org.jsoup.nodes.TextNode
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
|
||||
abstract class MangaReader : HttpSource(), ConfigurableSource {
|
||||
abstract class MangaReader(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
final override val lang: String,
|
||||
) : HttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
final override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
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 searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
var entries = document.select(searchMangaSelector()).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
|
||||
// =============================== Latest ===============================
|
||||
|
||||
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)
|
||||
}
|
||||
listOf(manga, volume)
|
||||
}
|
||||
|
||||
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 entries = document.select(searchMangaSelector())
|
||||
.map(::searchMangaFromElement)
|
||||
|
||||
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
|
||||
return MangasPage(entries, hasNextPage)
|
||||
}
|
||||
|
||||
final override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX)
|
||||
// =========================== Manga Details ============================
|
||||
|
||||
abstract fun searchMangaSelector(): String
|
||||
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
|
||||
|
||||
abstract fun searchMangaNextPageSelector(): String
|
||||
private val authorText: String = when (lang) {
|
||||
"ja" -> "著者"
|
||||
else -> "Authors"
|
||||
}
|
||||
|
||||
abstract fun searchMangaFromElement(element: Element): SManga
|
||||
private val statusText: String = when (lang) {
|
||||
"ja" -> "地位"
|
||||
else -> "Status"
|
||||
}
|
||||
|
||||
abstract fun mangaDetailsParse(document: Document): SManga
|
||||
|
||||
final override fun mangaDetailsParse(response: Response): SManga {
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
val manga = mangaDetailsParse(document)
|
||||
if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
|
||||
manga.title = VOLUME_TITLE_PREFIX + manga.title
|
||||
|
||||
return SManga.create().apply {
|
||||
document.selectFirst("#ani_detail")!!.run {
|
||||
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
|
||||
}
|
||||
|
||||
abstract val chapterType: String
|
||||
abstract val volumeType: String
|
||||
private fun Element.parseStatus(manga: SManga): SManga {
|
||||
manga.status = this.selectFirst(".name")?.text().getStatus()
|
||||
return manga
|
||||
}
|
||||
|
||||
abstract fun chapterListRequest(mangaUrl: String, type: String): Request
|
||||
open fun String?.getStatus(): Int = when (this?.lowercase()) {
|
||||
"ongoing", "publishing", "releasing" -> SManga.ONGOING
|
||||
"completed", "finished" -> SManga.COMPLETED
|
||||
"on-hold", "on_hiatus" -> SManga.ON_HIATUS
|
||||
"canceled", "discontinued" -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
abstract fun parseChapterElements(response: Response, isVolume: Boolean): List<Element>
|
||||
// ============================== Chapters ==============================
|
||||
|
||||
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
|
||||
override fun getChapterUrl(chapter: SChapter): String {
|
||||
return baseUrl + chapter.url.substringBeforeLast('#')
|
||||
}
|
||||
|
||||
open fun updateChapterList(manga: SManga, chapters: List<SChapter>) = Unit
|
||||
open val chapterIdSelect = "en-chapters"
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
|
||||
val path = manga.url
|
||||
val isVolume = path.endsWith(VOLUME_URL_SUFFIX)
|
||||
val type = if (isVolume) volumeType else chapterType
|
||||
val request = chapterListRequest(path.removeSuffix(VOLUME_URL_SUFFIX), type)
|
||||
val response = client.newCall(request).execute()
|
||||
open fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
element.selectFirst("a")!!.run {
|
||||
setUrlWithoutDomain(attr("href") + "#${element.attr("data-id")}")
|
||||
name = selectFirst(".name")?.text() ?: text()
|
||||
}
|
||||
}
|
||||
|
||||
val abbrPrefix = if (isVolume) "Vol" else "Chap"
|
||||
val fullPrefix = if (isVolume) "Volume" else "Chapter"
|
||||
val linkSelector = Evaluator.Tag("a")
|
||||
parseChapterElements(response, isVolume).map { element ->
|
||||
SChapter.create().apply {
|
||||
val number = element.attr("data-number")
|
||||
chapter_number = number.toFloatOrNull() ?: -1f
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
return document.select("#$chapterIdSelect > li.chapter-item").map(::chapterFromElement)
|
||||
}
|
||||
|
||||
val link = element.selectFirst(linkSelector)!!
|
||||
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"
|
||||
}
|
||||
setUrlWithoutDomain(link.attr("href") + '#' + type + '/' + element.attr("data-id"))
|
||||
// =============================== Pages ================================
|
||||
|
||||
open fun getChapterId(chapter: SChapter): String {
|
||||
val document = client.newCall(GET(baseUrl + chapter.url, headers)).execute().asJsoup()
|
||||
return document.selectFirst("div[data-reading-id]")
|
||||
?.attr("data-reading-id")
|
||||
.orEmpty()
|
||||
.ifEmpty {
|
||||
throw Exception("Unable to retrieve chapter id")
|
||||
}
|
||||
}.also { if (!isVolume && it.isNotEmpty()) updateChapterList(manga, it) }
|
||||
}
|
||||
|
||||
final override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast('#')
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
|
||||
open fun getAjaxUrl(id: String): String {
|
||||
return "$baseUrl//ajax/image/list/$id?mode=vertical"
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SHOW_VOLUME_PREF = "show_volume"
|
||||
open fun pageListParseSelector(): String = ".container-reader-chapter > div > img"
|
||||
|
||||
private const val VOLUME_URL_FRAGMENT = "vol"
|
||||
private const val VOLUME_URL_SUFFIX = "#" + VOLUME_URL_FRAGMENT
|
||||
private const val VOLUME_TITLE_PREFIX = "[VOL] "
|
||||
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 {
|
||||
builder.addQueryParameter(param, checked.joinToString(join) { it.value })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class SortFilter(
|
||||
title: String,
|
||||
param: String,
|
||||
values: Array<Pair<String, String>>,
|
||||
default: String? = null,
|
||||
) : UriPartFilter(title, param, values, default)
|
||||
|
||||
private val sortFilterName: String = when (lang) {
|
||||
"ja" -> "選別"
|
||||
else -> "Sort"
|
||||
}
|
||||
|
||||
protected open val sortFilterParam: String = "sort"
|
||||
|
||||
protected open fun sortFilterValues(): Array<Pair<String, String>> {
|
||||
return arrayOf(
|
||||
Pair("Default", "default"),
|
||||
Pair("Latest Updated", sortLatestValue),
|
||||
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())
|
||||
|
||||
override fun getFilterList(): FilterList = FilterList(
|
||||
getSortFilter(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
||||
baseVersionCode = 3
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
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,7 +28,11 @@ abstract class MangaWorld(
|
|||
) : ParsedHttpSource() {
|
||||
|
||||
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 {
|
||||
protected val CHAPTER_NUMBER_REGEX by lazy { Regex("""(?i)capitolo\s([0-9]+)""") }
|
||||
|
|
|
@ -2,4 +2,4 @@ plugins {
|
|||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
||||
baseVersionCode = 4
|
||||
|
|
|
@ -267,7 +267,7 @@ abstract class SlimeReadTheme(
|
|||
|
||||
companion object {
|
||||
const val PREFIX_SEARCH = "id:"
|
||||
val FUNCTION_REGEX = """(?<script>\[""\.concat\("[^,]+,"\."\)\.concat\((?<infix>[^,]+),":\d+"\)\])""".toRegex(RegexOption.DOT_MATCHES_ALL)
|
||||
val FUNCTION_REGEX = """(\[""\.concat\("[^,]+,"\."\)\.concat\(([^,]+),":\d+"\)\])""".toRegex(RegexOption.DOT_MATCHES_ALL)
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
include(":core")
|
||||
include(":utils")
|
||||
|
||||
// Load all modules under /lib
|
||||
File(rootDir, "lib").eachDir { include("lib:${it.name}") }
|
||||
|
|
|
@ -14,8 +14,12 @@
|
|||
|
||||
<data android:host="*.bato.to" />
|
||||
<data android:host="bato.to" />
|
||||
<data android:host="*.batocc.com" />
|
||||
<data android:host="batocc.com" />
|
||||
<data android:host="*.batocomic.com" />
|
||||
<data android:host="batocomic.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="*.batotwo.com" />
|
||||
|
@ -24,18 +28,40 @@
|
|||
<data android:host="battwo.com" />
|
||||
<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.net" />
|
||||
<data android:host="mangatoto.net" />
|
||||
<data android:host="*.mangatoto.org" />
|
||||
<data android:host="mangatoto.org" />
|
||||
<data android:host="*.mycordant.co.uk" />
|
||||
<data android:host="mycordant.co.uk" />
|
||||
<data android:host="*.readtoto.com" />
|
||||
<data android:host="readtoto.com" />
|
||||
<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="*.fto.to" />
|
||||
<data android:host="fto.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="*.wto.to" />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Bato.to'
|
||||
extClass = '.BatoToFactory'
|
||||
extVersionCode = 46
|
||||
extVersionCode = 48
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.SharedPreferences
|
|||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.extension.BuildConfig
|
||||
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
||||
import eu.kanade.tachiyomi.lib.cryptoaes.Deobfuscator
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
|
@ -100,11 +101,25 @@ open class BatoTo(
|
|||
if (current.isNotEmpty()) {
|
||||
return current
|
||||
}
|
||||
field = getMirrorPref()!!
|
||||
field = getMirrorPref()
|
||||
return field
|
||||
}
|
||||
|
||||
private fun getMirrorPref(): String? = preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
|
||||
private fun getMirrorPref(): String {
|
||||
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 isRemoveTitleVersion(): Boolean {
|
||||
return preferences.getBoolean("${REMOVE_TITLE_VERSION_PREF}_$lang", false)
|
||||
|
@ -327,6 +342,12 @@ open class BatoTo(
|
|||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
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 super.mangaDetailsRequest(manga)
|
||||
|
@ -337,8 +358,8 @@ open class BatoTo(
|
|||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.selectFirst("div#mainer div.container-fluid")!!
|
||||
val manga = SManga.create()
|
||||
val workStatus = infoElement.select("div.attr-item:contains(original work) span").text()
|
||||
val uploadStatus = infoElement.select("div.attr-item:contains(upload status) span").text()
|
||||
val workStatus = infoElement.selectFirst("div.attr-item:contains(original work) span")?.text()
|
||||
val uploadStatus = infoElement.selectFirst("div.attr-item:contains(upload status) span")?.text()
|
||||
val originalTitle = infoElement.select("h3").text().removeEntities()
|
||||
val description = buildString {
|
||||
append(infoElement.select("div.limit-html").text())
|
||||
|
@ -346,13 +367,13 @@ open class BatoTo(
|
|||
append("\n\n${it.text()}")
|
||||
}
|
||||
infoElement.selectFirst("h5:containsOwn(Extra Info:) + div")?.also {
|
||||
append("\n\nExtra Info:\n${it.text()}")
|
||||
append("\n\nExtra Info:\n${it.wholeText()}")
|
||||
}
|
||||
document.selectFirst("div.pb-2.alias-set.line-b-f")?.also {
|
||||
document.selectFirst("div.pb-2.alias-set.line-b-f")?.takeIf { it.hasText() }?.also {
|
||||
append("\n\nAlternative Titles:\n")
|
||||
append(it.text().split('/').joinToString("\n") { "• ${it.trim()}" })
|
||||
}
|
||||
}
|
||||
}.trim()
|
||||
|
||||
val cleanedTitle = if (isRemoveTitleVersion()) {
|
||||
originalTitle.replace(titleRegex, "").trim()
|
||||
|
@ -369,16 +390,19 @@ open class BatoTo(
|
|||
manga.thumbnail_url = document.select("div.attr-cover img").attr("abs:src")
|
||||
return manga
|
||||
}
|
||||
private fun parseStatus(workStatus: String?, uploadStatus: String?) = when {
|
||||
workStatus == null -> SManga.UNKNOWN
|
||||
workStatus.contains("Ongoing") -> SManga.ONGOING
|
||||
workStatus.contains("Cancelled") -> SManga.CANCELLED
|
||||
workStatus.contains("Hiatus") -> SManga.ON_HIATUS
|
||||
workStatus.contains("Completed") -> when {
|
||||
uploadStatus?.contains("Ongoing") == true -> SManga.PUBLISHING_FINISHED
|
||||
else -> SManga.COMPLETED
|
||||
private fun parseStatus(workStatus: String?, uploadStatus: String?): Int {
|
||||
val status = workStatus ?: uploadStatus
|
||||
return when {
|
||||
status == null -> SManga.UNKNOWN
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Cancelled") -> SManga.CANCELLED
|
||||
status.contains("Hiatus") -> SManga.ON_HIATUS
|
||||
status.contains("Completed") -> when {
|
||||
uploadStatus?.contains("Ongoing") == true -> SManga.PUBLISHING_FINISHED
|
||||
else -> SManga.COMPLETED
|
||||
}
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
private fun altChapterParse(response: Response): List<SChapter> {
|
||||
|
@ -411,6 +435,12 @@ open class BatoTo(
|
|||
|
||||
GET("$baseUrl/rss/series/$id.xml", headers)
|
||||
} 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)
|
||||
} else {
|
||||
super.chapterListRequest(manga)
|
||||
|
@ -507,6 +537,12 @@ open class BatoTo(
|
|||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
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 super.pageListRequest(chapter)
|
||||
|
@ -1001,7 +1037,7 @@ open class BatoTo(
|
|||
private const val MIRROR_PREF_TITLE = "Mirror"
|
||||
private const val REMOVE_TITLE_VERSION_PREF = "REMOVE_TITLE_VERSION"
|
||||
private val MIRROR_PREF_ENTRIES = arrayOf(
|
||||
"zbato.org",
|
||||
"Auto",
|
||||
"batocomic.com",
|
||||
"batocomic.net",
|
||||
"batocomic.org",
|
||||
|
@ -1013,23 +1049,25 @@ open class BatoTo(
|
|||
"readtoto.com",
|
||||
"readtoto.net",
|
||||
"readtoto.org",
|
||||
"dto.to",
|
||||
"fto.to",
|
||||
"jto.to",
|
||||
"hto.to",
|
||||
"mto.to",
|
||||
"wto.to",
|
||||
"xbato.com",
|
||||
"xbato.net",
|
||||
"xbato.org",
|
||||
"zbato.com",
|
||||
"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_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES[0]
|
||||
|
||||
private val DEPRECATED_MIRRORS = listOf(
|
||||
"https://bato.to",
|
||||
"https://batocc.com", // parked
|
||||
"https://mangatoto.com",
|
||||
"https://mangatoto.net",
|
||||
"https://mangatoto.org",
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
ext {
|
||||
extName = 'Comic Growl'
|
||||
extClass = '.ComicGrowl'
|
||||
themePkg = 'gigaviewer'
|
||||
baseUrl = 'https://comic-growl.com'
|
||||
overrideVersionCode = 0
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 18 KiB |
|
@ -0,0 +1,63 @@
|
|||
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 {
|
||||
extName = 'Comick'
|
||||
extClass = '.ComickFactory'
|
||||
extVersionCode = 51
|
||||
extVersionCode = 52
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Comico'
|
||||
extClass = '.ComicoFactory'
|
||||
extVersionCode = 5
|
||||
extVersionCode = 6
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import okhttp3.Headers
|
|||
import okhttp3.HttpUrl
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.lang.Exception
|
||||
import java.security.MessageDigest
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
|
@ -159,7 +160,7 @@ open class Comico(
|
|||
if (!chapter.name.endsWith(LOCK)) {
|
||||
super.fetchPageList(chapter)
|
||||
} else {
|
||||
throw Error("You are not authorized to view this!")
|
||||
throw Exception("You are not authorized to view this!")
|
||||
}
|
||||
|
||||
private fun search(query: String, page: Int) =
|
||||
|
@ -176,7 +177,7 @@ open class Comico(
|
|||
private val Response.data: JsonElement?
|
||||
get() = json.parseToJsonElement(body.string()).jsonObject.also {
|
||||
val code = it["result"]["code"].jsonPrimitive.int
|
||||
if (code != 200) throw Error(status(code))
|
||||
if (code != 200) throw Exception(status(code))
|
||||
}["data"]
|
||||
|
||||
private operator fun JsonElement?.get(key: String) =
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'CosplayTele'
|
||||
extClass = '.CosplayTele'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 4
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ class CosplayTele : ParsedHttpSource() {
|
|||
override fun latestUpdatesNextPageSelector() = ".next.page-number"
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page/")
|
||||
|
||||
override fun latestUpdatesSelector() = "div.box"
|
||||
override fun latestUpdatesSelector() = "main div.box"
|
||||
|
||||
// Popular
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'DeviantArt'
|
||||
extClass = '.DeviantArt'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 6
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
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.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
|
@ -15,16 +20,22 @@ import okhttp3.Response
|
|||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.parser.Parser
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class DeviantArt : HttpSource() {
|
||||
class DeviantArt : HttpSource(), ConfigurableSource {
|
||||
override val name = "DeviantArt"
|
||||
override val baseUrl = "https://deviantart.com"
|
||||
override val baseUrl = "https://www.deviantart.com"
|
||||
override val lang = "all"
|
||||
override val supportsLatest = false
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
@ -76,28 +87,32 @@ class DeviantArt : HttpSource() {
|
|||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
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
|
||||
title = subFolderGallery?.selectFirst("._2vMZg + ._2vMZg")?.text()?.substringBeforeLast(" ")
|
||||
?: subFolderGallery?.selectFirst("[aria-haspopup=listbox] > div")!!.ownText()
|
||||
val gallery = document.selectFirst("#sub-folder-gallery")
|
||||
|
||||
// If manga is sub-gallery then use sub-gallery name, else use gallery name
|
||||
val galleryName = gallery?.selectFirst("._2vMZg + ._2vMZg")?.text()?.substringBeforeLast(" ")
|
||||
?: gallery?.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(" ")
|
||||
description = subFolderGallery?.selectFirst(".legacy-journal")?.wholeText()
|
||||
thumbnail_url = subFolderGallery?.selectFirst("img[property=contentUrl]")?.absUrl("src")
|
||||
title = when (artistInTitle) {
|
||||
true -> "$author - $galleryName"
|
||||
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 {
|
||||
val pathSegments = getMangaUrl(manga).toHttpUrl().pathSegments
|
||||
val username = pathSegments[0]
|
||||
val folderId = pathSegments[2]
|
||||
|
||||
val query = if (folderId == "all") {
|
||||
"gallery:$username"
|
||||
} else {
|
||||
"gallery:$username/$folderId"
|
||||
val query = when (val folderId = pathSegments[2]) {
|
||||
"all" -> "gallery:$username"
|
||||
else -> "gallery:$username/$folderId"
|
||||
}
|
||||
|
||||
val url = backendBuilder()
|
||||
|
@ -123,15 +138,14 @@ class DeviantArt : HttpSource() {
|
|||
nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href")
|
||||
}
|
||||
|
||||
return indexChapterList(chapterList.toList())
|
||||
return chapterList.toList().also(::indexChapterList)
|
||||
}
|
||||
|
||||
private fun parseToChapterList(document: Document): List<SChapter> {
|
||||
val items = document.select("item")
|
||||
return items.map {
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(it.selectFirst("link")!!.text())
|
||||
chapter.apply {
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain(it.selectFirst("link")!!.text())
|
||||
name = it.selectFirst("title")!!.text()
|
||||
date_upload = parseDate(it.selectFirst("pubDate")?.text())
|
||||
scanlator = it.selectFirst("media|credit")?.text()
|
||||
|
@ -139,24 +153,34 @@ class DeviantArt : HttpSource() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun indexChapterList(chapterList: List<SChapter>): List<SChapter> {
|
||||
private fun indexChapterList(chapterList: List<SChapter>) {
|
||||
// DeviantArt allows users to arrange galleries arbitrarily so we will
|
||||
// primitively index the list by checking the first and last dates
|
||||
return if (chapterList.first().date_upload > chapterList.last().date_upload) {
|
||||
chapterList.mapIndexed { i, chapter ->
|
||||
chapter.apply { chapter_number = chapterList.size - i.toFloat() }
|
||||
if (chapterList.first().date_upload > chapterList.last().date_upload) {
|
||||
chapterList.forEachIndexed { i, chapter ->
|
||||
chapter.chapter_number = chapterList.size - i.toFloat()
|
||||
}
|
||||
} else {
|
||||
chapterList.mapIndexed { i, chapter ->
|
||||
chapter.apply { chapter_number = i.toFloat() + 1 }
|
||||
chapterList.forEachIndexed { i, chapter ->
|
||||
chapter.chapter_number = i.toFloat() + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
val imageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src")
|
||||
return listOf(Page(0, imageUrl = imageUrl))
|
||||
val firstImageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src")
|
||||
return when (val buttons = document.selectFirst("[draggable=false]")?.children()) {
|
||||
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 {
|
||||
|
@ -167,7 +191,38 @@ class DeviantArt : HttpSource() {
|
|||
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 {
|
||||
const val SEARCH_FORMAT_MSG = "Please enter a query in the format of gallery:{username} or gallery:{username}/{folderId}"
|
||||
private const val SEARCH_FORMAT_MSG = "Please enter a query in the format of gallery:{username} or gallery:{username}/{folderId}"
|
||||
}
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 16 KiB |
|
@ -1,12 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
ext {
|
||||
extName = 'Frelein Books'
|
||||
extClass = '.FreleinBooks'
|
||||
extVersionCode = 1
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 8.2 KiB |
|
@ -1,271 +0,0 @@
|
|||
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 {
|
||||
extName = 'Hitomi'
|
||||
extClass = '.HitomiFactory'
|
||||
extVersionCode = 35
|
||||
extVersionCode = 36
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -64,8 +64,11 @@ class Hitomi(
|
|||
|
||||
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()
|
||||
.addInterceptor(::jxlContentTypeInterceptor)
|
||||
.addInterceptor(::updateImageUrlInterceptor)
|
||||
.apply {
|
||||
interceptors().add(0, ::streamResetRetry)
|
||||
}
|
||||
|
@ -748,6 +751,25 @@ 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 popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'Little Garden'
|
||||
extClass = '.LittleGarden'
|
||||
extVersionCode = 2
|
||||
extVersionCode = 3
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -31,7 +31,7 @@ class LittleGarden : ParsedHttpSource() {
|
|||
private const val cdnUrl = "https://littlexgarden.com/static/images/webp/"
|
||||
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||
private val slugRegex = Regex("\\\\\"slug\\\\\":\\\\\"(.*?(?=\\\\\"))")
|
||||
private val oricolPageRegex = Regex("\\{colored:(?<colored>.*?(?=,)),original:(?<original>.*?(?=,))")
|
||||
private val oricolPageRegex = Regex("\\{colored:(.*?(?=,)),original:(.*?(?=,))")
|
||||
private val oriPageRegex = Regex("""original:"(.*?(?="))""")
|
||||
}
|
||||
|
||||
|
@ -176,10 +176,10 @@ class LittleGarden : ParsedHttpSource() {
|
|||
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
|
||||
oricolPageRegex.findAll(document.select("script:containsData(pages)").toString()).asIterable().mapIndexed { i, it ->
|
||||
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["colored"]?.value?.replace("\"", "") + ".webp"))
|
||||
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
|
||||
pages.add(Page(i, "", cdnUrl + it.groups[1]?.value?.replace("\"", "") + ".webp"))
|
||||
} else {
|
||||
pages.add(Page(i, "", cdnUrl + it.groups["original"]?.value?.replace("\"", "") + ".webp"))
|
||||
pages.add(Page(i, "", cdnUrl + it.groups[2]?.value?.replace("\"", "") + ".webp"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -23,6 +23,9 @@ data_saver_summary=Enables smaller, more compressed images
|
|||
excluded_tags_mode=Excluded tags mode
|
||||
filter_original_languages=Filter original languages
|
||||
filter_original_languages_summary=Only show content that was originally published in the selected languages in both latest and browse
|
||||
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_adaptation=Adaptation
|
||||
format_anthology=Anthology
|
||||
|
@ -76,8 +79,8 @@ original_language=Original language
|
|||
original_language_filter_chinese=%s (Manhua)
|
||||
original_language_filter_japanese=%s (Manga)
|
||||
original_language_filter_korean=%s (Manhwa)
|
||||
prefer_title_in_extension_language=Use Alternate Titles
|
||||
prefer_title_in_extension_language_summary=If there is an alternate title available which matches the extension language, it will be used
|
||||
prefer_title_in_extension_language=Use alternative titles
|
||||
prefer_title_in_extension_language_summary=If there is an alternative title available which matches the extension language, it will be used
|
||||
publication_demographic=Publication demographic
|
||||
publication_demographic_josei=Josei
|
||||
publication_demographic_none=None
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ext {
|
||||
extName = 'MangaDex'
|
||||
extClass = '.MangaDexFactory'
|
||||
extVersionCode = 196
|
||||
extVersionCode = 199
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -143,6 +143,11 @@ object MDConstants {
|
|||
return "${preferExtensionLangTitlePref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val finalChapterInDescPref = "finalChapterInDesc"
|
||||
fun getFinalChapterInDescPrefKey(dexLang: String): String {
|
||||
return "${finalChapterInDescPref}_$dexLang"
|
||||
}
|
||||
|
||||
private const val tagGroupContent = "content"
|
||||
private const val tagGroupFormat = "format"
|
||||
private const val tagGroupGenre = "genre"
|
||||
|
|
|
@ -424,6 +424,7 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
|
|||
preferences.coverQuality,
|
||||
preferences.altTitlesInDesc,
|
||||
preferences.preferExtensionLangTitle,
|
||||
preferences.finalChapterInDesc,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -773,12 +774,28 @@ 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(tryUsingFirstVolumeCoverPref)
|
||||
screen.addPreference(dataSaverPref)
|
||||
screen.addPreference(standardHttpsPortPref)
|
||||
screen.addPreference(altTitlesInDescPref)
|
||||
screen.addPreference(preferExtensionLangTitlePref)
|
||||
screen.addPreference(finalChapterInDescPref)
|
||||
screen.addPreference(contentRatingPref)
|
||||
screen.addPreference(originalLanguagePref)
|
||||
screen.addPreference(blockedGroupsPref)
|
||||
|
@ -860,6 +877,9 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
|
|||
private val SharedPreferences.preferExtensionLangTitle
|
||||
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
|
||||
* 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 {
|
||||
return removeEntities()
|
||||
.substringBefore("---")
|
||||
.substringBefore("\n---")
|
||||
.replace(markdownLinksRegex, "$1")
|
||||
.replace(markdownItalicBoldRegex, "$1")
|
||||
.replace(markdownItalicRegex, "$1")
|
||||
|
@ -324,6 +324,7 @@ class MangaDexHelper(lang: String) {
|
|||
coverSuffix: String?,
|
||||
altTitlesInDesc: Boolean,
|
||||
preferExtensionLangTitle: Boolean,
|
||||
finalChapterInDesc: Boolean,
|
||||
): SManga {
|
||||
val attr = mangaDataDto.attributes!!
|
||||
|
||||
|
@ -365,9 +366,12 @@ class MangaDexHelper(lang: String) {
|
|||
|
||||
val genreList = MDConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } + nonGenres
|
||||
|
||||
var desc = (attr.description[lang] ?: attr.description["en"])
|
||||
// Build description
|
||||
val desc = mutableListOf<String>()
|
||||
|
||||
(attr.description[lang] ?: attr.description["en"])
|
||||
?.removeEntitiesAndMarkdown()
|
||||
.orEmpty()
|
||||
?.let { desc.add(it) }
|
||||
|
||||
if (altTitlesInDesc) {
|
||||
val romanizedOriginalLang = MDConstants.romanizedLangCodes[attr.originalLanguage].orEmpty()
|
||||
|
@ -379,12 +383,24 @@ class MangaDexHelper(lang: String) {
|
|||
if (altTitles.isNotEmpty()) {
|
||||
val altTitlesDesc = altTitles
|
||||
.joinToString("\n", "${intl["alternative_titles"]}\n") { "• $it" }
|
||||
desc += (if (desc.isBlank()) "" else "\n\n") + altTitlesDesc.removeEntities()
|
||||
desc.add(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 {
|
||||
description = desc
|
||||
description = desc.joinToString("\n\n")
|
||||
author = authors.joinToString()
|
||||
artist = artists.joinToString()
|
||||
status = getPublicationStatus(attr, chapters)
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
ext {
|
||||
extName = 'MangaFire'
|
||||
extClass = '.MangaFireFactory'
|
||||
themePkg = 'mangareader'
|
||||
baseUrl = 'https://mangafire.to'
|
||||
overrideVersionCode = 5
|
||||
extVersionCode = 10
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,166 +1,190 @@
|
|||
package eu.kanade.tachiyomi.extension.all.mangafire
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import okhttp3.HttpUrl
|
||||
import java.util.Calendar
|
||||
|
||||
class Entry(name: String, val id: String) : Filter.CheckBox(name) {
|
||||
constructor(name: String) : this(name, name)
|
||||
interface UriFilter {
|
||||
fun addToUri(builder: HttpUrl.Builder)
|
||||
}
|
||||
|
||||
sealed class Group(
|
||||
open class UriPartFilter(
|
||||
name: String,
|
||||
val param: String,
|
||||
values: List<Entry>,
|
||||
) : Filter.Group<Entry>(name, values)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Select(
|
||||
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
|
||||
|
||||
open class UriMultiSelectFilter(
|
||||
name: String,
|
||||
val param: String,
|
||||
private val valuesMap: Map<String, String>,
|
||||
) : Filter.Select<String>(name, valuesMap.keys.toTypedArray()) {
|
||||
open val selection: String
|
||||
get() = valuesMap[values[state]]!!
|
||||
private val param: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
val checked = state.filter { it.state }
|
||||
|
||||
checked.forEach {
|
||||
builder.addQueryParameter(param, it.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TypeFilter : Group("Type", "type[]", types)
|
||||
open class UriTriSelectOption(name: String, val value: String) : Filter.TriState(name)
|
||||
|
||||
private val types: List<Entry>
|
||||
get() = listOf(
|
||||
Entry("Manga", "manga"),
|
||||
Entry("One-Shot", "one_shot"),
|
||||
Entry("Doujinshi", "doujinshi"),
|
||||
Entry("Light-Novel", "light_novel"),
|
||||
Entry("Novel", "novel"),
|
||||
Entry("Manhwa", "manhwa"),
|
||||
Entry("Manhua", "manhua"),
|
||||
)
|
||||
|
||||
class Genre(name: String, val id: String) : Filter.TriState(name) {
|
||||
val selection: String
|
||||
get() = (if (isExcluded()) "-" else "") + id
|
||||
open class UriTriSelectFilter(
|
||||
name: String,
|
||||
private val param: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
) : Filter.Group<UriTriSelectOption>(name, vals.map { UriTriSelectOption(it.first, it.second) }), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
state.forEach { s ->
|
||||
when (s.state) {
|
||||
TriState.STATE_INCLUDE -> builder.addQueryParameter(param, s.value)
|
||||
TriState.STATE_EXCLUDE -> builder.addQueryParameter(param, "-${s.value}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GenresFilter : Filter.Group<Genre>("Genre", genres) {
|
||||
val param = "genre[]"
|
||||
class TypeFilter : UriMultiSelectFilter(
|
||||
"Type",
|
||||
"type",
|
||||
arrayOf(
|
||||
Pair("Manga", "manga"),
|
||||
Pair("One-Shot", "one_shot"),
|
||||
Pair("Doujinshi", "doujinshi"),
|
||||
Pair("Novel", "novel"),
|
||||
Pair("Manhwa", "manhwa"),
|
||||
Pair("Manhua", "manhua"),
|
||||
),
|
||||
)
|
||||
|
||||
val combineMode: Boolean
|
||||
get() = state.filter { !it.isIgnored() }.size > 1
|
||||
class GenreFilter : UriTriSelectFilter(
|
||||
"Genres",
|
||||
"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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val genres: List<Genre>
|
||||
get() = listOf(
|
||||
Genre("Action", "1"),
|
||||
Genre("Adventure", "78"),
|
||||
Genre("Avant Garde", "3"),
|
||||
Genre("Boys Love", "4"),
|
||||
Genre("Comedy", "5"),
|
||||
Genre("Demons", "77"),
|
||||
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 StatusFilter : UriMultiSelectFilter(
|
||||
"Status",
|
||||
"status[]",
|
||||
arrayOf(
|
||||
Pair("Completed", "completed"),
|
||||
Pair("Releasing", "releasing"),
|
||||
Pair("On Hiatus", "on_hiatus"),
|
||||
Pair("Discontinued", "discontinued"),
|
||||
Pair("Not Yet Published", "info"),
|
||||
),
|
||||
)
|
||||
|
||||
class StatusFilter : Group("Status", "status[]", statuses)
|
||||
class YearFilter : UriMultiSelectFilter(
|
||||
"Year",
|
||||
"year[]",
|
||||
years,
|
||||
) {
|
||||
companion object {
|
||||
private val currentYear by lazy {
|
||||
Calendar.getInstance()[Calendar.YEAR]
|
||||
}
|
||||
|
||||
private val statuses: List<Entry>
|
||||
get() = listOf(
|
||||
Entry("Completed", "completed"),
|
||||
Entry("Releasing", "releasing"),
|
||||
Entry("On Hiatus", "on_hiatus"),
|
||||
Entry("Discontinued", "discontinued"),
|
||||
Entry("Not Yet Published", "info"),
|
||||
)
|
||||
private val years: Array<Pair<String, String>> = buildList(29) {
|
||||
addAll(
|
||||
(currentYear downTo (currentYear - 20)).map(Int::toString),
|
||||
)
|
||||
|
||||
class YearFilter : Group("Year", "year[]", years)
|
||||
addAll(
|
||||
(2000 downTo 1930 step 10).map { "${it}s" },
|
||||
)
|
||||
}.map { Pair(it, it) }.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
private val years: List<Entry>
|
||||
get() = listOf(
|
||||
Entry("2023"),
|
||||
Entry("2022"),
|
||||
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"),
|
||||
)
|
||||
class MinChapterFilter : Filter.Text("Minimum chapter length"), UriFilter {
|
||||
override fun addToUri(builder: HttpUrl.Builder) {
|
||||
if (state.isNotEmpty()) {
|
||||
val value = state.toIntOrNull()?.takeIf { it > 0 }
|
||||
?: throw IllegalArgumentException("Minimum chapter length must be a positive integer greater than 0")
|
||||
|
||||
class ChapterCountFilter : Select("Chapter Count", "minchap", chapterCounts)
|
||||
builder.addQueryParameter("minchap", value.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val chapterCounts
|
||||
get() = mapOf(
|
||||
"Any" to "",
|
||||
"At least 1 chapter" to "1",
|
||||
"At least 3 chapters" to "3",
|
||||
"At least 5 chapters" to "5",
|
||||
"At least 10 chapters" to "10",
|
||||
"At least 20 chapters" to "20",
|
||||
"At least 30 chapters" to "30",
|
||||
"At least 50 chapters" to "50",
|
||||
)
|
||||
|
||||
class SortFilter : Select("Sort", "sort", orders)
|
||||
|
||||
private val orders
|
||||
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",
|
||||
)
|
||||
class SortFilter(defaultValue: String? = null) : UriPartFilter(
|
||||
"Sort",
|
||||
"sort",
|
||||
arrayOf(
|
||||
Pair("Most relevance", "most_relevance"),
|
||||
Pair("Recently updated", "recently_updated"),
|
||||
Pair("Recently added", "recently_added"),
|
||||
Pair("Release date", "release_date"),
|
||||
Pair("Trending", "trending"),
|
||||
Pair("Name A-Z", "title_az"),
|
||||
Pair("Scores", "scores"),
|
||||
Pair("MAL scores", "mal_scores"),
|
||||
Pair("Most viewed", "most_viewed"),
|
||||
Pair("Most favourited", "most_favourited"),
|
||||
),
|
||||
defaultValue,
|
||||
)
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
package eu.kanade.tachiyomi.extension.all.mangafire
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
|
||||
import android.app.Application
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
@ -18,182 +23,245 @@ import okhttp3.Response
|
|||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Evaluator
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
open class MangaFire(
|
||||
class MangaFire(
|
||||
override val lang: String,
|
||||
private val langCode: String = lang,
|
||||
) : MangaReader() {
|
||||
) : ConfigurableSource, HttpSource() {
|
||||
override val name = "MangaFire"
|
||||
|
||||
override val baseUrl = "https://mangafire.to"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
.addInterceptor(ImageInterceptor)
|
||||
.build()
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
GET("$baseUrl/filter?sort=recently_updated&language[]=$langCode&page=$page", headers)
|
||||
override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build()
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
GET("$baseUrl/filter?sort=most_viewed&language[]=$langCode&page=$page", headers)
|
||||
// ============================== Popular ===============================
|
||||
|
||||
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 {
|
||||
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
|
||||
if (query.isNotBlank()) {
|
||||
urlBuilder.addPathSegment("filter").apply {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("filter")
|
||||
|
||||
if (query.isNotBlank()) {
|
||||
addQueryParameter("keyword", query)
|
||||
addQueryParameter("page", page.toString())
|
||||
}
|
||||
} else {
|
||||
urlBuilder.addPathSegment("filter").apply {
|
||||
addQueryParameter("language[]", langCode)
|
||||
addQueryParameter("page", page.toString())
|
||||
filters.ifEmpty(::getFilterList).forEach { filter ->
|
||||
when (filter) {
|
||||
is Group -> {
|
||||
filter.state.forEach {
|
||||
if (it.state) {
|
||||
addQueryParameter(filter.param, it.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Select -> {
|
||||
addQueryParameter(filter.param, filter.selection)
|
||||
}
|
||||
is GenresFilter -> {
|
||||
filter.state.forEach {
|
||||
if (it.state != 0) {
|
||||
addQueryParameter(filter.param, it.selection)
|
||||
}
|
||||
}
|
||||
if (filter.combineMode) {
|
||||
addQueryParameter("genre_mode", "and")
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
val filterList = filters.ifEmpty { getFilterList() }
|
||||
filterList.filterIsInstance<UriFilter>().forEach {
|
||||
it.addToUri(this)
|
||||
}
|
||||
|
||||
addQueryParameter("language[]", langCode)
|
||||
addQueryParameter("page", page.toString())
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
var entries = document.select(searchMangaSelector()).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)
|
||||
}
|
||||
}
|
||||
return GET(urlBuilder.build(), headers)
|
||||
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
|
||||
return MangasPage(entries, hasNextPage)
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
|
||||
private fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
|
||||
|
||||
override fun searchMangaSelector() = ".original.card-lg .unit .inner"
|
||||
private fun searchMangaSelector() = ".original.card-lg .unit .inner"
|
||||
|
||||
override fun searchMangaFromElement(element: Element) =
|
||||
SManga.create().apply {
|
||||
element.selectFirst(".info > a")!!.let {
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
title = it.ownText()
|
||||
}
|
||||
element.selectFirst(Evaluator.Tag("img"))!!.let {
|
||||
thumbnail_url = it.attr("src")
|
||||
}
|
||||
private fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
||||
element.selectFirst(".info > a")!!.let {
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
title = it.ownText()
|
||||
}
|
||||
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
val root = document.selectFirst(".info")!!
|
||||
val mangaTitle = root.child(1).ownText()
|
||||
title = mangaTitle
|
||||
description = document.run {
|
||||
val description = selectFirst(Evaluator.Class("description"))!!.ownText()
|
||||
when (val altTitle = root.child(2).ownText()) {
|
||||
"", mangaTitle -> description
|
||||
else -> "$description\n\nAlternative Title: $altTitle"
|
||||
// =============================== 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
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
override val chapterType get() = "chapter"
|
||||
override val volumeType get() = "volume"
|
||||
private fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
with(document.selectFirst(".main-inner:not(.manga-bottom)")!!) {
|
||||
title = selectFirst("h1")!!.text()
|
||||
thumbnail_url = selectFirst(".poster img")?.attr("src")
|
||||
status = selectFirst(".info > p").parseStatus()
|
||||
description = buildString {
|
||||
document.selectFirst("#synopsis .modal-content")?.textNodes()?.let {
|
||||
append(it.joinToString("\n\n"))
|
||||
}
|
||||
|
||||
override fun chapterListRequest(mangaUrl: String, type: String): Request {
|
||||
val id = mangaUrl.substringAfterLast('.')
|
||||
return GET("$baseUrl/ajax/manga/$id/$type/$langCode", headers)
|
||||
}
|
||||
selectFirst("h6")?.let {
|
||||
append("\n\nAlternative title: ${it.text()}")
|
||||
}
|
||||
}.trim()
|
||||
|
||||
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
|
||||
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
|
||||
val document = Jsoup.parse(result)
|
||||
val selector = if (isVolume) "div.unit" else "ul li"
|
||||
val elements = document.select(selector)
|
||||
if (elements.size > 0) {
|
||||
val linkToFirstChapter = elements[0].selectFirst(Evaluator.Tag("a"))!!.attr("href")
|
||||
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"))
|
||||
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()
|
||||
}
|
||||
}
|
||||
return elements.toList()
|
||||
}
|
||||
|
||||
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||
"releasing" -> SManga.ONGOING
|
||||
"completed" -> SManga.COMPLETED
|
||||
"on_hiatus" -> SManga.ON_HIATUS
|
||||
"discontinued" -> SManga.CANCELLED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// ============================== Chapters ==============================
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String {
|
||||
return baseUrl + chapter.url.substringBeforeLast("#")
|
||||
}
|
||||
|
||||
private fun getAjaxRequest(ajaxType: String, mangaId: String, chapterType: String): Request {
|
||||
return GET("$baseUrl/ajax/$ajaxType/$mangaId/$chapterType/$langCode", headers)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterIdsDto(
|
||||
class AjaxReadDto(
|
||||
val html: String,
|
||||
val title_format: String,
|
||||
)
|
||||
|
||||
override fun updateChapterList(manga: SManga, chapters: List<SChapter>) {
|
||||
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 chapterListParse(response: Response): List<SChapter> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
val elements = document.selectFirst(".scroll-sm")!!.children()
|
||||
val chapterCount = chapters.size
|
||||
if (elements.size != chapterCount) throw Exception("Chapter count doesn't match. Try updating again.")
|
||||
val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.US)
|
||||
for (i in 0 until chapterCount) {
|
||||
val chapter = chapters[i]
|
||||
val element = elements[i]
|
||||
val number = element.attr("data-number").toFloatOrNull() ?: -1f
|
||||
if (chapter.chapter_number != number) throw Exception("Chapter number doesn't match. Try updating again.")
|
||||
chapter.name = element.select(Evaluator.Tag("span"))[0].ownText()
|
||||
val date = element.select(Evaluator.Tag("span"))[1].ownText()
|
||||
chapter.date_upload = try {
|
||||
dateFormat.parse(date)!!.time
|
||||
} catch (_: Throwable) {
|
||||
0
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
val path = manga.url
|
||||
val mangaId = path.removeSuffix(VOLUME_URL_SUFFIX).substringAfterLast(".")
|
||||
val isVolume = path.endsWith(VOLUME_URL_SUFFIX)
|
||||
|
||||
val type = if (isVolume) "volume" else "chapter"
|
||||
val abbrPrefix = if (isVolume) "Vol" else "Chap"
|
||||
val fullPrefix = if (isVolume) "Volume" else "Chapter"
|
||||
|
||||
val ajaxMangaList = client.newCall(getAjaxRequest("manga", mangaId, type))
|
||||
.execute().parseAs<ResponseDto<String>>().result
|
||||
.toBodyFragment()
|
||||
.select(if (isVolume) ".vol-list > .item" else "li")
|
||||
|
||||
val ajaxReadList = client.newCall(getAjaxRequest("read", mangaId, type))
|
||||
.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 {
|
||||
val typeAndId = chapter.url.substringAfterLast('#')
|
||||
return GET("$baseUrl/ajax/read/$typeAndId", headers)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val result = json.decodeFromString<ResponseDto<PageListDto>>(response.body.string()).result
|
||||
val result = response.parseAs<ResponseDto<PageListDto>>().result
|
||||
|
||||
return result.pages.mapIndexed { index, image ->
|
||||
val url = image.url
|
||||
|
@ -206,27 +274,49 @@ open class MangaFire(
|
|||
|
||||
@Serializable
|
||||
class PageListDto(private val images: List<List<JsonPrimitive>>) {
|
||||
val pages get() = images.map {
|
||||
Image(it[0].content, it[2].int)
|
||||
}
|
||||
val pages
|
||||
get() = images.map {
|
||||
Image(it[0].content, it[2].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
|
||||
class ResponseDto<T>(
|
||||
val result: T,
|
||||
val status: Int,
|
||||
)
|
||||
|
||||
override fun getFilterList() =
|
||||
FilterList(
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
Filter.Separator(),
|
||||
TypeFilter(),
|
||||
GenresFilter(),
|
||||
StatusFilter(),
|
||||
YearFilter(),
|
||||
ChapterCountFilter(),
|
||||
SortFilter(),
|
||||
)
|
||||
private inline fun <reified T> Response.parseAs(): T {
|
||||
return json.decodeFromString(body.string())
|
||||
}
|
||||
|
||||
private fun String.toBodyFragment(): Document {
|
||||
return Jsoup.parseBodyFragment(this, baseUrl)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.US)
|
||||
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] "
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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=".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>
|
|
@ -1,6 +1,6 @@
|
|||
ext {
|
||||
extName = 'Manga Latino'
|
||||
extClass = '.MangaLatino'
|
||||
extName = 'Manga Hosted'
|
||||
extClass = '.MangaHostedFactory'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 8.5 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,202 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
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)
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
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,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.extension.all.ninenineninehentai
|
||||
package eu.kanade.tachiyomi.extension.all.mangahosted
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
|
@ -7,28 +7,30 @@ import android.os.Bundle
|
|||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class AnimeHUrlActivity : Activity() {
|
||||
class MangaHostedUrlActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
|
||||
if (pathSegments != null && pathSegments.size > 1) {
|
||||
val id = pathSegments[1]
|
||||
val mainIntent = Intent().apply {
|
||||
val intent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${AnimeH.SEARCH_PREFIX}$id")
|
||||
putExtra("query", slug(pathSegments))
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("AnimeHUrlActivity", e.toString())
|
||||
Log.e("UnionMangasUrlActivity", e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e("AnimeHUrlActivity", "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
private fun slug(pathSegments: List<String>) =
|
||||
"${MangaHosted.SEARCH_PREFIX}${pathSegments[1]}"
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
## 1.4.7
|
||||
|
||||
- Reworked the lib-multisrc theme
|
||||
|
||||
## 1.3.4
|
||||
|
||||
- Refactor and make multisrc
|
||||
|
|
|
@ -3,7 +3,7 @@ ext {
|
|||
extClass = '.MangaReaderFactory'
|
||||
themePkg = 'mangareader'
|
||||
baseUrl = 'https://mangareader.to'
|
||||
overrideVersionCode = 4
|
||||
overrideVersionCode = 5
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|