Compare commits

..

No commits in common. "2cfdda0bcff616fe7759bfa692761886afd2b1aa" and "dfb448cbffa8cd9ca190cbd1f689911d2f02c2ab" have entirely different histories.

1466 changed files with 15124 additions and 5994 deletions

1
.gitignore vendored
View File

@ -10,4 +10,3 @@ repo/
apk/
gen
generated-src/
.kotlin

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@ -38,7 +38,6 @@ kotlinter {
dependencies {
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
implementation(project(":utils"))
}
tasks {
@ -52,9 +51,3 @@ tasks {
}
}
}
tasks.register("printDependentExtensions") {
doLast {
project.printDependentExtensions()
}
}

View File

@ -103,7 +103,6 @@ dependencies {
if (theme != null) implementation(theme) // Overrides core launcher icons
implementation(project(":core"))
compileOnly(libs.bundles.common)
implementation(project(":utils"))
}
tasks.register("writeManifestFile") {

View File

@ -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.keiyoushi:extensions-lib", version = "v1.4.2.1" }
tachiyomi-lib = { module = "com.github.tachiyomiorg:extensions-lib", version = "1.4.2" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin_version" }
kotlin-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization_version" }

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 7
baseVersionCode = 5
dependencies {
api(project(":lib:synchrony"))

View File

@ -276,14 +276,14 @@ abstract class ColaManga(
}.also(screen::addPreference)
}
private val keyMappingRegex = Regex("""if\s*\(\s*([a-zA-Z0-9_]+)\s*==\s*(\d+)\s*\)\s*\{\s*return\s*'([a-zA-Z0-9_]+)'\s*;""")
private val keyMappingRegex = Regex("""if\s*\(\s*([a-zA-Z0-9_]+)\s*==\s*(?<keyType>\d+)\s*\)\s*\{\s*return\s*'(?<key>[a-zA-Z0-9_]+)'\s*;""")
private val keyMapping by lazy {
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[2]!!.value to it.groups[3]!!.value }
keyMappingRegex.findAll(readJs).associate { it.groups["keyType"]!!.value to it.groups["key"]!!.value }
}
private fun randomString() = buildString(15) {

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="eu.kanade.tachiyomi.multisrc.etoshore.EtoshoreUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="${SOURCEHOST}"
android:pathPattern="/.*/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,5 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -0,0 +1,242 @@
package eu.kanade.tachiyomi.multisrc.etoshore
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
abstract class Etoshore(
override val name: String,
override val baseUrl: String,
final override val lang: String,
) : ParsedHttpSource() {
override val supportsLatest = true
override val client = network.cloudflareClient
// ============================== Popular ==============================
open val popularFilter = FilterList(
SelectionList("", listOf(Tag(value = "views", query = "sort"))),
)
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
override fun popularMangaParse(response: Response) = searchMangaParse(response)
override fun popularMangaSelector() = throw UnsupportedOperationException()
override fun popularMangaNextPageSelector() = throw UnsupportedOperationException()
override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException()
// ============================== Latest ===============================
open val latestFilter = FilterList(
SelectionList("", listOf(Tag(value = "date", query = "sort"))),
)
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
// ============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/page/$page".toHttpUrl().newBuilder()
.addQueryParameter("s", query)
filters.forEach { filter ->
when (filter) {
is SelectionList -> {
val selected = filter.selected()
url.addQueryParameter(selected.query, selected.value)
}
else -> {}
}
}
return GET(url.build(), headers)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(PREFIX_SEARCH)) {
val slug = query.substringAfter(PREFIX_SEARCH)
return fetchMangaDetails(SManga.create().apply { url = "/manga/$slug/" })
.map { manga -> MangasPage(listOf(manga), false) }
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaSelector() = ".search-posts .chapter-box .poster a"
override fun searchMangaNextPageSelector() = ".navigation .naviright:has(a)"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
title = element.attr("title")
thumbnail_url = element.selectFirst("img")?.let(::imageFromElement)
setUrlWithoutDomain(element.absUrl("href"))
}
override fun searchMangaParse(response: Response): MangasPage {
if (filterList.isEmpty()) {
filterParse(response)
}
return super.searchMangaParse(response)
}
// ============================== Details ===============================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("h1")!!.text()
description = document.selectFirst(".excerpt p")?.text()
document.selectFirst(".details-right-con img")?.let { thumbnail_url = imageFromElement(it) }
genre = document.select("div.meta-item span.meta-title:contains(Genres) + span a")
.joinToString { it.text() }
author = document.selectFirst("div.meta-item span.meta-title:contains(Author) + span a")
?.text()
document.selectFirst(".status")?.text()?.let {
status = it.toMangaStatus()
}
setUrlWithoutDomain(document.location())
}
protected open fun imageFromElement(element: Element): String? {
return when {
element.hasAttr("data-src") -> element.attr("abs:data-src")
element.hasAttr("data-lazy-src") -> element.attr("abs:data-lazy-src")
element.hasAttr("srcset") -> element.attr("abs:srcset").getSrcSetImage()
element.hasAttr("data-cfsrc") -> element.attr("abs:data-cfsrc")
else -> element.attr("abs:src")
}
}
protected open fun String.getSrcSetImage(): String? {
return this.split(" ")
.filter(URL_REGEX::matches)
.maxOfOrNull(String::toString)
}
protected val completedStatusList: Array<String> = arrayOf(
"Finished",
"Completo",
)
protected open val ongoingStatusList: Array<String> = arrayOf(
"Publishing",
"Ativo",
)
protected val hiatusStatusList: Array<String> = arrayOf(
"on hiatus",
)
protected val canceledStatusList: Array<String> = arrayOf(
"Canceled",
"Discontinued",
)
open fun String.toMangaStatus(): Int {
return when {
containsIn(completedStatusList) -> SManga.COMPLETED
containsIn(ongoingStatusList) -> SManga.ONGOING
containsIn(hiatusStatusList) -> SManga.ON_HIATUS
containsIn(canceledStatusList) -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
// ============================== Chapters ============================
override fun chapterListSelector() = ".chapter-list li a"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
name = element.selectFirst(".title")!!.text()
setUrlWithoutDomain(element.absUrl("href"))
}
// ============================== Pages ===============================
override fun pageListParse(document: Document): List<Page> {
return document.select(".chapter-images .chapter-item > img").mapIndexed { index, element ->
Page(index, imageUrl = imageFromElement(element))
}
}
override fun imageUrlParse(document: Document) = ""
// ============================= Filters ==============================
private var filterList = emptyList<Pair<String, List<Tag>>>()
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<*>>()
filters += if (filterList.isNotEmpty()) {
filterList.map { SelectionList(it.first, it.second) }
} else {
listOf(Filter.Header("Aperte 'Redefinir' para tentar mostrar os filtros"))
}
return FilterList(filters)
}
protected open fun parseSelection(document: Document, selector: String): Pair<String, List<Tag>>? {
val selectorFilter = "#filter-form $selector .select-item-head .text"
return document.selectFirst(selectorFilter)?.text()?.let { displayName ->
displayName to document.select("#filter-form $selector li").map { element ->
element.selectFirst("input")!!.let { input ->
Tag(
name = element.selectFirst(".text")!!.text(),
value = input.attr("value"),
query = input.attr("name"),
)
}
}
}
}
open val filterListSelector: List<String> = listOf(
".filter-genre",
".filter-status",
".filter-type",
".filter-year",
".filter-sort",
)
open fun filterParse(response: Response) {
val document = Jsoup.parseBodyFragment(response.peekBody(Long.MAX_VALUE).string())
filterList = filterListSelector.mapNotNull { selector -> parseSelection(document, selector) }
}
protected data class Tag(val name: String = "", val value: String = "", val query: String = "")
private open class SelectionList(displayName: String, private val vals: List<Tag>, state: Int = 0) :
Filter.Select<String>(displayName, vals.map { it.name }.toTypedArray(), state) {
fun selected() = vals[state]
}
// ============================= Utils ==============================
private fun String.containsIn(array: Array<String>): Boolean {
return this.lowercase() in array.map { it.lowercase() }
}
companion object {
const val PREFIX_SEARCH = "id:"
val URL_REGEX = """^(https?://[^\s/$.?#].[^\s]*)${'$'}""".toRegex()
}
}

View File

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.multisrc.etoshore
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class EtoshoreUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${Etoshore.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 30
baseVersionCode = 28

View File

@ -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-6 .user-rating meta[itemprop=\"ratingCount\"]")
infoElement.select(".col-sm-7 .user-rating meta[itemprop=\"ratingCount\"]")
.attr("content")
val ratingStar = when {
ratingValue > 9.5 -> "★★★★★"
@ -209,16 +209,14 @@ abstract class GroupLe(
}
protected open fun getChapterSearchParams(document: Document): String {
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"
return "?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("МинтМанга") || contains("RuMix") }
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
) {
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
}
@ -311,7 +309,7 @@ abstract class GroupLe(
val html = document.html()
if (document.select(".user-avatar").isEmpty() &&
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") || contains("RuMix") }
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
) {
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
@ -324,9 +322,6 @@ 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("Дизайн сайта обновлен, для дальнейшей работы необходимо обновление дополнения")
}
}
@ -441,6 +436,5 @@ 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()
}
}

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 28
baseVersionCode = 27
dependencies {
api(project(":lib:i18n"))

View File

@ -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? = null,
@SerialName("created_at") private val createdAt: String,
val price: Int? = null,
) {
fun toSChapter(

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 18
baseVersionCode = 16

View File

@ -107,7 +107,7 @@ open class Kemono(
}
var mangas = mangasCache
if (page == 1 || mangasCache.isEmpty()) {
if (page == 1) {
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 = it }
}.also { mangasCache = mangas }
}
val sorted = when (sort.first) {

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 13
baseVersionCode = 12
dependencies {
api(project(":lib:i18n"))

View File

@ -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(1)?.value
?.groups?.get("host")?.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(1)?.value
IMG_REGEX.find(element.attr("style"))?.groups?.get("url")?.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*`[^`]+//([^/]+)""".toRegex()
val CDN_HOST_REGEX = """realUrl\s*=\s*`[^`]+//(?<host>[^/]+)""".toRegex()
val CDN_CLEAN_REGEX = """\$\{[^}]*\}""".toRegex()
val IMG_REGEX = """url\(['"]?([^(['"\)])]+)""".toRegex()
val IMG_REGEX = """url\(['"]?(?<url>[^(['"\)])]+)""".toRegex()
}
}

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 35
baseVersionCode = 34

View File

@ -313,33 +313,37 @@ 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>>>()
.also { if (it.data.isEmpty()) return emptyList() }
if (chaptersData.data.isEmpty()) {
throw Exception("Нет глав")
}
val sortingList = preferences.getString(SORTING_PREF, "ms_mixing")
val defaultBranchId = if (sortingList == "ms_mixing" && chaptersData.data.getBranchCount() > 1) {
val defaultBranchId = if (chaptersData.data.getBranchCount() > 1) { // excess request if branchesCount is only alone = slow update library witch rateLimitHost(apiDomain.toHttpUrl(), 1)
runCatching { getDefaultBranch(slugUrl.substringBefore("-")).first().id }.getOrNull()
} else {
null
}
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())
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()))
}
}
else -> listOf(chapter.toSChapter(slugUrl, isScanUser = isScanUser()))
} else {
chapters.add(it.value.toSChapter(slugUrl, isScanUser = isScanUser()))
}
}.reversed()
}
return chapters.reversed()
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {

View File

@ -1,6 +0,0 @@
font_size_title=Font size
font_size_summary=Font changes will not be applied to downloaded or cached chapters. The font size will be adjusted according to the size of the dialog box.
font_size_message=Font size changed to %s
default_font_size=Default
disable_website_setting_title=Disable source settings
disable_website_setting_summary=Site fonts will be disabled and your device's fonts will be applied. This does not apply to downloaded or cached chapters.

View File

@ -1 +0,0 @@
font_size_title=Tamaño de letra

View File

@ -1 +0,0 @@
font_size_title=Taille de la police

View File

@ -1 +0,0 @@
font_size_title=Ukuran font

View File

@ -1 +0,0 @@
font_size_title=Dimensione del carattere

View File

@ -1,6 +0,0 @@
font_size_title=Tamanho da fonte
font_size_summary=As alterações de fonte não serão aplicadas aos capítulos baixados ou armazenados em cache. O tamanho da fonte será ajustado de acordo com o tamanho da caixa de diálogo.
font_size_message=Tamanho da fonte foi alterada para %s
default_font_size=Padrão
disable_website_setting_title=Desativar configurações do site
disable_website_setting_summary=As fontes do site serão desativadas e as fontes de seu dispositivo serão aplicadas. Isso não se aplica a capítulos baixados ou armazenados em cache.

View File

@ -2,8 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 4
dependencies {
api(project(":lib:i18n"))
}
baseVersionCode = 2

View File

@ -1,18 +1,9 @@
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
@ -24,26 +15,21 @@ 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,
private val language: Language,
) : ParsedHttpSource(), ConfigurableSource {
val language: Language,
) : ParsedHttpSource() {
override val supportsLatest = true
@ -51,66 +37,9 @@ abstract class MachineTranslations(
override val lang = language.lang
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)
}
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(ComposedImageInterceptor(baseUrl, language))
.build()
// ============================== Popular ===============================
@ -274,76 +203,9 @@ 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)
}
}

View File

@ -2,18 +2,4 @@ package eu.kanade.tachiyomi.multisrc.machinetranslations
class MachineTranslationsFactoryUtils
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
data class Language(val lang: String, val target: String = lang, val origin: String = "en")

View File

@ -26,13 +26,16 @@ 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,
var language: Language,
val language: Language,
) : Interceptor {
private val json: Json by injectLazy()
@ -52,7 +55,7 @@ class ComposedImageInterceptor(
}
val dialogues = request.url.fragment?.parseAs<List<Dialog>>()
?: emptyList()
?: throw IOException("Dialogues not found")
val imageRequest = request.newBuilder()
.url(url)
@ -60,9 +63,7 @@ class ComposedImageInterceptor(
// Load the fonts before opening the connection to load the image,
// so there aren't two open connections inside the interceptor.
if (language.disableSourceSettings.not()) {
loadAllFont(chain)
}
loadAllFont(chain)
val response = chain.proceed(imageRequest)
@ -77,9 +78,9 @@ class ComposedImageInterceptor(
dialogues.forEach { dialog ->
val textPaint = createTextPaint(selectFontFamily(dialog.type))
val dialogBox = createDialogBox(dialog, textPaint)
val dialogBox = createDialogBox(dialog, textPaint, bitmap)
val y = getYAxis(textPaint, dialog, dialogBox)
canvas.draw(textPaint, dialogBox, dialog, dialog.x1, y)
canvas.draw(dialogBox, dialog, dialog.x1, y)
}
val output = ByteArrayOutputStream()
@ -103,7 +104,7 @@ class ComposedImageInterceptor(
}
private fun createTextPaint(font: Typeface?): TextPaint {
val defaultTextSize = language.fontSize.pt
val defaultTextSize = 24.pt // arbitrary
return TextPaint().apply {
color = Color.BLACK
textSize = defaultTextSize
@ -115,10 +116,6 @@ class ComposedImageInterceptor(
}
private fun selectFontFamily(type: String): Typeface? {
if (language.disableSourceSettings) {
return null
}
if (type in fontFamily) {
return fontFamily[type]?.second
}
@ -209,7 +206,7 @@ class ComposedImageInterceptor(
}
}
private fun createDialogBox(dialog: Dialog, textPaint: TextPaint): StaticLayout {
private fun createDialogBox(dialog: Dialog, textPaint: TextPaint, bitmap: Bitmap): StaticLayout {
var dialogBox = createBoxLayout(dialog, textPaint)
/**
@ -220,8 +217,18 @@ class ComposedImageInterceptor(
dialogBox = createBoxLayout(dialog, textPaint)
}
textPaint.color = Color.BLACK
textPaint.bgColor = Color.WHITE
// 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)
return dialogBox
}
@ -234,46 +241,59 @@ 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(textPaint: TextPaint, layout: StaticLayout, dialog: Dialog, x: Float, y: Float) {
private fun Canvas.draw(layout: StaticLayout, dialog: Dialog, x: Float, y: Float) {
save()
translate(x, y)
rotate(dialog.angle)
drawTextOutline(textPaint, layout)
drawText(textPaint, layout)
layout.draw(this)
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

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 40
baseVersionCode = 37
dependencies {
api(project(":lib:cryptoaes"))

View File

@ -82,12 +82,7 @@ abstract class Madara(
/**
* Automatically fetched genres from the source to be used in the filters.
*/
protected open var genresList: List<Genre> = emptyList()
/**
* Whether genres have been fetched
*/
private var genresFetched: Boolean = false
private var genresList: List<Genre> = emptyList()
/**
* Inner variable to control how much tries the genres request was called.
@ -242,14 +237,11 @@ abstract class Madara(
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(URL_SEARCH_PREFIX)) {
val mangaUrl = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment(mangaSubString)
addPathSegment(query.substringAfter(URL_SEARCH_PREFIX))
}.build()
return client.newCall(GET(mangaUrl, headers))
val mangaUrl = "/$mangaSubString/${query.substringAfter(URL_SEARCH_PREFIX)}/"
return client.newCall(GET("$baseUrl$mangaUrl", headers))
.asObservableSuccess().map { response ->
val manga = mangaDetailsParse(response).apply {
setUrlWithoutDomain(mangaUrl.toString())
url = mangaUrl
}
MangasPage(listOf(manga), false)
@ -586,13 +578,11 @@ 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(searchMangaUrlSelector)!!.let {
selectFirst("div.post-title a")!!.let {
manga.setUrlWithoutDomain(it.attr("abs:href"))
manga.title = it.ownText()
}
@ -633,7 +623,7 @@ abstract class Madara(
"Em Andamento", "En cours", "En Cours", "En cours de publication", "Ativo", "Lançando", "Đang Tiến Hành", "Devam Ediyor",
"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", "Atualizações Semanais",
"Đang làm", "Em postagem", "Devam Eden", "Em progresso", "Em curso",
)
protected val hiatusStatusList: Array<String> = arrayOf(
@ -688,7 +678,7 @@ abstract class Madara(
manga.thumbnail_url = imageFromElement(it)
}
select(mangaDetailsSelectorStatus).last()?.let {
manga.status = with(it.text().filter { ch -> ch.isLetterOrDigit() || ch.isWhitespace() }.trim()) {
manga.status = with(it.text()) {
when {
containsIn(completedStatusList) -> SManga.COMPLETED
containsIn(ongoingStatusList) -> SManga.ONGOING
@ -752,7 +742,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, div.manga-authors > a"
open val mangaDetailsSelectorAuthor = "div.author-content > 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"
@ -786,7 +776,7 @@ abstract class Madara(
/**
* Get the best image quality available from srcset
*/
protected fun String.getSrcSetImage(): String? {
private fun String.getSrcSetImage(): String? {
return this.split(" ")
.filter(URL_REGEX::matches)
.maxOfOrNull(String::toString)
@ -930,10 +920,6 @@ 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 {
@ -1077,17 +1063,10 @@ abstract class Madara(
* Fetch the genres from the source to be used in the filters.
*/
protected fun fetchGenres() {
if (fetchGenres && fetchGenresAttempts < 3 && !genresFetched) {
if (fetchGenres && fetchGenresAttempts < 3 && genresList.isEmpty()) {
try {
client.newCall(genresRequest()).execute()
genresList = client.newCall(genresRequest()).execute()
.use { parseGenres(it.asJsoup()) }
.also {
genresFetched = true
}
.takeIf { it.isNotEmpty() }
?.also {
genresList = it
}
} catch (_: Exception) {
} finally {
fetchGenresAttempts++

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 17
baseVersionCode = 14

View File

@ -11,8 +11,11 @@ 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
@ -22,6 +25,7 @@ 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
@ -51,6 +55,8 @@ abstract class MadTheme(
add("Referer", "$baseUrl/")
}
private val json: Json by injectLazy()
private var genreKey = "genre[]"
// Popular
@ -171,53 +177,19 @@ abstract class MadTheme(
}
override fun chapterListParse(response: Response): List<SChapter> {
if (response.request.url.fragment == "idFound") {
if (response.code in 200..299) {
return super.chapterListParse(response)
}
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}")
// Try to show message/error from site
response.body.let { body ->
json.decodeFromString<JsonObject>(body.string())["message"]
?.jsonPrimitive
?.content
?.let { throw Exception(it) }
}
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()
throw Exception("HTTP error ${response.code}")
}
override fun chapterListRequest(manga: SManga): Request =
@ -225,11 +197,10 @@ 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${manga.url}", headers)
} ?: GET("$baseUrl/api/manga${manga.url}/chapters?source=detail", headers)
override fun searchMangaParse(response: Response): MangasPage {
if (genresList == null) {

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 14
baseVersionCode = 13

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 3
baseVersionCode = 2

View File

@ -1,356 +1,129 @@
package eu.kanade.tachiyomi.multisrc.mangareader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import android.app.Application
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.source.ConfigurableSource
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.nodes.TextNode
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import org.jsoup.select.Evaluator
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
abstract class MangaReader(
override val name: String,
override val baseUrl: String,
final override val lang: String,
) : HttpSource() {
abstract class MangaReader : HttpSource(), ConfigurableSource {
override val supportsLatest = true
override val client = network.cloudflareClient
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 latestUpdatesParse(response: Response) = searchMangaParse(response)
final override fun popularMangaParse(response: Response) = searchMangaParse(response)
// =============================== 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)
}
}
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 {
final override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val entries = document.select(searchMangaSelector())
.map(::searchMangaFromElement)
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)
}
}
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
return MangasPage(entries, hasNextPage)
}
// =========================== Manga Details ============================
final override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX)
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
abstract fun searchMangaSelector(): String
private val authorText: String = when (lang) {
"ja" -> "著者"
else -> "Authors"
}
abstract fun searchMangaNextPageSelector(): String
private val statusText: String = when (lang) {
"ja" -> "地位"
else -> "Status"
}
abstract fun searchMangaFromElement(element: Element): SManga
override fun mangaDetailsParse(response: Response): SManga {
abstract fun mangaDetailsParse(document: Document): SManga
final override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
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)
}
}
}
val manga = mangaDetailsParse(document)
if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
manga.title = VOLUME_TITLE_PREFIX + manga.title
}
}
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
}
private fun Element.parseStatus(manga: SManga): SManga {
manga.status = this.selectFirst(".name")?.text().getStatus()
return manga
}
abstract val chapterType: String
abstract val volumeType: String
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 chapterListRequest(mangaUrl: String, type: String): Request
// ============================== Chapters ==============================
abstract fun parseChapterElements(response: Response, isVolume: Boolean): List<Element>
override fun getChapterUrl(chapter: SChapter): String {
return baseUrl + chapter.url.substringBeforeLast('#')
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
open val chapterIdSelect = "en-chapters"
open fun updateChapterList(manga: SManga, chapters: List<SChapter>) = Unit
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()
}
}
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()
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return document.select("#$chapterIdSelect > li.chapter-item").map(::chapterFromElement)
}
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
// =============================== 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")
}
}
open fun getAjaxUrl(id: String): String {
return "$baseUrl//ajax/image/list/$id?mode=vertical"
}
override fun pageListRequest(chapter: SChapter): Request {
val chapterId = chapter.url.substringAfterLast('#').ifEmpty {
getChapterId(chapter)
}
val ajaxHeaders = super.headersBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Referer", URLEncoder.encode(baseUrl + chapter.url.substringBeforeLast("#"), "utf-8"))
add("X-Requested-With", "XMLHttpRequest")
}.build()
return GET(getAjaxUrl(chapterId), ajaxHeaders)
}
open fun pageListParseSelector(): String = ".container-reader-chapter > div > img"
override fun pageListParse(response: Response): List<Page> {
val document = response.parseHtmlProperty()
val pageList = document.select(pageListParseSelector()).mapIndexed { index, element ->
val imgUrl = element.imgAttr().ifEmpty {
element.selectFirst("img")!!.imgAttr()
}
Page(index, imageUrl = imgUrl)
}
return pageList
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
// ============================= Utilities ==============================
open fun Element.imgAttr(): String = when {
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("data-src") -> attr("abs:data-src")
hasAttr("data-url") -> attr("abs:data-url")
else -> attr("abs:src")
}
open fun Response.parseHtmlProperty(): Document {
val html = json.parseToJsonElement(body.string()).jsonObject["html"]!!.jsonPrimitive.content
return Jsoup.parseBodyFragment(html)
}
// =============================== Filters ==============================
object Note : Filter.Header("NOTE: Ignored if using text search!")
interface UriFilter {
fun addToUri(builder: HttpUrl.Builder)
}
open class UriPartFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : Filter.Select<String>(
name,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
),
UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
builder.addQueryParameter(param, vals[state].second)
}
}
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
open class UriMultiSelectFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
private val join: String? = null,
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
val checked = state.filter { it.state }
if (join == null) {
checked.forEach {
builder.addQueryParameter(param, it.value)
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"
}
} else {
builder.addQueryParameter(param, checked.joinToString(join) { it.value })
setUrlWithoutDomain(link.attr("href") + '#' + type + '/' + element.attr("data-id"))
}
}
}.also { if (!isVolume && it.isNotEmpty()) updateChapterList(manga, it) }
}
open class SortFilter(
title: String,
param: String,
values: Array<Pair<String, String>>,
default: String? = null,
) : UriPartFilter(title, param, values, default)
final override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast('#')
private val sortFilterName: String = when (lang) {
"ja" -> "選別"
else -> "Sort"
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
}
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),
)
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)
}
open fun getSortFilter() = SortFilter(sortFilterName, sortFilterParam, sortFilterValues())
companion object {
private const val SHOW_VOLUME_PREF = "show_volume"
override fun getFilterList(): FilterList = FilterList(
getSortFilter(),
)
private const val VOLUME_URL_FRAGMENT = "vol"
private const val VOLUME_URL_SUFFIX = "#" + VOLUME_URL_FRAGMENT
private const val VOLUME_TITLE_PREFIX = "[VOL] "
}
}

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 3
baseVersionCode = 2

View File

@ -1,35 +0,0 @@
package eu.kanade.tachiyomi.multisrc.mangaworld
import eu.kanade.tachiyomi.network.GET
import okhttp3.Cookie
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
class CookieRedirectInterceptor(private val client: OkHttpClient) : Interceptor {
private val cookieRegex = Regex("""document\.cookie="(MWCookie[^"]+)""")
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
// ignore requests that already have completed the JS challenge
if (response.headers["vary"] != null) return response
val content = response.body.string()
val results = cookieRegex.find(content)
?: return response.newBuilder().body(content.toResponseBody(response.body.contentType())).build()
val (cookieString) = results.destructured
return chain.proceed(loadCookie(request, cookieString))
}
private fun loadCookie(request: Request, cookieString: String): Request {
val cookie = Cookie.parse(request.url, cookieString)!!
client.cookieJar.saveFromResponse(request.url, listOf(cookie))
val headers = request.headers.newBuilder()
.add("Cookie", cookie.toString())
.build()
return GET(request.url, headers)
}
}

View File

@ -28,11 +28,7 @@ abstract class MangaWorld(
) : ParsedHttpSource() {
override val supportsLatest = true
// 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()
override val client: OkHttpClient = network.cloudflareClient
companion object {
protected val CHAPTER_NUMBER_REGEX by lazy { Regex("""(?i)capitolo\s([0-9]+)""") }

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 4
baseVersionCode = 3

View File

@ -267,7 +267,7 @@ abstract class SlimeReadTheme(
companion object {
const val PREFIX_SEARCH = "id:"
val FUNCTION_REGEX = """(\[""\.concat\("[^,]+,"\."\)\.concat\(([^,]+),":\d+"\)\])""".toRegex(RegexOption.DOT_MATCHES_ALL)
val FUNCTION_REGEX = """(?<script>\[""\.concat\("[^,]+,"\."\)\.concat\((?<infix>[^,]+),":\d+"\)\])""".toRegex(RegexOption.DOT_MATCHES_ALL)
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
}
}

View File

@ -1,5 +1,4 @@
include(":core")
include(":utils")
// Load all modules under /lib
File(rootDir, "lib").eachDir { include("lib:${it.name}") }

View File

@ -14,12 +14,8 @@
<data android:host="*.bato.to" />
<data android:host="bato.to" />
<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="*.batocc.com" />
<data android:host="batocc.com" />
<data android:host="*.batotoo.com" />
<data android:host="batotoo.com" />
<data android:host="*.batotwo.com" />
@ -28,40 +24,18 @@
<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="*.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="*.mycordant.co.uk" />
<data android:host="mycordant.co.uk" />
<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" />

View File

@ -1,7 +1,7 @@
ext {
extName = 'Bato.to'
extClass = '.BatoToFactory'
extVersionCode = 48
extVersionCode = 46
isNsfw = true
}

View File

@ -5,7 +5,6 @@ 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
@ -101,25 +100,11 @@ open class BatoTo(
if (current.isNotEmpty()) {
return current
}
field = getMirrorPref()
field = getMirrorPref()!!
return field
}
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 getMirrorPref(): String? = preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
private fun getAltChapterListPref(): Boolean = preferences.getBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE)
private fun isRemoveTitleVersion(): Boolean {
return preferences.getBoolean("${REMOVE_TITLE_VERSION_PREF}_$lang", false)
@ -342,12 +327,6 @@ 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)
@ -358,8 +337,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.selectFirst("div.attr-item:contains(original work) span")?.text()
val uploadStatus = infoElement.selectFirst("div.attr-item:contains(upload status) span")?.text()
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 originalTitle = infoElement.select("h3").text().removeEntities()
val description = buildString {
append(infoElement.select("div.limit-html").text())
@ -367,13 +346,13 @@ open class BatoTo(
append("\n\n${it.text()}")
}
infoElement.selectFirst("h5:containsOwn(Extra Info:) + div")?.also {
append("\n\nExtra Info:\n${it.wholeText()}")
append("\n\nExtra Info:\n${it.text()}")
}
document.selectFirst("div.pb-2.alias-set.line-b-f")?.takeIf { it.hasText() }?.also {
document.selectFirst("div.pb-2.alias-set.line-b-f")?.also {
append("\n\nAlternative Titles:\n")
append(it.text().split('/').joinToString("\n") { "${it.trim()}" })
}
}.trim()
}
val cleanedTitle = if (isRemoveTitleVersion()) {
originalTitle.replace(titleRegex, "").trim()
@ -390,19 +369,16 @@ open class BatoTo(
manga.thumbnail_url = document.select("div.attr-cover img").attr("abs:src")
return manga
}
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
private fun parseStatus(workStatus: String?, uploadStatus: String?) = when {
workStatus == null -> SManga.UNKNOWN
workStatus.contains("Ongoing") -> SManga.ONGOING
workStatus.contains("Cancelled") -> SManga.CANCELLED
workStatus.contains("Hiatus") -> SManga.ON_HIATUS
workStatus.contains("Completed") -> when {
uploadStatus?.contains("Ongoing") == true -> SManga.PUBLISHING_FINISHED
else -> SManga.COMPLETED
}
else -> SManga.UNKNOWN
}
private fun altChapterParse(response: Response): List<SChapter> {
@ -435,12 +411,6 @@ 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)
@ -537,12 +507,6 @@ 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)
@ -1037,7 +1001,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(
"Auto",
"zbato.org",
"batocomic.com",
"batocomic.net",
"batocomic.org",
@ -1049,25 +1013,23 @@ 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",

View File

@ -1,9 +0,0 @@
ext {
extName = 'Comic Growl'
extClass = '.ComicGrowl'
themePkg = 'gigaviewer'
baseUrl = 'https://comic-growl.com'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,63 +0,0 @@
package eu.kanade.tachiyomi.extension.all.comicgrowl
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import org.jsoup.nodes.Element
// TODO: get manga status
// TODO: filter by status
// TODO: change cdnUrl as a array(upstream)
class ComicGrowl : GigaViewer(
"コミックグロウル",
"https://comic-growl.com",
"all",
"https://cdn-img.comic-growl.com/public/page",
) {
override val publisher = "BUSHIROAD WORKS"
override val chapterListMode = CHAPTER_LIST_LOCKED
override val supportsLatest: Boolean = true
override val client: OkHttpClient =
super.client.newBuilder().addInterceptor(::imageIntercept).build()
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
// Show only ongoing works
override fun popularMangaSelector(): String = "ul[class=\"lineup-list ongoing\"] > li > div > a"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
title = element.select("h5").text()
thumbnail_url = element.select("div > img").attr("data-src")
setUrlWithoutDomain(element.attr("href"))
}
override fun latestUpdatesSelector() =
"div[class=\"update latest\"] > div.card-board > " + "div[class~=card]:not([class~=ad]) > div > a"
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
title = element.select("div.data h3").text()
thumbnail_url = element.select("div.thumb-container img").attr("data-src")
setUrlWithoutDomain(element.attr("href"))
}
override fun getCollections(): List<Collection> = listOf(
Collection("連載作品", ""),
)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotEmpty()) {
val url = "$baseUrl/search".toHttpUrl().newBuilder().addQueryParameter("q", query)
return GET(url.build(), headers)
}
return GET(baseUrl, headers) // Currently just get all ongoing works
}
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'Comick'
extClass = '.ComickFactory'
extVersionCode = 52
extVersionCode = 51
isNsfw = true
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'Comico'
extClass = '.ComicoFactory'
extVersionCode = 6
extVersionCode = 5
isNsfw = true
}

View File

@ -24,7 +24,6 @@ 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
@ -160,7 +159,7 @@ open class Comico(
if (!chapter.name.endsWith(LOCK)) {
super.fetchPageList(chapter)
} else {
throw Exception("You are not authorized to view this!")
throw Error("You are not authorized to view this!")
}
private fun search(query: String, page: Int) =
@ -177,7 +176,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 Exception(status(code))
if (code != 200) throw Error(status(code))
}["data"]
private operator fun JsonElement?.get(key: String) =

View File

@ -1,7 +1,7 @@
ext {
extName = 'CosplayTele'
extClass = '.CosplayTele'
extVersionCode = 4
extVersionCode = 3
isNsfw = true
}

View File

@ -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() = "main div.box"
override fun latestUpdatesSelector() = "div.box"
// Popular
override fun popularMangaFromElement(element: Element): SManga {

View File

@ -1,7 +1,7 @@
ext {
extName = 'DeviantArt'
extClass = '.DeviantArt'
extVersionCode = 6
extVersionCode = 3
isNsfw = true
}

View File

@ -1,11 +1,6 @@
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
@ -20,22 +15,16 @@ 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(), ConfigurableSource {
class DeviantArt : HttpSource() {
override val name = "DeviantArt"
override val baseUrl = "https://www.deviantart.com"
override val baseUrl = "https://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")
}
@ -87,32 +76,28 @@ class DeviantArt : HttpSource(), ConfigurableSource {
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
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())
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()
author = document.title().substringBefore(" ")
title = when (artistInTitle) {
true -> "$author - $galleryName"
false -> galleryName
}
description = gallery?.selectFirst(".legacy-journal")?.wholeText()
thumbnail_url = gallery?.selectFirst("img[property=contentUrl]")?.absUrl("src")
description = subFolderGallery?.selectFirst(".legacy-journal")?.wholeText()
thumbnail_url = subFolderGallery?.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 query = when (val folderId = pathSegments[2]) {
"all" -> "gallery:$username"
else -> "gallery:$username/$folderId"
val folderId = pathSegments[2]
val query = if (folderId == "all") {
"gallery:$username"
} else {
"gallery:$username/$folderId"
}
val url = backendBuilder()
@ -138,14 +123,15 @@ class DeviantArt : HttpSource(), ConfigurableSource {
nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href")
}
return chapterList.toList().also(::indexChapterList)
return indexChapterList(chapterList.toList())
}
private fun parseToChapterList(document: Document): List<SChapter> {
val items = document.select("item")
return items.map {
SChapter.create().apply {
setUrlWithoutDomain(it.selectFirst("link")!!.text())
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(it.selectFirst("link")!!.text())
chapter.apply {
name = it.selectFirst("title")!!.text()
date_upload = parseDate(it.selectFirst("pubDate")?.text())
scanlator = it.selectFirst("media|credit")?.text()
@ -153,34 +139,24 @@ class DeviantArt : HttpSource(), ConfigurableSource {
}
}
private fun indexChapterList(chapterList: List<SChapter>) {
private fun indexChapterList(chapterList: List<SChapter>): List<SChapter> {
// DeviantArt allows users to arrange galleries arbitrarily so we will
// primitively index the list by checking the first and last dates
if (chapterList.first().date_upload > chapterList.last().date_upload) {
chapterList.forEachIndexed { i, chapter ->
chapter.chapter_number = chapterList.size - i.toFloat()
return if (chapterList.first().date_upload > chapterList.last().date_upload) {
chapterList.mapIndexed { i, chapter ->
chapter.apply { chapter_number = chapterList.size - i.toFloat() }
}
} else {
chapterList.forEachIndexed { i, chapter ->
chapter.chapter_number = i.toFloat() + 1
chapterList.mapIndexed { i, chapter ->
chapter.apply { chapter_number = i.toFloat() + 1 }
}
}
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
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
}
}
val imageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src")
return listOf(Page(0, imageUrl = imageUrl))
}
override fun imageUrlParse(response: Response): String {
@ -191,38 +167,7 @@ class DeviantArt : HttpSource(), ConfigurableSource {
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 {
private const val SEARCH_FORMAT_MSG = "Please enter a query in the format of gallery:{username} or gallery:{username}/{folderId}"
const val SEARCH_FORMAT_MSG = "Please enter a query in the format of gallery:{username} or gallery:{username}/{folderId}"
}
}

View File

@ -1,8 +1,8 @@
ext {
extName = 'Manga18Free'
extClass = '.manga18free'
extName = 'Eromanhwa'
extClass = '.Eromanhwa'
themePkg = 'madara'
baseUrl = 'https://manga18free.com'
baseUrl = 'https://eromanhwa.org'
overrideVersionCode = 1
isNsfw = true
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.extension.all.eromanhwa
import eu.kanade.tachiyomi.multisrc.madara.Madara
class Eromanhwa : Madara(
"Eromanhwa",
"https://eromanhwa.org",
"all",
) {
override val id = 3597355706480775153 // accidently set lang to en...
override val useNewChapterEndpoint = true
}

View File

@ -0,0 +1,8 @@
ext {
extName = 'Frelein Books'
extClass = '.FreleinBooks'
extVersionCode = 1
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -0,0 +1,271 @@
package eu.kanade.tachiyomi.extension.all.freleinbooks
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class FreleinBooks() : ParsedHttpSource() {
override val baseUrl = "https://books.frelein.my.id"
override val lang = "all"
override val name = "Frelein Books"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private val Element.imgSrc: String
get() = attr("data-lazy-src")
.ifEmpty { attr("data-src") }
.ifEmpty { attr("src") }
// Latest
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
manga.thumbnail_url = element.selectFirst("img")!!.imgSrc
manga.title = element.select(".postTitle").text()
manga.setUrlWithoutDomain(element.select(".postTitle > a").attr("abs:href"))
return manga
}
override fun latestUpdatesNextPageSelector() = ".olderLink"
override fun latestUpdatesRequest(page: Int): Request {
return if (page == 1) {
GET(baseUrl)
} else {
val dateParam = page * 7 * 2
// Calendar set to the current date
val calendar: Calendar = Calendar.getInstance()
// rollback 14 days
calendar.add(Calendar.DAY_OF_YEAR, -dateParam)
val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
// now the date is 14 days back
GET("$baseUrl/search?updated-max=${formatter.format(calendar.time)}T12:38:00%2B07:00&max-results=12&start=12&by-date=false")
}
}
override fun latestUpdatesSelector() = ".blogPosts > article"
// Popular
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
manga.thumbnail_url = element.selectFirst("img")!!.imgSrc
manga.title = element.select("h3").text()
manga.setUrlWithoutDomain(element.select("h3 > a").attr("abs:href"))
return manga
}
override fun popularMangaNextPageSelector(): String? = null
override fun popularMangaRequest(page: Int) = latestUpdatesRequest(page)
override fun popularMangaSelector() = ".itemPopulars article"
// Search
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val tagFilter = filterList.findInstance<TagFilter>()!!
val groupFilter = filterList.findInstance<GroupFilter>()!!
val magazineFilter = filterList.findInstance<MagazineFilter>()!!
val fashionMagazineFilter = filterList.findInstance<FashionMagazineFilter>()!!
return when {
query.isEmpty() && groupFilter.state != 0 -> GET("$baseUrl/search/label/${groupFilter.toUriPart()}")
query.isEmpty() && magazineFilter.state != 0 -> GET("$baseUrl/search/label/${magazineFilter.toUriPart()}")
query.isEmpty() && fashionMagazineFilter.state != 0 -> GET("$baseUrl/search/label/${fashionMagazineFilter.toUriPart()}")
query.isEmpty() && tagFilter.state.isNotEmpty() -> GET("$baseUrl/search/label/${tagFilter.state}")
query.isNotEmpty() -> GET("$baseUrl/search?q=$query")
else -> latestUpdatesRequest(page)
}
}
override fun searchMangaSelector() = latestUpdatesSelector()
// Details
override fun mangaDetailsParse(document: Document): SManga {
val manga = SManga.create()
manga.title = document.select(".postTitle").text()
manga.description = "Read ${document.select(".postTitle").text()} \n \nNote: If you encounters error when opening the magazine, please press the WebView button then leave a comment on our web so we can update it soon."
manga.genre = document.select(".labelLink > a")
.joinToString(", ") { it.text() }
manga.status = SManga.COMPLETED
return manga
}
override fun chapterFromElement(element: Element): SChapter {
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(element.select("link[rel=\"canonical\"]").attr("href"))
chapter.name = "Gallery"
chapter.date_upload = getDate(element.select("link[rel=\"canonical\"]").attr("href"))
return chapter
}
override fun chapterListSelector() = "html"
// Pages
override fun pageListParse(document: Document): List<Page> {
val pages = mutableListOf<Page>()
document.select("noscript").remove()
document.select(".gallerybox a > img").forEachIndexed { i, it ->
// format new img/b/
if (it.imgSrc.contains("img/b/")) {
if (it.imgSrc.contains("/w768-rw/")) {
val itUrl = it.imgSrc.replace("/w768-rw/", "/s0/")
pages.add(Page(i, itUrl, itUrl))
}
if (it.imgSrc.contains("/w480-rw/")) {
val itUrl = it.imgSrc.replace("/w480-rw/", "/s0/")
pages.add(Page(i, itUrl, itUrl))
}
}
// format new img/b/
else {
if (it.imgSrc.contains("=w768-rw")) {
val itUrl = it.imgSrc.replace("=w768-rw", "")
pages.add(Page(i, itUrl, itUrl))
} else if (it.imgSrc.contains("=w480-rw")) {
val itUrl = it.imgSrc.replace("=w480-rw", "")
pages.add(Page(i, itUrl, itUrl))
} else {
val itUrl = it.imgSrc
pages.add(Page(i, itUrl, itUrl))
}
}
}
return pages
}
override fun imageUrlParse(document: Document): String =
throw UnsupportedOperationException()
// Filters
override fun getFilterList(): FilterList = FilterList(
Filter.Header("NOTE: Only one filter will be applied!"),
Filter.Separator(),
GroupFilter(),
MagazineFilter(),
FashionMagazineFilter(),
TagFilter(),
)
open class UriPartFilter(
displayName: String,
private val valuePair: Array<Pair<String, String>>,
) : Filter.Select<String>(displayName, valuePair.map { it.first }.toTypedArray()) {
fun toUriPart() = valuePair[state].second
}
class MagazineFilter : UriPartFilter(
"Magazine",
arrayOf(
Pair("Any", ""),
Pair("B.L.T.", "B.L.T."),
Pair("BIG ONE GIRLS", "BIG ONE GIRLS"),
Pair("BOMB!", "BOMB!"),
Pair("BRODY", "BRODY"),
Pair("BUBKA", "BUBKA"),
Pair("ENTAME", "ENTAME"),
Pair("EX Taishu", "EX Taishu"),
Pair("FINEBOYS", "FINEBOYS"),
Pair("FLASH", "FLASH"),
Pair("Fine", "Fine"),
Pair("Friday", "Friday"),
Pair("HINA_SATSU", "HINA_SATSU"),
Pair("IDOL AND READ", "IDOL AND READ"),
Pair("Kadokawa Scene 07", "Kadokawa Scene 07"),
Pair("Monthly Basketball", "Monthly Basketball"),
Pair("Monthly Young Magazine", "Monthly Young Magazine"),
Pair("NOGI_SATSU", "NOGI_SATSU"),
Pair("Nylon Japan", "Nylon Japan"),
Pair("Platinum FLASH", "Platinum FLASH"),
Pair("Shonen Magazine", "Shonen Magazine"),
Pair("Shukan Post", "Shukan Post"),
Pair("TOKYO NEWS MOOK", "TOKYO NEWS MOOK"),
Pair("TV LIFE,Tarzan", "TV LIFE,Tarzan"),
Pair("Tokyo Calendar", "Tokyo Calendar"),
Pair("Top Yell NEO", "Top Yell NEO"),
Pair("UTB", "UTB"),
Pair("Weekly Playboy", "Weekly Playboy"),
Pair("Weekly SPA", "Weekly SPA"),
Pair("Weekly SPA!", "Weekly SPA!"),
Pair("Weekly Shonen Champion", "Weekly Shonen Champion"),
Pair("Weekly Shonen Magazine", "Weekly Shonen Magazine"),
Pair("Weekly Shonen Sunday", "Weekly Shonen Sunday"),
Pair("Weekly Shounen Magazine", "Weekly Shounen Magazine"),
Pair("Weekly The Television Plus", "Weekly The Television Plus"),
Pair("Weekly Zero Jump", "Weekly Zero Jump"),
Pair("Yanmaga Web", "Yanmaga Web"),
Pair("Young Animal", "Young Animal"),
Pair("Young Champion", "Young Champion"),
Pair("Young Gangan", "Young Gangan"),
Pair("Young Jump", "Young Jump"),
Pair("Young Magazine", "Young Magazine"),
Pair("blt graph.", "blt graph."),
Pair("mini", "mini"),
),
)
class FashionMagazineFilter : UriPartFilter(
"Fashion Magazine",
arrayOf(
Pair("Any", ""),
Pair("BAILA", "BAILA"),
Pair("Biteki", "Biteki"),
Pair("CLASSY", "CLASSY"),
Pair("CanCam", "CanCam"),
Pair("JJ", "JJ"),
Pair("LARME", "LARME"),
Pair("MARQUEE", "MARQUEE"),
Pair("Maquia", "Maquia"),
Pair("Men's non-no", "Men's non-no"),
Pair("More", "More"),
Pair("Oggi", "Oggi"),
Pair("Ray", "Ray"),
Pair("Seventeen", "Seventeen"),
Pair("Sweet", "Sweet"),
Pair("VOCE", "VOCE"),
Pair("ViVi", "ViVi"),
Pair("With", "With"),
Pair("aR", "aR"),
Pair("anan", "anan"),
Pair("bis", "bis"),
Pair("non-no", "non-no"),
),
)
class GroupFilter : UriPartFilter(
"Group",
arrayOf(
Pair("Any", ""),
Pair("Hinatazaka46", "Hinatazaka46"),
Pair("Nogizaka46", "Nogizaka46"),
Pair("Sakurazaka46", "Sakurazaka46"),
Pair("Keyakizaka46", "Keyakizaka46"),
),
)
class TagFilter : Filter.Text("Tag")
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
private fun getDate(str: String): Long {
val regex = "[0-9]{4}\\/[0-9]{2}\\/[0-9]{2}".toRegex()
val match = regex.find(str)
return runCatching { DATE_FORMAT.parse(match!!.value)?.time }.getOrNull() ?: 0L
}
companion object {
private val DATE_FORMAT by lazy {
SimpleDateFormat("yyyy/MM/dd", Locale.US)
}
}
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'Hitomi'
extClass = '.HitomiFactory'
extVersionCode = 36
extVersionCode = 35
isNsfw = true
}

View File

@ -64,11 +64,8 @@ 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)
}
@ -751,25 +748,6 @@ class Hitomi(
}
}
private fun updateImageUrlInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val cleanUrl = request.url.run { "$scheme://$host$encodedPath" }
REGEX_IMAGE_URL.matchEntire(cleanUrl)?.let { match ->
val (ext, hash) = match.destructured
val commonId = runBlocking { commonImageId() }
val imageId = imageIdFromHash(hash)
val subDomain = 'a' + runBlocking { subdomainOffset(imageId) }
val newUrl = "https://${subDomain}a.$domain/$ext/$commonId$imageId/$hash.$ext"
val newRequest = request.newBuilder().url(newUrl).build()
return chain.proceed(newRequest)
}
return chain.proceed(request)
}
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()

View File

@ -1,7 +1,7 @@
ext {
extName = 'Little Garden'
extClass = '.LittleGarden'
extVersionCode = 3
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

View File

@ -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:(.*?(?=,)),original:(.*?(?=,))")
private val oricolPageRegex = Regex("\\{colored:(?<colored>.*?(?=,)),original:(?<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[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"))
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"))
} else {
pages.add(Page(i, "", cdnUrl + it.groups[2]?.value?.replace("\"", "") + ".webp"))
pages.add(Page(i, "", cdnUrl + it.groups["original"]?.value?.replace("\"", "") + ".webp"))
}
}
} else {

View File

@ -23,9 +23,6 @@ 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
@ -79,8 +76,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 alternative titles
prefer_title_in_extension_language_summary=If there is an alternative title available which matches the extension language, it will be used
prefer_title_in_extension_language=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
publication_demographic=Publication demographic
publication_demographic_josei=Josei
publication_demographic_none=None

View File

@ -1,7 +1,7 @@
ext {
extName = 'MangaDex'
extClass = '.MangaDexFactory'
extVersionCode = 199
extVersionCode = 196
isNsfw = true
}

View File

@ -143,11 +143,6 @@ 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"

View File

@ -424,7 +424,6 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
preferences.coverQuality,
preferences.altTitlesInDesc,
preferences.preferExtensionLangTitle,
preferences.finalChapterInDesc,
)
}
@ -774,28 +773,12 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
}
}
val finalChapterInDescPref = SwitchPreferenceCompat(screen.context).apply {
key = MDConstants.getFinalChapterInDescPrefKey(dexLang)
title = helper.intl["final_chapter_in_description"]
summary = helper.intl["final_chapter_in_description_summary"]
setDefaultValue(true)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit()
.putBoolean(MDConstants.getFinalChapterInDescPrefKey(dexLang), checkValue)
.commit()
}
}
screen.addPreference(coverQualityPref)
screen.addPreference(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)
@ -877,9 +860,6 @@ 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

View File

@ -156,7 +156,7 @@ class MangaDexHelper(lang: String) {
*/
private fun String.removeEntitiesAndMarkdown(): String {
return removeEntities()
.substringBefore("\n---")
.substringBefore("---")
.replace(markdownLinksRegex, "$1")
.replace(markdownItalicBoldRegex, "$1")
.replace(markdownItalicRegex, "$1")
@ -324,7 +324,6 @@ class MangaDexHelper(lang: String) {
coverSuffix: String?,
altTitlesInDesc: Boolean,
preferExtensionLangTitle: Boolean,
finalChapterInDesc: Boolean,
): SManga {
val attr = mangaDataDto.attributes!!
@ -366,12 +365,9 @@ class MangaDexHelper(lang: String) {
val genreList = MDConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } + nonGenres
// Build description
val desc = mutableListOf<String>()
(attr.description[lang] ?: attr.description["en"])
var desc = (attr.description[lang] ?: attr.description["en"])
?.removeEntitiesAndMarkdown()
?.let { desc.add(it) }
.orEmpty()
if (altTitlesInDesc) {
val romanizedOriginalLang = MDConstants.romanizedLangCodes[attr.originalLanguage].orEmpty()
@ -383,24 +379,12 @@ class MangaDexHelper(lang: String) {
if (altTitles.isNotEmpty()) {
val altTitlesDesc = altTitles
.joinToString("\n", "${intl["alternative_titles"]}\n") { "$it" }
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())
desc += (if (desc.isBlank()) "" else "\n\n") + altTitlesDesc.removeEntities()
}
}
return createBasicManga(mangaDataDto, coverFileName, coverSuffix, lang, preferExtensionLangTitle).apply {
description = desc.joinToString("\n\n")
description = desc
author = authors.joinToString()
artist = artists.joinToString()
status = getPublicationStatus(attr, chapters)

View File

@ -1,7 +1,9 @@
ext {
extName = 'MangaFire'
extClass = '.MangaFireFactory'
extVersionCode = 10
themePkg = 'mangareader'
baseUrl = 'https://mangafire.to'
overrideVersionCode = 5
isNsfw = true
}

View File

@ -1,190 +1,166 @@
package eu.kanade.tachiyomi.extension.all.mangafire
import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl
import java.util.Calendar
interface UriFilter {
fun addToUri(builder: HttpUrl.Builder)
class Entry(name: String, val id: String) : Filter.CheckBox(name) {
constructor(name: String) : this(name, name)
}
open class UriPartFilter(
sealed class Group(
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)
}
}
val param: String,
values: List<Entry>,
) : Filter.Group<Entry>(name, values)
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
open class UriMultiSelectFilter(
sealed class Select(
name: String,
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)
}
}
val param: String,
private val valuesMap: Map<String, String>,
) : Filter.Select<String>(name, valuesMap.keys.toTypedArray()) {
open val selection: String
get() = valuesMap[values[state]]!!
}
open class UriTriSelectOption(name: String, val value: String) : Filter.TriState(name)
class TypeFilter : Group("Type", "type[]", types)
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}")
}
}
}
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
}
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"),
),
)
class GenresFilter : Filter.Group<Genre>("Genre", genres) {
val param = "genre[]"
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")
}
}
val combineMode: Boolean
get() = state.filter { !it.isIgnored() }.size > 1
}
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"),
),
)
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 YearFilter : UriMultiSelectFilter(
"Year",
"year[]",
years,
) {
companion object {
private val currentYear by lazy {
Calendar.getInstance()[Calendar.YEAR]
}
class StatusFilter : Group("Status", "status[]", statuses)
private val years: Array<Pair<String, String>> = buildList(29) {
addAll(
(currentYear downTo (currentYear - 20)).map(Int::toString),
)
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"),
)
addAll(
(2000 downTo 1930 step 10).map { "${it}s" },
)
}.map { Pair(it, it) }.toTypedArray()
}
}
class YearFilter : Group("Year", "year[]", years)
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")
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"),
)
builder.addQueryParameter("minchap", value.toString())
}
}
}
class ChapterCountFilter : Select("Chapter Count", "minchap", chapterCounts)
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,
)
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",
)

View File

@ -1,17 +1,12 @@
package eu.kanade.tachiyomi.extension.all.mangafire
import android.app.Application
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
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
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
@ -23,245 +18,182 @@ import okhttp3.Response
import org.jsoup.Jsoup
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 org.jsoup.select.Evaluator
import uy.kohesive.injekt.injectLazy
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
class MangaFire(
open class MangaFire(
override val lang: String,
private val langCode: String = lang,
) : ConfigurableSource, HttpSource() {
) : MangaReader() {
override val name = "MangaFire"
override val baseUrl = "https://mangafire.to"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
}
override val client = super.client.newBuilder()
.addInterceptor(ImageInterceptor)
.build()
override val client = network.cloudflareClient.newBuilder().addInterceptor(ImageInterceptor).build()
override fun latestUpdatesRequest(page: Int) =
GET("$baseUrl/filter?sort=recently_updated&language[]=$langCode&page=$page", headers)
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int): 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 popularMangaRequest(page: Int) =
GET("$baseUrl/filter?sort=most_viewed&language[]=$langCode&page=$page", headers)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("filter")
if (query.isNotBlank()) {
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
if (query.isNotBlank()) {
urlBuilder.addPathSegment("filter").apply {
addQueryParameter("keyword", query)
addQueryParameter("page", page.toString())
}
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
} 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 -> {}
}
}
listOf(manga, volume)
}
}
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
return MangasPage(entries, hasNextPage)
return GET(urlBuilder.build(), headers)
}
private fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
override fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
private fun searchMangaSelector() = ".original.card-lg .unit .inner"
override fun searchMangaSelector() = ".original.card-lg .unit .inner"
private fun searchMangaFromElement(element: Element) = SManga.create().apply {
element.selectFirst(".info > a")!!.let {
setUrlWithoutDomain(it.attr("href"))
title = it.ownText()
}
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
}
// =============================== Filters ==============================
override fun getFilterList() = FilterList(
TypeFilter(),
GenreFilter(),
GenreModeFilter(),
StatusFilter(),
YearFilter(),
MinChapterFilter(),
SortFilter(),
)
// =========================== Manga Details ============================
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX)
override fun mangaDetailsParse(response: Response): SManga {
return mangaDetailsParse(response.asJsoup()).apply {
if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
title = VOLUME_TITLE_PREFIX + title
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 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"))
}
selectFirst("h6")?.let {
append("\n\nAlternative title: ${it.text()}")
}
}.trim()
selectFirst(".meta")?.let {
author = it.selectFirst("span:contains(Author:) + span")?.text()
val type = it.selectFirst("span:contains(Type:) + span")?.text()
val genres = it.selectFirst("span:contains(Genres:) + span")?.text()
genre = listOfNotNull(type, genres).joinToString()
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"
}
}
thumbnail_url = document.selectFirst(".poster")!!
.selectFirst("img")!!.attr("src")
status = when (root.child(0).ownText()) {
"Completed" -> SManga.COMPLETED
"Releasing" -> SManga.ONGOING
"On_hiatus" -> SManga.ON_HIATUS
"Discontinued" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
with(document.selectFirst(Evaluator.Class("meta"))!!) {
author = selectFirst("span:contains(Author:) + span")?.text()
val type = selectFirst("span:contains(Type:) + span")?.text()
val genres = selectFirst("span:contains(Genres:) + span")?.text()
genre = listOfNotNull(type, genres).joinToString()
}
}
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
"releasing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
"on_hiatus" -> SManga.ON_HIATUS
"discontinued" -> SManga.CANCELLED
else -> SManga.UNKNOWN
override val chapterType get() = "chapter"
override val volumeType get() = "volume"
override fun chapterListRequest(mangaUrl: String, type: String): Request {
val id = mangaUrl.substringAfterLast('.')
return GET("$baseUrl/ajax/manga/$id/$type/$langCode", headers)
}
// ============================== 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)
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"))
}
}
return elements.toList()
}
@Serializable
class AjaxReadDto(
class ChapterIdsDto(
val html: String,
val title_format: String,
)
override fun chapterListParse(response: Response): List<SChapter> {
throw UnsupportedOperationException()
}
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 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
}
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
}
}
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 = response.parseAs<ResponseDto<PageListDto>>().result
val result = json.decodeFromString<ResponseDto<PageListDto>>(response.body.string()).result
return result.pages.mapIndexed { index, image ->
val url = image.url
@ -274,49 +206,27 @@ 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,
)
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] "
}
override fun getFilterList() =
FilterList(
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
TypeFilter(),
GenresFilter(),
StatusFilter(),
YearFilter(),
ChapterCountFilter(),
SortFilter(),
)
}

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".all.mangahosted.MangaHostedUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="mangahosted.org"
android:pathPattern="/.*/..*" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,202 +0,0 @@
package eu.kanade.tachiyomi.extension.all.mangahosted
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class MangaHosted(private val langOption: LanguageOption) : HttpSource() {
override val lang = langOption.lang
override val name: String = "Manga Hosted${langOption.nameSuffix}"
override val baseUrl: String = "https://mangahosted.org"
override val supportsLatest = true
private val json: Json by injectLazy()
override val client = network.client.newBuilder()
.rateLimit(2)
.build()
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.set("Referer", "$baseUrl/")
// ================================= Popular ==========================================
override fun popularMangaRequest(page: Int): Request {
val maxResult = 24
return GET("$apiUrl/${langOption.infix}/HomeTopFllow/$maxResult/${page - 1}")
}
override fun popularMangaParse(response: Response): MangasPage {
val dto = response.parseAs<Pageable<MangaDto>>()
val mangas = dto.data.map(::mangaParse)
return MangasPage(
mangas = mangas,
hasNextPage = dto.hasNextPage(),
)
}
// ================================= Latest ===========================================
override fun latestUpdatesRequest(page: Int): Request {
val maxResult = 24
val url = "$apiUrl/${langOption.infix}/HomeLastUpdate".toHttpUrl().newBuilder()
.addPathSegment("$maxResult")
.addPathSegment("${page - 1}")
.build()
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
// ================================= Search ===========================================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val maxResult = 20
val url = "$apiUrl/${langOption.infix}/SeachPage/$maxResult/${page - 1}".toHttpUrl().newBuilder()
.addPathSegment(query)
.build()
return GET(url, headers)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(SEARCH_PREFIX)) {
val url = "$baseUrl/${langOption.infix}/${query.substringAfter(SEARCH_PREFIX)}"
return client.newCall(GET(url, headers))
.asObservableSuccess().map { response ->
val mangas = try { listOf(mangaDetailsParse(response)) } catch (_: Exception) { emptyList() }
MangasPage(mangas, false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseAs<SearchDto>()
return MangasPage(
dto.mangas.map(::mangaParse),
false,
)
}
// ================================= Details ==========================================
override fun mangaDetailsRequest(manga: SManga): Request {
val url = "$apiUrl/${langOption.infix}/getInfoManga".toHttpUrl().newBuilder()
.addPathSegment(manga.slug())
.build()
return GET(url, headers)
}
override fun mangaDetailsParse(response: Response): SManga {
val dto = response.parseAs<MangaDetailsDto>()
return mangaParse(dto.details)
}
override fun getMangaUrl(manga: SManga): String {
return baseUrl + manga.url.replace(langOption.infix, langOption.mangaSubstring)
}
// ================================= Chapter ==========================================
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val chapters = mutableListOf<SChapter>()
var currentPage = 0
do {
val chaptersDto = fetchChapterListPageable(manga, currentPage++)
chapters += chaptersDto.data.map { chapter ->
SChapter.create().apply {
name = chapter.name
date_upload = chapter.date.toDate()
url = chapter.toChapterUrl(langOption.infix)
}
}
} while (chaptersDto.hasNextPage())
return Observable.just(chapters)
}
private fun fetchChapterListPageable(manga: SManga, page: Int): Pageable<ChapterDto> {
val maxResult = 100
val url = "$apiUrl/${langOption.infix}/GetChapterListFilter/${manga.slug()}/$maxResult/$page/all/${langOption.orderBy}"
return client.newCall(GET(url, headers)).execute()
.parseAs<Pageable<ChapterDto>>()
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
// ================================= Pages ============================================
override fun pageListRequest(chapter: SChapter): Request {
val chapterSlug = chapter.url.substringAfter(langOption.infix)
val url = "$apiUrl/${langOption.infix}/GetImageChapter$chapterSlug"
return GET(url, headers)
}
override fun imageRequest(page: Page): Request {
val imageHeaders = headers.newBuilder()
.set("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
.removeAll("Referer")
.build()
return super.imageRequest(page).newBuilder()
.headers(imageHeaders)
.build()
}
override fun pageListParse(response: Response): List<Page> {
val location = response.request.url.toString()
val dto = response.parseAs<PageDto>()
return dto.pages.mapIndexed { index, url ->
Page(index, location, imageUrl = url)
}
}
override fun imageUrlParse(response: Response): String = ""
// ================================= Utilities =======================================
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
private fun SManga.slug() = this.url.split("/").last()
private fun mangaParse(dto: MangaDto): SManga {
return SManga.create().apply {
title = dto.title
thumbnail_url = dto.thumbnailUrl
status = dto.status
url = "/${langOption.infix}/${dto.slug}"
genre = dto.genres
initialized = true
}
}
private fun String.toDate(): Long =
try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
companion object {
const val SEARCH_PREFIX = "slug:"
val baseApiUrl = "https://api.novelfull.us"
val apiUrl = "$baseApiUrl/api"
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
}
}

View File

@ -1,68 +0,0 @@
package eu.kanade.tachiyomi.extension.all.mangahosted
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class MangaDetailsDto(private val data: Props) {
val details: MangaDto get() = data.details
@Serializable
class Props(
@SerialName("infoDoc") val details: MangaDto,
)
}
@Serializable
open class Pageable<T>(
var currentPage: Int,
var totalPage: Int,
val data: List<T>,
) {
fun hasNextPage() = (currentPage + 1) <= totalPage
}
@Serializable
class ChapterDto(
val date: String,
@SerialName("idDoc") val slugManga: String,
@SerialName("idDetail") val id: String,
@SerialName("nameChapter") val name: String,
) {
fun toChapterUrl(lang: String) = "/$lang/${this.slugManga}/$id"
}
@Serializable
class MangaDto(
@SerialName("name") val title: String,
@SerialName("image") private val _thumbnailUrl: String,
@SerialName("idDoc") val slug: String,
@SerialName("genresName") val genres: String,
@SerialName("status") val _status: String,
) {
val thumbnailUrl get() = "${MangaHosted.baseApiUrl}$_thumbnailUrl"
val status get() = when (_status) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
@Serializable
class SearchDto(
@SerialName("data")
val mangas: List<MangaDto>,
)
@Serializable
class PageDto(val `data`: Data) {
val pages: List<String> get() = `data`.detailDocuments.source.split("#")
@Serializable
class Data(@SerialName("detail_documents") val detailDocuments: DetailDocuments)
@Serializable
class DetailDocuments(val source: String)
}

View File

@ -1,30 +0,0 @@
package eu.kanade.tachiyomi.extension.all.mangahosted
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class MangaHostedFactory : SourceFactory {
override fun createSources(): List<Source> = languages.map { MangaHosted(it) }
}
class LanguageOption(
val lang: String,
val infix: String = lang,
val mangaSubstring: String = infix,
val nameSuffix: String = "",
val orderBy: String = "DESC",
)
val languages = listOf(
LanguageOption("en", "manga", "scan"),
LanguageOption("en", "manga-v2", "kaka", " v2"),
LanguageOption("en", "comic", "comic-dc", " Comics"),
LanguageOption("es", "manga-spanish", "manga-es"),
LanguageOption("id", "manga-indo", "id"),
LanguageOption("it", "manga-italia", "manga-it"),
LanguageOption("ja", "mangaraw", "raw"),
LanguageOption("pt-BR", "manga-br", orderBy = "ASC"),
LanguageOption("ru", "manga-ru", "mangaru"),
LanguageOption("ru", "manga-ru-hentai", "hentai", " +18"),
LanguageOption("ru", "manga-ru-yaoi", "yaoi", " +18 Yaoi"),
)

View File

@ -1,7 +1,3 @@
## 1.4.7
- Reworked the lib-multisrc theme
## 1.3.4
- Refactor and make multisrc

View File

@ -3,7 +3,7 @@ ext {
extClass = '.MangaReaderFactory'
themePkg = 'mangareader'
baseUrl = 'https://mangareader.to'
overrideVersionCode = 5
overrideVersionCode = 4
isNsfw = true
}

View File

@ -1,208 +1,247 @@
package eu.kanade.tachiyomi.extension.all.mangareaderto
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader.UriFilter
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader.UriMultiSelectFilter
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader.UriPartFilter
import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl
import java.util.Calendar
class TypeFilter : UriPartFilter(
"Type",
"type",
arrayOf(
Pair("All", ""),
Pair("Manga", "1"),
Pair("One-Shot", "2"),
Pair("Doujinshi", "3"),
Pair("Light Novel", "4"),
Pair("Manhwa", "5"),
Pair("Manhua", "6"),
Pair("Comic", "7"),
),
)
object Note : Filter.Header("NOTE: Ignored if using text search!")
class StatusFilter : UriPartFilter(
"Status",
"status",
arrayOf(
Pair("All", ""),
Pair("Finished", "1"),
Pair("Publishing", "2"),
Pair("On Hiatus", "3"),
Pair("Discontinued", "4"),
Pair("Not yet published", "5"),
),
)
sealed class Select(
name: String,
val param: String,
values: Array<String>,
) : Filter.Select<String>(name, values) {
open val selection: String
get() = if (state == 0) "" else state.toString()
}
class RatingFilter : UriPartFilter(
"Rating Type",
"rating_type",
arrayOf(
Pair("All", ""),
Pair("G - All Ages", "1"),
Pair("PG - Children", "2"),
Pair("PG-13 - Teens 13 or older", "3"),
Pair("R - 17+ (violence & profanity)", "4"),
Pair("R+ - Mild Nudity", "5"),
Pair("Rx - Hentai", "6"),
),
)
class TypeFilter(
values: Array<String> = types,
) : Select("Type", "type", values) {
companion object {
private val types: Array<String>
get() = arrayOf(
"All",
"Manga",
"One-Shot",
"Doujinshi",
"Light Novel",
"Manhwa",
"Manhua",
"Comic",
)
}
}
class ScoreFilter : UriPartFilter(
"Score",
"score",
arrayOf(
Pair("All", ""),
Pair("(1) Appalling", "1"),
Pair("(2) Horrible", "2"),
Pair("(3) Very Bad", "3"),
Pair("(4) Bad", "4"),
Pair("(5) Average", "5"),
Pair("(6) Fine", "6"),
Pair("(7) Good", "7"),
Pair("(8) Very Good", "8"),
Pair("(9) Great", "9"),
Pair("(10) Masterpiece", "10"),
),
)
class StatusFilter(
values: Array<String> = statuses,
) : Select("Status", "status", values) {
companion object {
private val statuses: Array<String>
get() = arrayOf(
"All",
"Finished",
"Publishing",
"On Hiatus",
"Discontinued",
"Not yet published",
)
}
}
class YearFilter(name: String, param: String) : UriPartFilter(
name,
param,
years,
) {
class RatingFilter(
values: Array<String> = ratings,
) : Select("Rating Type", "rating_type", values) {
companion object {
private val ratings: Array<String>
get() = arrayOf(
"All",
"G - All Ages",
"PG - Children",
"PG-13 - Teens 13 or older",
"R - 17+ (violence & profanity)",
"R+ - Mild Nudity",
"Rx - Hentai",
)
}
}
class ScoreFilter(
values: Array<String> = scores,
) : Select("Score", "score", values) {
companion object {
private val scores: Array<String>
get() = arrayOf(
"All",
"(1) Appalling",
"(2) Horrible",
"(3) Very Bad",
"(4) Bad",
"(5) Average",
"(6) Fine",
"(7) Good",
"(8) Very Good",
"(9) Great",
"(10) Masterpiece",
)
}
}
sealed class DateSelect(
name: String,
param: String,
values: Array<String>,
) : Select(name, param, values) {
override val selection: String
get() = if (state == 0) "" else values[state]
}
class YearFilter(
param: String,
values: Array<String> = years,
) : DateSelect("Year", param, values) {
companion object {
private val nextYear by lazy {
Calendar.getInstance()[Calendar.YEAR] + 1
}
private val years = Array(nextYear - 1916) { year ->
if (year == 0) {
Pair("Any", "")
} else {
(nextYear - year).toString().let { Pair(it, it) }
private val years: Array<String>
get() = Array(nextYear - 1916) {
if (it == 0) "Any" else (nextYear - it).toString()
}
}
}
}
class MonthFilter(name: String, param: String) : UriPartFilter(
name,
param,
months,
) {
class MonthFilter(
param: String,
values: Array<String> = months,
) : DateSelect("Month", param, values) {
companion object {
private val months = Array(13) { months ->
if (months == 0) {
Pair("Any", "")
} else {
Pair("%02d".format(months), months.toString())
private val months: Array<String>
get() = Array(13) {
if (it == 0) "Any" else "%02d".format(it)
}
}
}
}
class DayFilter(name: String, param: String) : UriPartFilter(
name,
param,
days,
) {
class DayFilter(
param: String,
values: Array<String> = days,
) : DateSelect("Day", param, values) {
companion object {
private val days = Array(32) { day ->
if (day == 0) {
Pair("Any", "")
} else {
Pair("%02d".format(day), day.toString())
private val days: Array<String>
get() = Array(32) {
if (it == 0) "Any" else "%02d".format(it)
}
}
}
}
sealed class DateFilter(
type: String,
private val values: List<UriPartFilter>,
) : Filter.Group<UriPartFilter>("$type Date", values), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
values.forEach {
it.addToUri(builder)
}
}
}
values: List<DateSelect>,
) : Filter.Group<DateSelect>("$type Date", values)
class StartDateFilter(
values: List<UriPartFilter> = parts,
values: List<DateSelect> = parts,
) : DateFilter("Start", values) {
companion object {
private val parts = listOf(
YearFilter("Year", "sy"),
MonthFilter("Month", "sm"),
DayFilter("Day", "sd"),
)
private val parts: List<DateSelect>
get() = listOf(
YearFilter("sy"),
MonthFilter("sm"),
DayFilter("sd"),
)
}
}
class EndDateFilter(
values: List<UriPartFilter> = parts,
values: List<DateSelect> = parts,
) : DateFilter("End", values) {
companion object {
private val parts = listOf(
YearFilter("Year", "ey"),
MonthFilter("Month", "em"),
DayFilter("Day", "ed"),
private val parts: List<DateSelect>
get() = listOf(
YearFilter("ey"),
MonthFilter("em"),
DayFilter("ed"),
)
}
}
class SortFilter(
values: Array<String> = orders.keys.toTypedArray(),
) : Select("Sort", "sort", values) {
override val selection: String
get() = orders[values[state]]!!
companion object {
private val orders = mapOf(
"Default" to "default",
"Latest Updated" to "latest-updated",
"Score" to "score",
"Name A-Z" to "name-az",
"Release Date" to "release-date",
"Most Viewed" to "most-viewed",
)
}
}
class GenreFilter : UriMultiSelectFilter(
"Genres",
"genres",
arrayOf(
Pair("Action", "1"),
Pair("Adventure", "2"),
Pair("Cars", "3"),
Pair("Comedy", "4"),
Pair("Dementia", "5"),
Pair("Demons", "6"),
Pair("Doujinshi", "7"),
Pair("Drama", "8"),
Pair("Ecchi", "9"),
Pair("Fantasy", "10"),
Pair("Game", "11"),
Pair("Gender Bender", "12"),
Pair("Harem", "13"),
Pair("Hentai", "14"),
Pair("Historical", "15"),
Pair("Horror", "16"),
Pair("Josei", "17"),
Pair("Kids", "18"),
Pair("Magic", "19"),
Pair("Martial Arts", "20"),
Pair("Mecha", "21"),
Pair("Military", "22"),
Pair("Music", "23"),
Pair("Mystery", "24"),
Pair("Parody", "25"),
Pair("Police", "26"),
Pair("Psychological", "27"),
Pair("Romance", "28"),
Pair("Samurai", "29"),
Pair("School", "30"),
Pair("Sci-Fi", "31"),
Pair("Seinen", "32"),
Pair("Shoujo", "33"),
Pair("Shoujo Ai", "34"),
Pair("Shounen", "35"),
Pair("Shounen Ai", "36"),
Pair("Slice of Life", "37"),
Pair("Space", "38"),
Pair("Sports", "39"),
Pair("Super Power", "40"),
Pair("Supernatural", "41"),
Pair("Thriller", "42"),
Pair("Vampire", "43"),
Pair("Yaoi", "44"),
Pair("Yuri", "45"),
),
",",
)
class Genre(name: String, val id: String) : Filter.CheckBox(name)
class GenresFilter(
values: List<Genre> = genres,
) : Filter.Group<Genre>("Genres", values) {
val param = "genres"
val selection: String
get() = state.filter { it.state }.joinToString(",") { it.id }
companion object {
private val genres: List<Genre>
get() = listOf(
Genre("Action", "1"),
Genre("Adventure", "2"),
Genre("Cars", "3"),
Genre("Comedy", "4"),
Genre("Dementia", "5"),
Genre("Demons", "6"),
Genre("Doujinshi", "7"),
Genre("Drama", "8"),
Genre("Ecchi", "9"),
Genre("Fantasy", "10"),
Genre("Game", "11"),
Genre("Gender Bender", "12"),
Genre("Harem", "13"),
Genre("Hentai", "14"),
Genre("Historical", "15"),
Genre("Horror", "16"),
Genre("Josei", "17"),
Genre("Kids", "18"),
Genre("Magic", "19"),
Genre("Martial Arts", "20"),
Genre("Mecha", "21"),
Genre("Military", "22"),
Genre("Music", "23"),
Genre("Mystery", "24"),
Genre("Parody", "25"),
Genre("Police", "26"),
Genre("Psychological", "27"),
Genre("Romance", "28"),
Genre("Samurai", "29"),
Genre("School", "30"),
Genre("Sci-Fi", "31"),
Genre("Seinen", "32"),
Genre("Shoujo", "33"),
Genre("Shoujo Ai", "34"),
Genre("Shounen", "35"),
Genre("Shounen Ai", "36"),
Genre("Slice of Life", "37"),
Genre("Space", "38"),
Genre("Sports", "39"),
Genre("Super Power", "40"),
Genre("Supernatural", "41"),
Genre("Thriller", "42"),
Genre("Vampire", "43"),
Genre("Yaoi", "44"),
Genre("Yuri", "45"),
)
}
}

Some files were not shown because too many files have changed in this diff Show More