Update MadTheme, migrate ManhuaScan to MadTheme (#3072)

* MadTheme: general cleanup

* MadTheme: add support for both site formats

* Remove ManhuaScan

* Add KaliScan.com, KaliScan.io, MGJinx

* MadTheme: bump base version

* Add KaliScan.me

* Only set genreKey once
This commit is contained in:
Vetle Ledaal 2024-05-17 12:04:27 +00:00 committed by GitHub
parent c5c4dfe6d5
commit 7ef6508210
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 124 additions and 495 deletions

View File

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

View File

@ -21,6 +21,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
@ -29,24 +30,25 @@ import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.concurrent.TimeUnit
abstract class MadTheme(
override val name: String,
override val baseUrl: String,
override val lang: String,
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM dd, yyy", Locale.US),
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.ENGLISH),
) : ParsedHttpSource() {
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimit(1, 1)
.rateLimit(1, 1, TimeUnit.SECONDS)
.build()
// TODO: better cookie sharing
// TODO: don't count cached responses against rate limit
private val chapterClient: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimit(1, 12)
.rateLimit(1, 12, TimeUnit.SECONDS)
.build()
override fun headersBuilder() = Headers.Builder().apply {
@ -55,6 +57,8 @@ abstract class MadTheme(
private val json: Json by injectLazy()
private var genreKey = "genre[]"
// Popular
override fun popularMangaRequest(page: Int): Request =
searchMangaRequest(page, "", FilterList(OrderFilter(0)))
@ -100,7 +104,7 @@ abstract class MadTheme(
.filter { it.state }
.let { list ->
if (list.isNotEmpty()) {
list.forEach { genre -> url.addQueryParameter("genre[]", genre.id) }
list.forEach { genre -> url.addQueryParameter(genreKey, genre.id) }
}
}
}
@ -120,11 +124,11 @@ abstract class MadTheme(
override fun searchMangaSelector(): String = ".book-detailed-item"
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
setUrlWithoutDomain(element.select("a").first()!!.attr("abs:href"))
title = element.select("a").first()!!.attr("title")
description = element.select(".summary").first()?.text()
genre = element.select(".genres > *").joinToString { it.text() }
thumbnail_url = element.select("img").first()!!.attr("abs:data-src")
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
title = element.selectFirst("a")!!.attr("title")
element.selectFirst(".summary")?.text()?.let { description = it }
element.select(".genres > *").joinToString { it.text() }.takeIf { it.isNotEmpty() }?.let { genre = it }
thumbnail_url = element.selectFirst("img")!!.attr("abs:data-src")
}
/*
@ -135,23 +139,25 @@ abstract class MadTheme(
// Details
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
title = document.select(".detail h1").first()!!.text()
title = document.selectFirst(".detail h1")!!.text()
author = document.select(".detail .meta > p > strong:contains(Authors) ~ a").joinToString { it.text().trim(',', ' ') }
genre = document.select(".detail .meta > p > strong:contains(Genres) ~ a").joinToString { it.text().trim(',', ' ') }
thumbnail_url = document.select("#cover img").first()!!.attr("abs:data-src")
thumbnail_url = document.selectFirst("#cover img")!!.attr("abs:data-src")
val altNames = document.select(".detail h2").first()?.text()
val altNames = document.selectFirst(".detail h2")?.text()
?.split(',', ';')
?.mapNotNull { it.trim().takeIf { it != title } }
?: listOf()
description = document.select(".summary .content").first()?.text() +
description = document.select(".summary .content, .summary .content ~ p").text() +
(altNames.takeIf { it.isNotEmpty() }?.let { "\n\nAlt name(s): ${it.joinToString()}" } ?: "")
val statusText = document.select(".detail .meta > p > strong:contains(Status) ~ a").first()!!.text()
status = when (statusText.lowercase(Locale.US)) {
val statusText = document.selectFirst(".detail .meta > p > strong:contains(Status) ~ a")!!.text()
status = when (statusText.lowercase(Locale.ENGLISH)) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
"on-hold" -> SManga.ON_HIATUS
"canceled" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
@ -187,7 +193,14 @@ abstract class MadTheme(
}
override fun chapterListRequest(manga: SManga): Request =
GET("$baseUrl/api/manga${manga.url}/chapters?source=detail", headers)
MANGA_ID_REGEX.find(manga.url)?.groupValues?.get(1)?.let { mangaId ->
val url = "$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
.addQueryParameter("manga_id", mangaId)
.addQueryParameter("manga_name", manga.title)
.build()
GET(url, headers)
} ?: GET("$baseUrl/api/manga${manga.url}/chapters?source=detail", headers)
override fun searchMangaParse(response: Response): MangasPage {
if (genresList == null) {
@ -204,16 +217,25 @@ abstract class MadTheme(
.absUrl("href")
.removePrefix(baseUrl)
name = element.select(".chapter-title").first()!!.text()
date_upload = parseChapterDate(element.select(".chapter-update").first()?.text())
name = element.selectFirst(".chapter-title")!!.text()
date_upload = parseChapterDate(element.selectFirst(".chapter-update")?.text())
}
// Pages
override fun pageListParse(document: Document): List<Page> {
val html = document.html()
val mangaId = MANGA_ID_REGEX.find(document.location())?.groupValues?.get(1)
val chapterId = CHAPTER_ID_REGEX.find(document.html())?.groupValues?.get(1)
val html = if (mangaId != null && chapterId != null) {
val url = GET("$baseUrl/service/backend/chapterServer/?server_id=1&chapter_id=$chapterId", headers)
client.newCall(url).execute().body.string()
} else {
document.html()
}
val realDocument = Jsoup.parse(html, document.location())
if (!html.contains("var mainServer = \"")) {
val chapterImagesFromHtml = document.select("#chapter-images img")
val chapterImagesFromHtml = realDocument.select("#chapter-images img, .chapter-image[data-src]")
// 17/03/2023: Certain hosts only embed two pages in their "#chapter-images" and leave
// the rest to be lazily(?) loaded by javascript. Let's extract `chapImages` and compare
@ -292,7 +314,7 @@ abstract class MadTheme(
}
return when {
"ago".endsWith(date) -> {
" ago" in date -> {
parseRelativeDate(date)
}
else -> dateFormat.tryParse(date)
@ -300,10 +322,12 @@ abstract class MadTheme(
}
private fun parseRelativeDate(date: String): Long {
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val number = NUMBER_REGEX.find(date)?.groupValues?.getOrNull(0)?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance()
return when {
date.contains("year") -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
date.contains("month") -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
date.contains("day") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
date.contains("hour") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
date.contains("minute") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
@ -314,13 +338,21 @@ abstract class MadTheme(
// Dynamic genres
private fun parseGenres(document: Document): List<Genre>? {
return document.select(".checkbox-group.genres").first()?.select("label")?.map {
Genre(it.select(".radio__label").first()!!.text(), it.select("input").`val`())
return document.selectFirst(".checkbox-group.genres")?.select(".checkbox-wrapper")?.run {
firstOrNull()?.selectFirst("input")?.attr("name")?.takeIf { it.isNotEmpty() }?.let { genreKey = it }
map {
Genre(it.selectFirst(".radio__label")!!.text(), it.selectFirst("input")!!.`val`())
}
}
}
// Filters
override fun getFilterList() = FilterList(
// TODO: Filters for sites that support it:
// excluded genres
// genre inclusion mode
// bookmarks
// author
GenreFilter(getGenreList()),
StatusFilter(),
OrderFilter(),
@ -352,6 +384,7 @@ abstract class MadTheme(
Pair("Updated", "updated_at"),
Pair("Created", "created_at"),
Pair("Name A-Z", "name"),
// Pair("Number of Chapters", "total_chapters"),
Pair("Rating", "rating"),
),
state,
@ -365,4 +398,10 @@ abstract class MadTheme(
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
fun toUriPart() = vals[state].second
}
companion object {
private val MANGA_ID_REGEX = """/manga/(\d+)-""".toRegex()
private val CHAPTER_ID_REGEX = """chapterId\s*=\s*(\d+)""".toRegex()
private val NUMBER_REGEX = """\d+""".toRegex()
}
}

View File

@ -0,0 +1,10 @@
ext {
extName = 'KaliScan.com'
extClass = '.KaliScanCom'
themePkg = 'madtheme'
baseUrl = 'https://kaliscan.com'
overrideVersionCode = 0
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.extension.en.kaliscancom
import eu.kanade.tachiyomi.multisrc.madtheme.MadTheme
class KaliScanCom : MadTheme("KaliScan.com", "https://kaliscan.com", "en")

View File

@ -0,0 +1,10 @@
ext {
extName = 'KaliScan.io'
extClass = '.KaliScanIo'
themePkg = 'madtheme'
baseUrl = 'https://kaliscan.io'
overrideVersionCode = 0
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.extension.en.kaliscanio
import eu.kanade.tachiyomi.multisrc.madtheme.MadTheme
class KaliScanIo : MadTheme("KaliScan.io", "https://kaliscan.io", "en")

View File

@ -0,0 +1,10 @@
ext {
extName = 'KaliScan.me'
extClass = '.KaliScanMe'
themePkg = 'madtheme'
baseUrl = 'https://kaliscan.me'
overrideVersionCode = 0
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.extension.en.kaliscanme
import eu.kanade.tachiyomi.multisrc.madtheme.MadTheme
class KaliScanMe : MadTheme("KaliScan.me", "https://kaliscan.me", "en")

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,333 +0,0 @@
package eu.kanade.tachiyomi.extension.en.manhuascan
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Calendar
class ManhuaScan : ConfigurableSource, ParsedHttpSource() {
override val lang = "en"
override val supportsLatest = true
override val name = "ManhuaScan"
private val preferences: SharedPreferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
override val baseUrl = getMirror()
override val client by lazy {
network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()
}
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/popular${page.getPage()}", headers)
override fun popularMangaSelector(): String = ".manga-list > .book-item"
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
thumbnail_url = element.selectFirst(".thumb img")?.imgAttr()
element.selectFirst(".title a")!!.run {
title = text()
setUrlWithoutDomain(attr("abs:href"))
}
}
override fun popularMangaNextPageSelector(): String = ".paginator > .active + a"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/latest${page.getPage()}", headers)
override fun latestUpdatesSelector(): String =
popularMangaSelector()
override fun latestUpdatesFromElement(element: Element): SManga =
popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector(): String =
popularMangaNextPageSelector()
// =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val genre = filters.firstInstanceOrNull<GenreFilter>()
val genreInclusion = filters.firstInstanceOrNull<GenreInclusionFilter>()
val status = filters.firstInstanceOrNull<StatusFilter>()
val orderBy = filters.firstInstanceOrNull<OrderByFilter>()
val author = filters.firstInstanceOrNull<AuthorFilter>()
val url = "$baseUrl/search${page.getPage()}".toHttpUrl().newBuilder().apply {
genre?.included?.forEach {
addEncodedQueryParameter("include[]", it)
}
genre?.excluded?.forEach {
addEncodedQueryParameter("exclude[]", it)
}
addQueryParameter("include_mode", genreInclusion?.toUriPart())
addQueryParameter("bookmark", "off")
addQueryParameter("status", status?.toUriPart())
addQueryParameter("sort", orderBy?.toUriPart())
if (query.isNotEmpty()) {
addQueryParameter("q", query)
}
if (author?.state?.isNotEmpty() == true) {
addQueryParameter("author", author.state)
}
}
return GET(url.build(), headers)
}
override fun searchMangaSelector(): String = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga =
popularMangaFromElement(element)
override fun searchMangaNextPageSelector(): String =
popularMangaNextPageSelector()
// =============================== Filters ==============================
override fun getFilterList(): FilterList = FilterList(
GenreFilter(),
GenreInclusionFilter(),
Filter.Separator(),
StatusFilter(),
OrderByFilter(),
AuthorFilter(),
)
// =========================== Manga Details ============================
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
var alternativeName = ""
document.selectFirst(".book-info")?.run {
genre = select(".meta p:has(strong:contains(Genres)) a").joinToString(", ") { it.text().removeSuffix(" ,") }
author = select(".meta p:has(strong:contains(Authors)) a").joinToString(", ") { it.text() }
thumbnail_url = selectFirst("#cover img")?.imgAttr()
status = selectFirst(".meta p:has(strong:contains(Status)) a").parseStatus()
title = selectFirst("h1")!!.text()
selectFirst("h2")?.also {
alternativeName = it.text()
}
}
description = buildString {
document.selectFirst(".summary > p:not([style]):not(:empty)")?.let {
append(it.text())
if (alternativeName.isNotEmpty()) {
append("\n\n")
}
}
if (alternativeName.isNotEmpty()) {
append("Alternative name(s): $alternativeName")
}
}
}
private fun Element?.parseStatus(): Int = with(this?.text()) {
return when {
equals("ongoing", true) -> SManga.ONGOING
equals("completed", true) -> SManga.COMPLETED
equals("on-hold", true) -> SManga.ON_HIATUS
equals("canceled", true) -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
// ============================== Chapters ==============================
override fun chapterListRequest(manga: SManga): Request {
val id = manga.url.substringAfter("manga/").substringBefore("-")
val chapterHeaders = headersBuilder().apply {
add("Accept", "*/*")
add("Host", baseUrl.toHttpUrl().host)
set("Referer", baseUrl + manga.url)
}.build()
val url = "$baseUrl/service/backend/chaplist/?manga_id=$id&manga_name=${manga.title}"
return GET(url, chapterHeaders)
}
override fun chapterListSelector() = "ul > li"
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
element.selectFirst("time")?.also {
date_upload = it.text().parseRelativeDate()
}
name = element.selectFirst("strong")!!.text()
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
}
// From OppaiStream
private fun String.parseRelativeDate(): Long {
val now = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
var parsedDate = 0L
val relativeDate = this.split(" ").firstOrNull()
?.replace("one", "1")
?.replace("a", "1")
?.toIntOrNull()
?: return 0L
when {
// parse: 30 seconds ago
"second" in this -> {
parsedDate = now.apply { add(Calendar.SECOND, -relativeDate) }.timeInMillis
}
// parses: "42 minutes ago"
"minute" in this -> {
parsedDate = now.apply { add(Calendar.MINUTE, -relativeDate) }.timeInMillis
}
// parses: "1 hour ago" and "2 hours ago"
"hour" in this -> {
parsedDate = now.apply { add(Calendar.HOUR, -relativeDate) }.timeInMillis
}
// parses: "2 days ago"
"day" in this -> {
parsedDate = now.apply { add(Calendar.DAY_OF_YEAR, -relativeDate) }.timeInMillis
}
// parses: "2 weeks ago"
"week" in this -> {
parsedDate = now.apply { add(Calendar.WEEK_OF_YEAR, -relativeDate) }.timeInMillis
}
// parses: "2 months ago"
"month" in this -> {
parsedDate = now.apply { add(Calendar.MONTH, -relativeDate) }.timeInMillis
}
// parse: "2 years ago"
"year" in this -> {
parsedDate = now.apply { add(Calendar.YEAR, -relativeDate) }.timeInMillis
}
}
return parsedDate
}
// =============================== Pages ================================
override fun pageListParse(document: Document): List<Page> {
val scriptData = document.selectFirst("script:containsData(chapterId)")?.data()
?: throw Exception("Unable to find script data")
val chapterId = CHAPTER_ID_REGEX.find(scriptData)?.groupValues?.get(1)
?: throw Exception("Unable to retrieve chapterId")
val pagesHeaders = headersBuilder().apply {
add("Accept", "*/*")
add("Host", baseUrl.toHttpUrl().host)
set("Referer", document.location())
}.build()
val pagesUrl = "$baseUrl/service/backend/chapterServer/?server_id=$server&chapter_id=$chapterId"
val pagesDocument = client.newCall(
GET(pagesUrl, pagesHeaders),
).execute().asJsoup()
return pagesDocument.select("div").map { page ->
val url = page.imgAttr()
val index = page.id().substringAfterLast("-").toInt()
Page(index, document.location(), url)
}.sortedBy { it.index }
}
override fun imageUrlParse(document: Document) = ""
override fun imageRequest(page: Page): Request {
val imgHeaders = headersBuilder().apply {
add("Accept", "*/*")
add("Host", page.imageUrl!!.toHttpUrl().host)
set("Referer", page.url)
}.build()
return GET(page.imageUrl!!, imgHeaders)
}
// ============================= Utilities ==============================
private fun Int.getPage(): String = if (this == 1) "" else "?page=$this"
private fun Element.imgAttr(): String = when {
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("data-src") -> attr("abs:data-src")
else -> attr("abs:src")
}
companion object {
private val CHAPTER_ID_REGEX = Regex("""chapterId\s*=\s*(\d+)""")
private const val MIRROR_PREF_KEY = "pref_mirror"
private const val MIRROR_PREF_TITLE = "Select Mirror (Requires Restart)"
private val MIRROR_PREF_ENTRIES = arrayOf("manhuascan.com", "manhuascan.io", "mangajinx.com")
private val MIRROR_PREF_ENTRY_VALUES = MIRROR_PREF_ENTRIES.map { "https://$it" }.toTypedArray()
private val MIRROR_PREF_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES.first()
private const val SERVER_PREF_KEY = "pref_server"
private const val SERVER_PREF_TITLE = "Image Server"
private val SERVER_PREF_ENTRIES = arrayOf("Server 1", "Server 2")
private val SERVER_PREF_ENTRY_VALUES = SERVER_PREF_ENTRIES.map { it.substringAfter(" ") }.toTypedArray()
private val SERVER_PREF_DEFAULT_VALUE = SERVER_PREF_ENTRY_VALUES.first()
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = MIRROR_PREF_KEY
title = MIRROR_PREF_TITLE
entries = MIRROR_PREF_ENTRIES
entryValues = MIRROR_PREF_ENTRY_VALUES
setDefaultValue(MIRROR_PREF_DEFAULT_VALUE)
summary = "%s"
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = SERVER_PREF_KEY
title = SERVER_PREF_TITLE
entries = SERVER_PREF_ENTRIES
entryValues = SERVER_PREF_ENTRY_VALUES
setDefaultValue(SERVER_PREF_DEFAULT_VALUE)
summary = "%s"
}.also(screen::addPreference)
}
private fun getMirror(): String =
preferences.getString(MIRROR_PREF_KEY, MIRROR_PREF_DEFAULT_VALUE)!!
private val server
get() = preferences.getString(SERVER_PREF_KEY, SERVER_PREF_DEFAULT_VALUE)!!
}

View File

@ -1,129 +0,0 @@
package eu.kanade.tachiyomi.extension.en.manhuascan
import eu.kanade.tachiyomi.source.model.Filter
inline fun <reified T> List<*>.firstInstanceOrNull() = firstOrNull { it is T } as? T
open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
class Genre(val id: String, name: String) : Filter.TriState(name)
class GenreFilter : Filter.Group<Genre>(
"Genres",
listOf(
Genre("action", "Action"),
Genre("adaptation", "Adaptation"),
Genre("adult", "Adult"),
Genre("adventure", "Adventure"),
Genre("animal", "Animal"),
Genre("anthology", "Anthology"),
Genre("cartoon", "Cartoon"),
Genre("comedy", "Comedy"),
Genre("comic", "Comic"),
Genre("cooking", "Cooking"),
Genre("demons", "Demons"),
Genre("doujinshi", "Doujinshi"),
Genre("drama", "Drama"),
Genre("ecchi", "Ecchi"),
Genre("fantasy", "Fantasy"),
Genre("full-color", "Full Color"),
Genre("game", "Game"),
Genre("gender-bender", "Gender bender"),
Genre("ghosts", "Ghosts"),
Genre("harem", "Harem"),
Genre("historical", "Historical"),
Genre("horror", "Horror"),
Genre("isekai", "Isekai"),
Genre("josei", "Josei"),
Genre("long-strip", "Long strip"),
Genre("mafia", "Mafia"),
Genre("magic", "Magic"),
Genre("manga", "Manga"),
Genre("manhua", "Manhua"),
Genre("manhwa", "Manhwa"),
Genre("martial-arts", "Martial arts"),
Genre("mature", "Mature"),
Genre("mecha", "Mecha"),
Genre("medical", "Medical"),
Genre("military", "Military"),
Genre("monster", "Monster"),
Genre("monster-girls", "Monster girls"),
Genre("monsters", "Monsters"),
Genre("music", "Music"),
Genre("mystery", "Mystery"),
Genre("office", "Office"),
Genre("office-workers", "Office workers"),
Genre("one-shot", "One shot"),
Genre("police", "Police"),
Genre("psychological", "Psychological"),
Genre("reincarnation", "Reincarnation"),
Genre("romance", "Romance"),
Genre("school-life", "School life"),
Genre("sci-fi", "Sci fi"),
Genre("science-fiction", "Science fiction"),
Genre("seinen", "Seinen"),
Genre("shoujo", "Shoujo"),
Genre("shoujo-ai", "Shoujo ai"),
Genre("shounen", "Shounen"),
Genre("shounen-ai", "Shounen ai"),
Genre("slice-of-life", "Slice of life"),
Genre("smut", "Smut"),
Genre("soft-yaoi", "Soft Yaoi"),
Genre("sports", "Sports"),
Genre("super-power", "Super Power"),
Genre("superhero", "Superhero"),
Genre("supernatural", "Supernatural"),
Genre("thriller", "Thriller"),
Genre("time-travel", "Time travel"),
Genre("tragedy", "Tragedy"),
Genre("vampire", "Vampire"),
Genre("vampires", "Vampires"),
Genre("video-games", "Video games"),
Genre("villainess", "Villainess"),
Genre("web-comic", "Web comic"),
Genre("webtoons", "Webtoons"),
Genre("yaoi", "Yaoi"),
Genre("yuri", "Yuri"),
Genre("zombies", "Zombies"),
),
) {
val included: List<String>?
get() = state.filter { it.isIncluded() }.map { it.id }.takeUnless { it.isEmpty() }
val excluded: List<String>?
get() = state.filter { it.isExcluded() }.map { it.id }.takeUnless { it.isEmpty() }
}
class GenreInclusionFilter : UriPartFilter(
"Genre Inclusion Mode",
arrayOf(
Pair("AND (All Selected Genres)", "and"),
Pair("OR (Any Selected Genres)", "or"),
),
)
class StatusFilter : UriPartFilter(
"Status",
arrayOf(
Pair("All", "all"),
Pair("Ongoing", "ongoing"),
Pair("Completed", "completed"),
),
)
class OrderByFilter : UriPartFilter(
"Order By",
arrayOf(
Pair("Views", "views"),
Pair("Updated", "updated_at"),
Pair("Created", "created_at"),
Pair("Name A-Z", "name"),
Pair("Number of Chapters", "total_chapters"),
Pair("Rating", "rating"),
),
)
class AuthorFilter : Filter.Text("Author name")

View File

@ -0,0 +1,10 @@
ext {
extName = 'MGJinx'
extClass = '.MGJinx'
themePkg = 'madtheme'
baseUrl = 'https://mgjinx.com'
overrideVersionCode = 0
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.extension.en.mgjinx
import eu.kanade.tachiyomi.multisrc.madtheme.MadTheme
class MGJinx : MadTheme("MGJinx", "https://mgjinx.com", "en")