revert kotlin 2.1.0 and deps for now (#7468)

generated serializers seem to be missing in final apk, need further investigation
This commit is contained in:
AwkwardPeak7 2025-02-02 14:24:40 +05:00 committed by Draff
parent 2e45708568
commit 0658c1926c
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
25 changed files with 443 additions and 386 deletions

View File

@ -12,5 +12,5 @@ dependencies {
implementation(libs.gradle.agp)
implementation(libs.gradle.kotlin)
implementation(libs.gradle.serialization)
implementation(libs.spotless.gradle)
implementation(libs.gradle.kotlinter)
}

View File

@ -1,50 +0,0 @@
plugins {
id("com.diffplug.spotless")
}
spotless {
ratchetFrom = "105f615b339e681a630f21dc0d363b8ca1cb17d5"
kotlin {
target("**/*.kt", "**/*.kts")
targetExclude("**/build/**/*.kt")
ktlint()
.editorConfigOverride(mapOf(
"ktlint_standard_discouraged-comment-location" to "disabled",
"ktlint_function_signature_body_expression_wrapping" to "default",
"ktlint_standard_no-empty-first-line-in-class-body" to "disable",
"ktlint_standard_chain-method-continuation" to "disable"
))
trimTrailingWhitespace()
endWithNewline()
}
format("gradle") {
target("**/*.gradle")
trimTrailingWhitespace()
endWithNewline()
}
format("xml") {
target("**/*.xml")
targetExclude("**/build/**/*.xml")
trimTrailingWhitespace()
endWithNewline()
}
}
tasks {
named("preBuild") {
dependsOn(
tasks.getByName("spotlessCheck")
)
}
if (System.getenv("CI") != "true") {
named("spotlessCheck") {
dependsOn(
tasks.getByName("spotlessApply")
)
}
}
}

View File

@ -2,7 +2,6 @@ plugins {
id("com.android.library")
kotlin("android")
id("kotlinx-serialization")
id("keiyoushi.lint")
}
android {
@ -17,16 +16,6 @@ android {
buildFeatures {
androidResources = false
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
}
}
dependencies {

View File

@ -2,7 +2,7 @@ plugins {
id("com.android.library")
kotlin("android")
id("kotlinx-serialization")
id("keiyoushi.lint")
id("org.jmailen.kotlinter")
}
android {
@ -23,21 +23,35 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
}
}
kotlinter {
experimentalRules = true
disabledRules = arrayOf(
"experimental:argument-list-wrapping", // Doesn't play well with Android Studio
"experimental:comment-wrapping",
)
}
dependencies {
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
}
tasks {
preBuild {
dependsOn(lintKotlin)
}
if (System.getenv("CI") != "true") {
lintKotlin {
dependsOn(formatKotlin)
}
}
}
tasks.register("printDependentExtensions") {
doLast {
project.printDependentExtensions()

View File

@ -1,7 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
apply plugin: 'keiyoushi.lint'
apply plugin: 'org.jmailen.kotlinter'
assert !ext.has("pkgNameSuffix")
assert !ext.has("libVersion")
@ -18,17 +18,6 @@ android {
sourceSets {
main {
manifest.srcFile "AndroidManifest.xml"
if (!manifest.srcFile.exists()) {
def buildDir = layout.buildDirectory.get().getAsFile().path
mkdir(buildDir)
File tempFile = new File(buildDir, "tempAndroidManifest.xml")
if (!tempFile.exists()) {
tempFile.withWriter {
it.write('<?xml version="1.0" encoding="utf-8"?>\n<manifest />\n')
}
}
manifest.srcFile(tempFile.path)
}
java.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
@ -92,14 +81,22 @@ android {
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
}
kotlinter {
experimentalRules = true
disabledRules = [
"experimental:argument-list-wrapping", // Doesn't play well with Android Studio
"experimental:comment-wrapping",
]
}
}
dependencies {
@ -107,3 +104,23 @@ dependencies {
implementation(project(":core"))
compileOnly(libs.bundles.common)
}
tasks.register("writeManifestFile") {
doLast {
def manifest = android.sourceSets.getByName("main").manifest
if (!manifest.srcFile.exists()) {
File tempFile = layout.buildDirectory.get().file("tempAndroidManifest.xml").getAsFile()
if (!tempFile.exists()) {
tempFile.withWriter {
it.write('<?xml version="1.0" encoding="utf-8"?>\n<manifest />\n')
}
}
manifest.srcFile(tempFile.path)
}
}
}
preBuild.dependsOn(writeManifestFile, lintKotlin)
if (System.getenv("CI") != "true") {
lintKotlin.dependsOn(formatKotlin)
}

View File

@ -1,14 +1,13 @@
[versions]
kotlin_version = "2.1.0"
coroutines_version = "1.10.1"
serialization_version = "1.8.0"
kotlin_version = "1.7.21"
coroutines_version = "1.6.4"
serialization_version = "1.4.0"
[libraries]
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.8.0" }
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.6.1" }
gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" }
gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
spotless-gradle = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version = "7.0.2" }
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }
tachiyomi-lib = { module = "com.github.tachiyomiorg:extensions-lib", version = "1.4.2" }
@ -21,8 +20,8 @@ coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-androi
injekt-core = { module = "com.github.null2264.injekt:injekt-core", version = "4135455a2a" }
rxjava = { module = "io.reactivex:rxjava", version = "1.3.8" }
jsoup = { module = "org.jsoup:jsoup", version = "1.18.3" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version = "5.0.0-alpha.14" }
jsoup = { module = "org.jsoup:jsoup", version = "1.15.1" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version = "5.0.0-alpha.11" }
quickjs = { module = "app.cash.quickjs:quickjs-android", version = "0.9.2" }
[bundles]

View File

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

View File

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

View File

@ -109,9 +109,7 @@ abstract class Madara(
protected open val useLoadMoreRequest = LoadMoreStrategy.AutoDetect
enum class LoadMoreStrategy {
AutoDetect,
Always,
Never,
AutoDetect, Always, Never
}
/**
@ -120,9 +118,7 @@ abstract class Madara(
private var loadMoreRequestDetected = LoadMoreDetection.Pending
private enum class LoadMoreDetection {
Pending,
True,
False,
Pending, True, False
}
protected fun detectLoadMore(document: Document) {
@ -136,10 +132,12 @@ abstract class Madara(
}
}
protected fun useLoadMoreRequest(): Boolean = when (useLoadMoreRequest) {
LoadMoreStrategy.Always -> true
LoadMoreStrategy.Never -> false
else -> loadMoreRequestDetected == LoadMoreDetection.True
protected fun useLoadMoreRequest(): Boolean {
return when (useLoadMoreRequest) {
LoadMoreStrategy.Always -> true
LoadMoreStrategy.Never -> false
else -> loadMoreRequestDetected == LoadMoreDetection.True
}
}
// Popular Manga
@ -178,17 +176,19 @@ abstract class Madara(
return manga
}
override fun popularMangaRequest(page: Int): Request = if (useLoadMoreRequest()) {
loadMoreRequest(page, popular = true)
} else {
GET("$baseUrl/$mangaSubString/${searchPage(page)}?m_orderby=views", headers)
}
override fun popularMangaRequest(page: Int): Request =
if (useLoadMoreRequest()) {
loadMoreRequest(page, popular = true)
} else {
GET("$baseUrl/$mangaSubString/${searchPage(page)}?m_orderby=views", headers)
}
override fun popularMangaNextPageSelector(): String? = if (useLoadMoreRequest()) {
"body:not(:has(.no-posts))"
} else {
"div.nav-previous, nav.navigation-ajax, a.nextpostslink"
}
override fun popularMangaNextPageSelector(): String? =
if (useLoadMoreRequest()) {
"body:not(:has(.no-posts))"
} else {
"div.nav-previous, nav.navigation-ajax, a.nextpostslink"
}
// Latest Updates
@ -199,11 +199,12 @@ abstract class Madara(
return popularMangaFromElement(element)
}
override fun latestUpdatesRequest(page: Int): Request = if (useLoadMoreRequest()) {
loadMoreRequest(page, popular = false)
} else {
GET("$baseUrl/$mangaSubString/${searchPage(page)}?m_orderby=latest", headers)
}
override fun latestUpdatesRequest(page: Int): Request =
if (useLoadMoreRequest()) {
loadMoreRequest(page, popular = false)
} else {
GET("$baseUrl/$mangaSubString/${searchPage(page)}?m_orderby=latest", headers)
}
override fun latestUpdatesNextPageSelector(): String? = popularMangaNextPageSelector()
@ -250,16 +251,20 @@ abstract class Madara(
return super.fetchSearchManga(page, query, filters)
}
protected open fun searchPage(page: Int): String = if (page == 1) {
""
} else {
"page/$page/"
protected open fun searchPage(page: Int): String {
return if (page == 1) {
""
} else {
"page/$page/"
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = if (useLoadMoreRequest()) {
searchLoadMoreRequest(page, query, filters)
} else {
searchRequest(page, query, filters)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return if (useLoadMoreRequest()) {
searchLoadMoreRequest(page, query, filters)
} else {
searchRequest(page, query, filters)
}
}
protected open fun searchRequest(page: Int, query: String, filters: FilterList): Request {
@ -305,9 +310,7 @@ abstract class Madara(
filter.state
.filter { it.state }
.let { list ->
if (list.isNotEmpty()) {
list.forEach { genre -> url.addQueryParameter("genre[]", genre.id) }
}
if (list.isNotEmpty()) { list.forEach { genre -> url.addQueryParameter("genre[]", genre.id) } }
}
}
else -> {}
@ -486,7 +489,8 @@ abstract class Madara(
intl["adult_content_filter_only"] to "1",
)
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>, state: Int = 0) : Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>, state: Int = 0) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
fun toUriPart() = vals[state].second
}
@ -495,21 +499,21 @@ abstract class Madara(
protected class AuthorFilter(title: String) : Filter.Text(title)
protected class ArtistFilter(title: String) : Filter.Text(title)
protected class YearFilter(title: String) : Filter.Text(title)
protected class StatusFilter(title: String, status: List<Tag>) : Filter.Group<Tag>(title, status)
protected class StatusFilter(title: String, status: List<Tag>) :
Filter.Group<Tag>(title, status)
protected class OrderByFilter(title: String, options: List<Pair<String, String>>, state: Int = 0) : UriPartFilter(title, options.toTypedArray(), state)
protected class OrderByFilter(title: String, options: List<Pair<String, String>>, state: Int = 0) :
UriPartFilter(title, options.toTypedArray(), state)
protected class GenreConditionFilter(title: String, options: List<Pair<String, String>>) :
UriPartFilter(
title,
options.toTypedArray(),
)
protected class GenreConditionFilter(title: String, options: List<Pair<String, String>>) : UriPartFilter(
title,
options.toTypedArray(),
)
protected class AdultContentFilter(title: String, options: List<Pair<String, String>>) :
UriPartFilter(
title,
options.toTypedArray(),
)
protected class AdultContentFilter(title: String, options: List<Pair<String, String>>) : UriPartFilter(
title,
options.toTypedArray(),
)
protected class GenreList(title: String, genres: List<Genre>) : Filter.Group<GenreCheckBox>(title, genres.map { GenreCheckBox(it.name, it.id) })
class GenreCheckBox(name: String, val id: String = name) : Filter.CheckBox(name)
@ -753,24 +757,32 @@ abstract class Madara(
open val altName = intl["alt_names_heading"]
open val updatingRegex = "Updating|Atualizando".toRegex(RegexOption.IGNORE_CASE)
fun String.notUpdating(): Boolean = this.contains(updatingRegex).not()
fun String.notUpdating(): Boolean {
return this.contains(updatingRegex).not()
}
private fun String.containsIn(array: Array<String>): Boolean = this.lowercase() in array.map { it.lowercase() }
private fun String.containsIn(array: Array<String>): Boolean {
return this.lowercase() in array.map { it.lowercase() }
}
protected open fun imageFromElement(element: Element): String? = 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 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")
}
}
/**
* Get the best image quality available from srcset
*/
protected fun String.getSrcSetImage(): String? = this.split(" ")
.filter(URL_REGEX::matches)
.maxOfOrNull(String::toString)
protected fun String.getSrcSetImage(): String? {
return this.split(" ")
.filter(URL_REGEX::matches)
.maxOfOrNull(String::toString)
}
/**
* Set it to true if the source uses the new AJAX endpoint to
@ -795,7 +807,9 @@ abstract class Madara(
return POST("$baseUrl/wp-admin/admin-ajax.php", xhrHeaders, form)
}
protected open fun xhrChaptersRequest(mangaUrl: String): Request = POST("$mangaUrl/ajax/chapters", xhrHeaders)
protected open fun xhrChaptersRequest(mangaUrl: String): Request {
return POST("$mangaUrl/ajax/chapters", xhrHeaders)
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
@ -866,10 +880,12 @@ abstract class Madara(
open fun parseChapterDate(date: String?): Long {
date ?: return 0
fun SimpleDateFormat.tryParse(string: String): Long = try {
parse(string)?.time ?: 0
} catch (_: ParseException) {
0
fun SimpleDateFormat.tryParse(string: String): Long {
return try {
parse(string)?.time ?: 0
} catch (_: ParseException) {
0
}
}
return when {
@ -990,7 +1006,9 @@ abstract class Madara(
}
}
override fun imageRequest(page: Page): Request = GET(page.imageUrl!!, headers.newBuilder().set("Referer", page.url).build())
override fun imageRequest(page: Page): Request {
return GET(page.imageUrl!!, headers.newBuilder().set("Referer", page.url).build())
}
override fun imageUrlParse(document: Document) = ""
@ -1065,22 +1083,26 @@ abstract class Madara(
/**
* The request to the search page (or another one) that have the genres list.
*/
protected open fun genresRequest(): Request = GET("$baseUrl/?s=genre&post_type=wp-manga", headers)
protected open fun genresRequest(): Request {
return GET("$baseUrl/?s=genre&post_type=wp-manga", headers)
}
/**
* Get the genres from the search page document.
*
* @param document The search page document
*/
protected open fun parseGenres(document: Document): List<Genre> = document.selectFirst("div.checkbox-group")
?.select("div.checkbox")
.orEmpty()
.map { li ->
Genre(
li.selectFirst("label")!!.text(),
li.selectFirst("input[type=checkbox]")!!.`val`(),
)
}
protected open fun parseGenres(document: Document): List<Genre> {
return document.selectFirst("div.checkbox-group")
?.select("div.checkbox")
.orEmpty()
.map { li ->
Genre(
li.selectFirst("label")!!.text(),
li.selectFirst("input[type=checkbox]")!!.`val`(),
)
}
}
// https://stackoverflow.com/a/66614516
protected fun String.decodeHex(): ByteArray {

View File

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

View File

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

View File

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

View File

@ -6,11 +6,13 @@ import org.jsoup.nodes.Element
class CoffeeManga : Madara("Coffee Manga", "https://coffeemanga.io", "en") {
override val useNewChapterEndpoint = false
override fun imageFromElement(element: Element): String? = when {
element.attr("data-src").isNotBlank() -> element.attr("abs:data-src")
element.attr("data-lazy-src").isNotBlank() -> element.attr("abs:data-lazy-src")
element.attr("srcset").isNotBlank() -> element.attr("abs:srcset").getSrcSetImage()
element.attr("data-cfsrc").isNotBlank() -> element.attr("abs:data-cfsrc")
else -> element.attr("abs:src")
override fun imageFromElement(element: Element): String? {
return when {
element.attr("data-src").isNotBlank() -> element.attr("abs:data-src")
element.attr("data-lazy-src").isNotBlank() -> element.attr("abs:data-lazy-src")
element.attr("srcset").isNotBlank() -> element.attr("abs:srcset").getSrcSetImage()
element.attr("data-cfsrc").isNotBlank() -> element.attr("abs:data-cfsrc")
else -> element.attr("abs:src")
}
}
}

View File

@ -2,12 +2,11 @@ package eu.kanade.tachiyomi.extension.en.luascans
import eu.kanade.tachiyomi.multisrc.heancms.HeanCms
class LuaScans :
HeanCms(
"Lua Scans",
"https://luacomic.org",
"en",
) {
class LuaScans : HeanCms(
"Lua Scans",
"https://luacomic.org",
"en",
) {
// Moved from Keyoapp to HeanCms
override val versionId = 3

View File

@ -1,7 +1,7 @@
ext {
extName = 'Mangamo'
extClass = '.Mangamo'
extVersionCode = 2
extVersionCode = 1
isNsfw = false
}

View File

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.network.POST
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.internal.EMPTY_HEADERS
class MangamoAuth(
private val helper: MangamoHelper,
@ -52,7 +53,8 @@ class MangamoAuth(
val googleIdentityResponse = client.newCall(
POST(
"https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${MangamoConstants.FIREBASE_API_KEY}",
body = "{\"token\":\"$customToken\",\"returnSecureToken\":true}".toRequestBody(),
EMPTY_HEADERS,
"{\"token\":\"$customToken\",\"returnSecureToken\":true}".toRequestBody(),
),
).execute()
@ -95,7 +97,8 @@ class MangamoAuth(
val googleIdentityResponse = client.newCall(
POST(
"https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=${MangamoConstants.FIREBASE_API_KEY}",
body = "{\"returnSecureToken\":true}".toRequestBody(),
EMPTY_HEADERS,
"{\"returnSecureToken\":true}".toRequestBody(),
),
).execute()

View File

@ -2,12 +2,11 @@ package eu.kanade.tachiyomi.extension.en.retsu
import eu.kanade.tachiyomi.multisrc.madara.Madara
class Retsu :
Madara(
"Retsu",
"https://retsu.org",
"en",
) {
class Retsu : Madara(
"Retsu",
"https://retsu.org",
"en",
) {
override fun popularMangaSelector() = "div.manga__item"
override val popularMangaUrlSelector = "h4 a"

View File

@ -12,13 +12,12 @@ import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Locale
class SirenKomik :
MangaThemesia(
"Siren Komik",
"https://sirenkomik.my.id",
"id",
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("id")),
) {
class SirenKomik : MangaThemesia(
"Siren Komik",
"https://sirenkomik.my.id",
"id",
dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale("id")),
) {
override val id = 8457447675410081142
override val hasProjectPage = true

View File

@ -32,9 +32,7 @@ import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class SussyToons :
HttpSource(),
ConfigurableSource {
class SussyToons : HttpSource(), ConfigurableSource {
override val name = "Sussy Toons"
@ -104,7 +102,9 @@ class SussyToons :
// ============================= Popular ==================================
override fun popularMangaRequest(page: Int): Request = GET("$apiUrl/obras/top5", headers)
override fun popularMangaRequest(page: Int): Request {
return GET("$apiUrl/obras/top5", headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val dto = response.parseAs<WrapperDto<List<MangaDto>>>()
@ -152,7 +152,8 @@ class SussyToons :
return GET(url, headers)
}
override fun mangaDetailsParse(response: Response) = response.parseAs<WrapperDto<MangaDto>>().results.toSManga()
override fun mangaDetailsParse(response: Response) =
response.parseAs<WrapperDto<MangaDto>>().results.toSManga()
private val SManga.id: String get() {
val mangaUrl = apiUrl.toHttpUrl().newBuilder()
@ -165,16 +166,18 @@ class SussyToons :
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> = response.parseAs<WrapperDto<WrapperChapterDto>>().results.chapters.map {
SChapter.create().apply {
name = it.name
it.chapterNumber?.let {
chapter_number = it
override fun chapterListParse(response: Response): List<SChapter> {
return response.parseAs<WrapperDto<WrapperChapterDto>>().results.chapters.map {
SChapter.create().apply {
name = it.name
it.chapterNumber?.let {
chapter_number = it
}
setUrlWithoutDomain("$baseUrl/capitulo/${it.id}")
date_upload = it.updateAt.toDate()
}
setUrlWithoutDomain("$baseUrl/capitulo/${it.id}")
date_upload = it.updateAt.toDate()
}
}.sortedBy(SChapter::chapter_number).reversed()
}.sortedBy(SChapter::chapter_number).reversed()
}
// ============================= Pages ====================================
@ -205,22 +208,30 @@ class SussyToons :
Page(index, imageUrl = imageUrl.toString())
}
}
private fun pageListParse(document: Document): List<Page> = document.select(pageUrlSelector).mapIndexed { index, element ->
Page(index, document.location(), element.absUrl("src"))
private fun pageListParse(document: Document): List<Page> {
return document.select(pageUrlSelector).mapIndexed { index, element ->
Page(index, document.location(), element.absUrl("src"))
}
}
private fun extractScriptData(document: Document): String {
return document.select("script").map(Element::data)
.firstOrNull(pageRegex::containsMatchIn)
?: throw Exception("Failed to load pages: Script data not found")
}
private fun extractScriptData(document: Document): String = document.select("script").map(Element::data)
.firstOrNull(pageRegex::containsMatchIn)
?: throw Exception("Failed to load pages: Script data not found")
private fun extractJsonContent(scriptData: String): String = pageRegex.find(scriptData)
?.groups?.get(1)?.value
?.let { json.decodeFromString<String>("\"$it\"") }
?: throw Exception("Failed to extract JSON from script")
private fun extractJsonContent(scriptData: String): String {
return pageRegex.find(scriptData)
?.groups?.get(1)?.value
?.let { json.decodeFromString<String>("\"$it\"") }
?: throw Exception("Failed to extract JSON from script")
}
private fun parseJsonToChapterPageDto(jsonContent: String): ChapterPageDto = try {
jsonContent.parseAs<WrapperDto<ChapterPageDto>>().results
} catch (e: Exception) {
throw Exception("Failed to load pages: ${e.message}")
private fun parseJsonToChapterPageDto(jsonContent: String): ChapterPageDto {
return try {
jsonContent.parseAs<WrapperDto<ChapterPageDto>>().results
} catch (e: Exception) {
throw Exception("Failed to load pages: ${e.message}")
}
}
override fun imageUrlParse(response: Response): String = ""
@ -334,14 +345,13 @@ class SussyToons :
return json.decodeFromStream(body.byteStream())
}
private inline fun <reified T> String.parseAs(): T = json.decodeFromString(this)
private fun String.toDate() = try {
dateFormat.parse(this)!!.time
} catch (_: Exception) {
0L
private inline fun <reified T> String.parseAs(): T {
return json.decodeFromString(this)
}
private fun String.toDate() =
try { dateFormat.parse(this)!!.time } catch (_: Exception) { 0L }
/**
* Normalizes path segments:
* Ex: [ "/a/b/", "/a/b", "a/b/", "a/b" ]

View File

@ -41,11 +41,13 @@ class MangaDto(
@SerialName("stt_nome")
val value: String?,
) {
fun toStatus(): Int = when (value?.lowercase()) {
"em andamento" -> SManga.ONGOING
"completo" -> SManga.COMPLETED
"hiato" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
fun toStatus(): Int {
return when (value?.lowercase()) {
"em andamento" -> SManga.ONGOING
"completo" -> SManga.COMPLETED
"hiato" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
}
}

View File

@ -32,6 +32,8 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.internal.http.HTTP_FORBIDDEN
import okhttp3.internal.http.HTTP_UNAUTHORIZED
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
@ -39,8 +41,6 @@ import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.net.HttpURLConnection.HTTP_FORBIDDEN
import java.net.HttpURLConnection.HTTP_UNAUTHORIZED
import java.text.SimpleDateFormat
import java.util.Locale
@ -85,13 +85,15 @@ class Taiyo : ParsedHttpSource() {
// =============================== Search ===============================
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/media/$id"))
.asObservableSuccess()
.map(::searchMangaByIdParse)
} else {
super.fetchSearchManga(page, query, filters)
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/media/$id"))
.asObservableSuccess()
.map(::searchMangaByIdParse)
} else {
super.fetchSearchManga(page, query, filters)
}
}
private fun searchMangaByIdParse(response: Response): MangasPage {
@ -144,11 +146,17 @@ class Taiyo : ParsedHttpSource() {
return MangasPage(mangas, mangas.isNotEmpty())
}
override fun searchMangaSelector(): String = throw UnsupportedOperationException()
override fun searchMangaSelector(): String {
throw UnsupportedOperationException()
}
override fun searchMangaFromElement(element: Element): SManga = throw UnsupportedOperationException()
override fun searchMangaFromElement(element: Element): SManga {
throw UnsupportedOperationException()
}
override fun searchMangaNextPageSelector(): String? = throw UnsupportedOperationException()
override fun searchMangaNextPageSelector(): String? {
throw UnsupportedOperationException()
}
// =========================== Manga Details ============================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
@ -227,9 +235,13 @@ class Taiyo : ParsedHttpSource() {
return Observable.just(chapters.sortedByDescending { it.chapter_number })
}
override fun chapterListSelector(): String = throw UnsupportedOperationException()
override fun chapterListSelector(): String {
throw UnsupportedOperationException()
}
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException()
override fun chapterFromElement(element: Element): SChapter {
throw UnsupportedOperationException()
}
// =============================== Pages ================================
override fun pageListParse(document: Document): List<Page> {
@ -244,7 +256,9 @@ class Taiyo : ParsedHttpSource() {
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
override fun imageUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
// ============================= Utilities ==============================
private fun Element.getImageUrl() = absUrl("srcset").substringBefore(" ")
@ -253,19 +267,23 @@ class Taiyo : ParsedHttpSource() {
json.decodeFromStream(it.body.byteStream())
}
private fun String.toDate(): Long = runCatching { DATE_FORMATTER.parse(this)?.time }
.getOrNull() ?: 0L
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(this)?.time }
.getOrNull() ?: 0L
}
private inline fun <reified T> Document.parseJsonFromDocument(
itemName: String = "media",
crossinline transformer: String.() -> String,
): T? = runCatching {
val script = selectFirst("script:containsData($itemName\\\\\":):containsData(\\\"6:\\[)")!!.data()
val obj = script.substringAfter(",{\\\"$itemName\\\":")
.run(transformer)
.replace("\\", "")
json.decodeFromString<T>(obj)
}.onFailure { it.printStackTrace() }.getOrNull()
): T? {
return runCatching {
val script = selectFirst("script:containsData($itemName\\\\\":):containsData(\\\"6:\\[)")!!.data()
val obj = script.substringAfter(",{\\\"$itemName\\\":")
.run(transformer)
.replace("\\", "")
json.decodeFromString<T>(obj)
}.onFailure { it.printStackTrace() }.getOrNull()
}
// ============================= Authorization ========================
@ -286,10 +304,12 @@ class Taiyo : ParsedHttpSource() {
return chain.proceed(req)
}
private fun getToken(): String = fetchBearerToken().also {
preferences.edit()
.putString(BEARER_TOKEN_PREF, it)
.apply()
private fun getToken(): String {
return fetchBearerToken().also {
preferences.edit()
.putString(BEARER_TOKEN_PREF, it)
.apply()
}
}
private fun fetchBearerToken(): String {

View File

@ -93,7 +93,8 @@ class HattoriManga : HttpSource() {
csrfToken = document.selectFirst("meta[name=csrf-token]")!!.attr("content")
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
override fun chapterListParse(response: Response) =
throw UnsupportedOperationException()
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val slug = manga.url.substringAfterLast('/')
@ -115,9 +116,10 @@ class HattoriManga : HttpSource() {
return Observable.just(chapters)
}
private fun fetchChapterPageableList(slug: String, page: Int, manga: SManga): HMChapterDto = client.newCall(GET("$baseUrl/load-more-chapters/$slug?page=$page", headers))
.execute()
.parseAs<HMChapterDto>()
private fun fetchChapterPageableList(slug: String, page: Int, manga: SManga): HMChapterDto =
client.newCall(GET("$baseUrl/load-more-chapters/$slug?page=$page", headers))
.execute()
.parseAs<HMChapterDto>()
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-chapters")
@ -147,20 +149,24 @@ class HattoriManga : HttpSource() {
}
}
override fun pageListParse(response: Response): List<Page> = response.asJsoup().select(".image-wrapper img").mapIndexed { index, element ->
Page(index, imageUrl = "$baseUrl${element.attr("data-src")}")
}.takeIf { it.isNotEmpty() } ?: throw Exception("Oturum açmanız, WebView'ı açmanız ve oturum açmanız gerekir")
override fun pageListParse(response: Response): List<Page> {
return response.asJsoup().select(".image-wrapper img").mapIndexed { index, element ->
Page(index, imageUrl = "$baseUrl${element.attr("data-src")}")
}.takeIf { it.isNotEmpty() } ?: throw Exception("Oturum açmanız, WebView'ı açmanız ve oturum açmanız gerekir")
}
override fun latestUpdatesParse(response: Response): MangasPage = response.use {
val mangas = it.parseAs<HMLatestUpdateDto>().chapters.map {
SManga.create().apply {
val manga = it.manga
title = manga.title
thumbnail_url = "$baseUrl/storage/${manga.thumbnail}"
url = "/manga/${manga.slug}"
}
}.distinctBy { manga -> manga.title }
MangasPage(mangas, false)
override fun latestUpdatesParse(response: Response): MangasPage {
return response.use {
val mangas = it.parseAs<HMLatestUpdateDto>().chapters.map {
SManga.create().apply {
val manga = it.manga
title = manga.title
thumbnail_url = "$baseUrl/storage/${manga.thumbnail}"
url = "/manga/${manga.slug}"
}
}.distinctBy { manga -> manga.title }
MangasPage(mangas, false)
}
}
override fun popularMangaParse(response: Response): MangasPage {
@ -257,18 +263,19 @@ class HattoriManga : HttpSource() {
setUrlWithoutDomain(REGEX_MANGA_URL.find(script)!!.groups[1]!!.value)
}
private fun parseGenres(document: Document): List<Genre> = document.select(".tags-blog a")
.map { element -> Genre(element.text()) }
private fun parseGenres(document: Document): List<Genre> {
return document.select(".tags-blog a")
.map { element -> Genre(element.text()) }
}
private inline fun <reified T> Response.parseAs(): T = json.decodeFromString(body.string())
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
private fun Response.isPageExpired() = code == 419
private fun String.toDate(): Long = try {
dateFormat.parse(trim())!!.time
} catch (_: Exception) {
0L
}
private fun String.toDate(): Long =
try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
class GenreList(title: String, genres: List<Genre>) : Filter.Group<GenreCheckBox>(title, genres.map { GenreCheckBox(it.name, it.id) })

View File

@ -25,9 +25,7 @@ import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
class LxHentai :
ParsedHttpSource(),
ConfigurableSource {
class LxHentai : ParsedHttpSource(), ConfigurableSource {
override val name = "LXHentai"
@ -43,30 +41,38 @@ class LxHentai :
override fun headersBuilder(): Headers.Builder = super.headersBuilder().add("Referer", baseUrl)
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", FilterList(SortBy(3)))
override fun popularMangaRequest(page: Int) =
searchMangaRequest(page, "", FilterList(SortBy(3)))
override fun popularMangaSelector() = searchMangaSelector()
override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element)
override fun popularMangaFromElement(element: Element) =
searchMangaFromElement(element)
override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
override fun popularMangaNextPageSelector() =
searchMangaNextPageSelector()
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", FilterList(SortBy(0)))
override fun latestUpdatesRequest(page: Int) =
searchMangaRequest(page, "", FilterList(SortBy(0)))
override fun latestUpdatesSelector() = searchMangaSelector()
override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
override fun latestUpdatesFromElement(element: Element) =
searchMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
override fun latestUpdatesNextPageSelector() =
searchMangaNextPageSelector()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = when {
query.startsWith(PREFIX_ID_SEARCH) -> {
val slug = query.substringAfter(PREFIX_ID_SEARCH)
val mangaUrl = "/truyen/$slug"
fetchMangaDetails(SManga.create().apply { url = mangaUrl })
.map { MangasPage(listOf(it), false) }
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return when {
query.startsWith(PREFIX_ID_SEARCH) -> {
val slug = query.substringAfter(PREFIX_ID_SEARCH)
val mangaUrl = "/truyen/$slug"
fetchMangaDetails(SManga.create().apply { url = mangaUrl })
.map { MangasPage(listOf(it), false) }
}
else -> super.fetchSearchManga(page, query, filters)
}
else -> super.fetchSearchManga(page, query, filters)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
@ -162,33 +168,32 @@ class LxHentai :
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>, state: Int = 0) : Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>, state: Int = 0) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
fun toUriPart() = vals[state].second
}
private class SortBy(state: Int = 0) :
UriPartFilter(
"Sắp xếp theo",
arrayOf(
Pair("Mới cập nhật", "-updated_at"),
Pair("Mới nhất", "-created_at"),
Pair("Cũ nhất", "created_at"),
Pair("Xem nhiều", "-views"),
Pair("A-Z", "name"),
Pair("Z-A", "-name"),
),
state,
)
private class SortBy(state: Int = 0) : UriPartFilter(
"Sắp xếp theo",
arrayOf(
Pair("Mới cập nhật", "-updated_at"),
Pair("Mới nhất", "-created_at"),
Pair("Cũ nhất", "created_at"),
Pair("Xem nhiều", "-views"),
Pair("A-Z", "name"),
Pair("Z-A", "-name"),
),
state,
)
private class Status :
UriPartFilter(
"Trạng thái",
arrayOf(
Pair("Tất cả", "1,2"),
Pair("Đang tiến hành", "2"),
Pair("Đã hoàn thành", "1"),
),
)
private class Status : UriPartFilter(
"Trạng thái",
arrayOf(
Pair("Tất cả", "1,2"),
Pair("Đang tiến hành", "2"),
Pair("Đã hoàn thành", "1"),
),
)
private class Genre(name: String, val id: Int) : Filter.TriState(name)
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Thể loại", genres)

View File

@ -34,31 +34,39 @@ class TruyenTranh3Q : ParsedHttpSource() {
.rateLimit(3)
.build()
override fun headersBuilder(): Headers.Builder = super.headersBuilder().add("Referer", "$baseUrl/")
override fun headersBuilder(): Headers.Builder {
return super.headersBuilder().add("Referer", "$baseUrl/")
}
private val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.US)
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/danh-sach/truyen-yeu-thich?page=$page", headers)
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/danh-sach/truyen-yeu-thich?page=$page", headers)
}
override fun popularMangaSelector(): String = "ul.list_grid.grid > li"
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
element.select("h3 a").let {
title = it.text()
setUrlWithoutDomain(it.attr("abs:href"))
}
thumbnail_url = element.selectFirst(".book_avatar a img")
?.absUrl("src")
?.let { url ->
url.toHttpUrlOrNull()
?.queryParameter("url")
?: url
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
element.select("h3 a").let {
title = it.text()
setUrlWithoutDomain(it.attr("abs:href"))
}
thumbnail_url = element.selectFirst(".book_avatar a img")
?.absUrl("src")
?.let { url ->
url.toHttpUrlOrNull()
?.queryParameter("url")
?: url
}
}
}
override fun popularMangaNextPageSelector(): String? = ".page_redirect > a:last-child > p:not(.active)"
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/danh-sach/truyen-moi-cap-nhat?page=$page", headers)
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/danh-sach/truyen-moi-cap-nhat?page=$page", headers)
}
// same as popularManga
override fun latestUpdatesSelector(): String = popularMangaSelector()
@ -113,19 +121,21 @@ class TruyenTranh3Q : ParsedHttpSource() {
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun searchMangaNextPageSelector(): String? = popularMangaNextPageSelector()
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
document.selectFirst(".book_info > .book_other")?.let { info ->
title = info.selectFirst("h1[itemprop=name]")!!.text()
author = info.selectFirst("ul.list-info li.author p.col-xs-9")?.text()
status = when (info.selectFirst("ul.list-info li.status p.col-xs-9")?.text()) {
"Đang Cập Nhật" -> SManga.ONGOING
"Hoàn Thành" -> SManga.COMPLETED
else -> SManga.UNKNOWN
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
document.selectFirst(".book_info > .book_other")?.let { info ->
title = info.selectFirst("h1[itemprop=name]")!!.text()
author = info.selectFirst("ul.list-info li.author p.col-xs-9")?.text()
status = when (info.selectFirst("ul.list-info li.status p.col-xs-9")?.text()) {
"Đang Cập Nhật" -> SManga.ONGOING
"Hoàn Thành" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
genre = info.select(".list01 li a").joinToString { it.text() }
}
genre = info.select(".list01 li a").joinToString { it.text() }
description = document.selectFirst(".book_detail > .story-detail-info")?.text()
thumbnail_url = document.selectFirst(".book_detail > .book_info > .book_avatar > img")?.attr("abs:src")
}
description = document.selectFirst(".book_detail > .story-detail-info")?.text()
thumbnail_url = document.selectFirst(".book_detail > .book_info > .book_avatar > img")?.attr("abs:src")
}
// chapters
@ -173,19 +183,23 @@ class TruyenTranh3Q : ParsedHttpSource() {
}
}
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
element.selectFirst(".name-chap > a")?.let {
name = it.text()
setUrlWithoutDomain(it.attr("abs:href"))
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
element.selectFirst(".name-chap > a")?.let {
name = it.text()
setUrlWithoutDomain(it.attr("abs:href"))
}
date_upload = parseChapterDate(element.selectFirst(".time-chap")?.text() ?: "")
}
date_upload = parseChapterDate(element.selectFirst(".time-chap")?.text() ?: "")
}
// parse pages
private val pageListSelector = ".chapter_content .page-chapter img"
override fun pageListParse(document: Document): List<Page> = document.select(pageListSelector).mapIndexed { idx, it ->
Page(idx, imageUrl = it.absUrl("data-src"))
override fun pageListParse(document: Document): List<Page> {
return document.select(pageListSelector).mapIndexed { idx, it ->
Page(idx, imageUrl = it.absUrl("data-src"))
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
@ -234,8 +248,10 @@ class TruyenTranh3Q : ParsedHttpSource() {
private fun genresRequest() = GET("$baseUrl/$searchPath", headers)
private fun parseGenres(document: Document): List<Genre> = document.select(".genre-item").mapIndexed { index, element ->
Genre(element.text(), index + 1)
private fun parseGenres(document: Document): List<Genre> {
return document.select(".genre-item").mapIndexed { index, element ->
Genre(element.text(), index + 1)
}
}
private fun fetchGenres() {

View File

@ -36,9 +36,7 @@ import java.io.IOException
import java.net.URLDecoder
import java.util.concurrent.TimeUnit
class YuriNeko :
HttpSource(),
ConfigurableSource {
class YuriNeko : HttpSource(), ConfigurableSource {
override val name = "YuriNeko"
@ -109,20 +107,22 @@ class YuriNeko :
override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = when {
query.startsWith(PREFIX_ID_SEARCH) -> {
val id = query.removePrefix(PREFIX_ID_SEARCH).trim()
if (id.toIntOrNull() == null) {
throw Exception("ID tìm kiếm không hợp lệ (phải là một số).")
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return when {
query.startsWith(PREFIX_ID_SEARCH) -> {
val id = query.removePrefix(PREFIX_ID_SEARCH).trim()
if (id.toIntOrNull() == null) {
throw Exception("ID tìm kiếm không hợp lệ (phải là một số).")
}
fetchMangaDetails(
SManga.create().apply {
url = "/manga/$id"
},
)
.map { MangasPage(listOf(it), false) }
}
fetchMangaDetails(
SManga.create().apply {
url = "/manga/$id"
},
)
.map { MangasPage(listOf(it), false) }
else -> super.fetchSearchManga(page, query, filters)
}
else -> super.fetchSearchManga(page, query, filters)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
@ -182,13 +182,15 @@ class YuriNeko :
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = client.newCall(GET("$apiUrl${manga.url}"))
.asObservableSuccess()
.map { mangaDetailsParse(it) }
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
client.newCall(GET("$apiUrl${manga.url}"))
.asObservableSuccess()
.map { mangaDetailsParse(it) }
override fun mangaDetailsRequest(manga: SManga): Request = GET("$baseUrl${manga.url}")
override fun mangaDetailsParse(response: Response): SManga = response.parseAs<MangaDto>().toSManga(storageUrl)
override fun mangaDetailsParse(response: Response): SManga =
response.parseAs<MangaDto>().toSManga(storageUrl)
override fun chapterListRequest(manga: SManga): Request = GET("$apiUrl${manga.url}")
@ -200,11 +202,13 @@ class YuriNeko :
override fun pageListRequest(chapter: SChapter): Request = GET("$apiUrl${chapter.url}")
override fun pageListParse(response: Response): List<Page> = response.parseAs<ReadResponseDto>().toPageList(storageUrl)
override fun pageListParse(response: Response): List<Page> =
response.parseAs<ReadResponseDto>().toPageList(storageUrl)
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) : Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}