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
| @ -2,4 +2,4 @@ plugins { | |||||||
|     id("lib-multisrc") |     id("lib-multisrc") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| baseVersionCode = 13 | baseVersionCode = 14 | ||||||
|  | |||||||
| @ -21,6 +21,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull | |||||||
| import okhttp3.OkHttpClient | import okhttp3.OkHttpClient | ||||||
| import okhttp3.Request | import okhttp3.Request | ||||||
| import okhttp3.Response | import okhttp3.Response | ||||||
|  | import org.jsoup.Jsoup | ||||||
| import org.jsoup.nodes.Document | import org.jsoup.nodes.Document | ||||||
| import org.jsoup.nodes.Element | import org.jsoup.nodes.Element | ||||||
| import rx.Observable | import rx.Observable | ||||||
| @ -29,24 +30,25 @@ import java.text.ParseException | |||||||
| import java.text.SimpleDateFormat | import java.text.SimpleDateFormat | ||||||
| import java.util.Calendar | import java.util.Calendar | ||||||
| import java.util.Locale | import java.util.Locale | ||||||
|  | import java.util.concurrent.TimeUnit | ||||||
| 
 | 
 | ||||||
| abstract class MadTheme( | abstract class MadTheme( | ||||||
|     override val name: String, |     override val name: String, | ||||||
|     override val baseUrl: String, |     override val baseUrl: String, | ||||||
|     override val lang: 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() { | ) : ParsedHttpSource() { | ||||||
| 
 | 
 | ||||||
|     override val supportsLatest = true |     override val supportsLatest = true | ||||||
| 
 | 
 | ||||||
|     override val client: OkHttpClient = network.cloudflareClient.newBuilder() |     override val client: OkHttpClient = network.cloudflareClient.newBuilder() | ||||||
|         .rateLimit(1, 1) |         .rateLimit(1, 1, TimeUnit.SECONDS) | ||||||
|         .build() |         .build() | ||||||
| 
 | 
 | ||||||
|     // TODO: better cookie sharing |     // TODO: better cookie sharing | ||||||
|     // TODO: don't count cached responses against rate limit |     // TODO: don't count cached responses against rate limit | ||||||
|     private val chapterClient: OkHttpClient = network.cloudflareClient.newBuilder() |     private val chapterClient: OkHttpClient = network.cloudflareClient.newBuilder() | ||||||
|         .rateLimit(1, 12) |         .rateLimit(1, 12, TimeUnit.SECONDS) | ||||||
|         .build() |         .build() | ||||||
| 
 | 
 | ||||||
|     override fun headersBuilder() = Headers.Builder().apply { |     override fun headersBuilder() = Headers.Builder().apply { | ||||||
| @ -55,6 +57,8 @@ abstract class MadTheme( | |||||||
| 
 | 
 | ||||||
|     private val json: Json by injectLazy() |     private val json: Json by injectLazy() | ||||||
| 
 | 
 | ||||||
|  |     private var genreKey = "genre[]" | ||||||
|  | 
 | ||||||
|     // Popular |     // Popular | ||||||
|     override fun popularMangaRequest(page: Int): Request = |     override fun popularMangaRequest(page: Int): Request = | ||||||
|         searchMangaRequest(page, "", FilterList(OrderFilter(0))) |         searchMangaRequest(page, "", FilterList(OrderFilter(0))) | ||||||
| @ -100,7 +104,7 @@ abstract class MadTheme( | |||||||
|                         .filter { it.state } |                         .filter { it.state } | ||||||
|                         .let { list -> |                         .let { list -> | ||||||
|                             if (list.isNotEmpty()) { |                             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 searchMangaSelector(): String = ".book-detailed-item" | ||||||
| 
 | 
 | ||||||
|     override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply { |     override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply { | ||||||
|         setUrlWithoutDomain(element.select("a").first()!!.attr("abs:href")) |         setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href")) | ||||||
|         title = element.select("a").first()!!.attr("title") |         title = element.selectFirst("a")!!.attr("title") | ||||||
|         description = element.select(".summary").first()?.text() |         element.selectFirst(".summary")?.text()?.let { description = it } | ||||||
|         genre = element.select(".genres > *").joinToString { it.text() } |         element.select(".genres > *").joinToString { it.text() }.takeIf { it.isNotEmpty() }?.let { genre = it } | ||||||
|         thumbnail_url = element.select("img").first()!!.attr("abs:data-src") |         thumbnail_url = element.selectFirst("img")!!.attr("abs:data-src") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /* |     /* | ||||||
| @ -135,23 +139,25 @@ abstract class MadTheme( | |||||||
| 
 | 
 | ||||||
|     // Details |     // Details | ||||||
|     override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { |     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(',', ' ') } |         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(',', ' ') } |         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(',', ';') |             ?.split(',', ';') | ||||||
|             ?.mapNotNull { it.trim().takeIf { it != title } } |             ?.mapNotNull { it.trim().takeIf { it != title } } | ||||||
|             ?: listOf() |             ?: 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()}" } ?: "") |             (altNames.takeIf { it.isNotEmpty() }?.let { "\n\nAlt name(s): ${it.joinToString()}" } ?: "") | ||||||
| 
 | 
 | ||||||
|         val statusText = document.select(".detail .meta > p > strong:contains(Status) ~ a").first()!!.text() |         val statusText = document.selectFirst(".detail .meta > p > strong:contains(Status) ~ a")!!.text() | ||||||
|         status = when (statusText.lowercase(Locale.US)) { |         status = when (statusText.lowercase(Locale.ENGLISH)) { | ||||||
|             "ongoing" -> SManga.ONGOING |             "ongoing" -> SManga.ONGOING | ||||||
|             "completed" -> SManga.COMPLETED |             "completed" -> SManga.COMPLETED | ||||||
|  |             "on-hold" -> SManga.ON_HIATUS | ||||||
|  |             "canceled" -> SManga.CANCELLED | ||||||
|             else -> SManga.UNKNOWN |             else -> SManga.UNKNOWN | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -187,7 +193,14 @@ abstract class MadTheme( | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun chapterListRequest(manga: SManga): Request = |     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 { |     override fun searchMangaParse(response: Response): MangasPage { | ||||||
|         if (genresList == null) { |         if (genresList == null) { | ||||||
| @ -204,16 +217,25 @@ abstract class MadTheme( | |||||||
|             .absUrl("href") |             .absUrl("href") | ||||||
|             .removePrefix(baseUrl) |             .removePrefix(baseUrl) | ||||||
| 
 | 
 | ||||||
|         name = element.select(".chapter-title").first()!!.text() |         name = element.selectFirst(".chapter-title")!!.text() | ||||||
|         date_upload = parseChapterDate(element.select(".chapter-update").first()?.text()) |         date_upload = parseChapterDate(element.selectFirst(".chapter-update")?.text()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Pages |     // Pages | ||||||
|     override fun pageListParse(document: Document): List<Page> { |     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 = \"")) { |         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 |             // 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 |             // the rest to be lazily(?) loaded by javascript. Let's extract `chapImages` and compare | ||||||
| @ -292,7 +314,7 @@ abstract class MadTheme( | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return when { |         return when { | ||||||
|             "ago".endsWith(date) -> { |             " ago" in date -> { | ||||||
|                 parseRelativeDate(date) |                 parseRelativeDate(date) | ||||||
|             } |             } | ||||||
|             else -> dateFormat.tryParse(date) |             else -> dateFormat.tryParse(date) | ||||||
| @ -300,10 +322,12 @@ abstract class MadTheme( | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private fun parseRelativeDate(date: String): Long { |     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() |         val cal = Calendar.getInstance() | ||||||
| 
 | 
 | ||||||
|         return when { |         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("day") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis | ||||||
|             date.contains("hour") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis |             date.contains("hour") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis | ||||||
|             date.contains("minute") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis |             date.contains("minute") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis | ||||||
| @ -314,13 +338,21 @@ abstract class MadTheme( | |||||||
| 
 | 
 | ||||||
|     // Dynamic genres |     // Dynamic genres | ||||||
|     private fun parseGenres(document: Document): List<Genre>? { |     private fun parseGenres(document: Document): List<Genre>? { | ||||||
|         return document.select(".checkbox-group.genres").first()?.select("label")?.map { |         return document.selectFirst(".checkbox-group.genres")?.select(".checkbox-wrapper")?.run { | ||||||
|             Genre(it.select(".radio__label").first()!!.text(), it.select("input").`val`()) |             firstOrNull()?.selectFirst("input")?.attr("name")?.takeIf { it.isNotEmpty() }?.let { genreKey = it } | ||||||
|  |             map { | ||||||
|  |                 Genre(it.selectFirst(".radio__label")!!.text(), it.selectFirst("input")!!.`val`()) | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Filters |     // Filters | ||||||
|     override fun getFilterList() = FilterList( |     override fun getFilterList() = FilterList( | ||||||
|  |         // TODO: Filters for sites that support it: | ||||||
|  |         // excluded genres | ||||||
|  |         // genre inclusion mode | ||||||
|  |         // bookmarks | ||||||
|  |         // author | ||||||
|         GenreFilter(getGenreList()), |         GenreFilter(getGenreList()), | ||||||
|         StatusFilter(), |         StatusFilter(), | ||||||
|         OrderFilter(), |         OrderFilter(), | ||||||
| @ -352,6 +384,7 @@ abstract class MadTheme( | |||||||
|             Pair("Updated", "updated_at"), |             Pair("Updated", "updated_at"), | ||||||
|             Pair("Created", "created_at"), |             Pair("Created", "created_at"), | ||||||
|             Pair("Name A-Z", "name"), |             Pair("Name A-Z", "name"), | ||||||
|  |             // Pair("Number of Chapters", "total_chapters"), | ||||||
|             Pair("Rating", "rating"), |             Pair("Rating", "rating"), | ||||||
|         ), |         ), | ||||||
|         state, |         state, | ||||||
| @ -365,4 +398,10 @@ abstract class MadTheme( | |||||||
|         Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) { |         Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) { | ||||||
|         fun toUriPart() = vals[state].second |         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() | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								src/en/kaliscancom/build.gradle
									
									
									
									
									
										Normal 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" | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/en/kaliscancom/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 7.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/kaliscancom/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/kaliscancom/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/kaliscancom/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 24 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/kaliscancom/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 41 KiB | 
| @ -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") | ||||||
							
								
								
									
										10
									
								
								src/en/kaliscanio/build.gradle
									
									
									
									
									
										Normal 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" | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/en/kaliscanio/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 7.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/kaliscanio/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/kaliscanio/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/kaliscanio/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 24 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/kaliscanio/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 41 KiB | 
| @ -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") | ||||||
							
								
								
									
										10
									
								
								src/en/kaliscanme/build.gradle
									
									
									
									
									
										Normal 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" | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/en/kaliscanme/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 7.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/kaliscanme/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/kaliscanme/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/kaliscanme/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 24 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/kaliscanme/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 41 KiB | 
| @ -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") | ||||||
| @ -1,8 +0,0 @@ | |||||||
| ext { |  | ||||||
|     extName = 'ManhuaScan' |  | ||||||
|     extClass = '.ManhuaScan' |  | ||||||
|     extVersionCode = 8 |  | ||||||
|     isNsfw = true |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| apply from: "$rootDir/common.gradle" |  | ||||||
| Before Width: | Height: | Size: 4.2 KiB | 
| Before Width: | Height: | Size: 2.5 KiB | 
| Before Width: | Height: | Size: 5.9 KiB | 
| Before Width: | Height: | Size: 9.7 KiB | 
| Before Width: | Height: | Size: 15 KiB | 
| @ -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)!! |  | ||||||
| } |  | ||||||
| @ -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") |  | ||||||
							
								
								
									
										10
									
								
								src/en/mgjinx/build.gradle
									
									
									
									
									
										Normal 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" | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/en/mgjinx/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 7.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/mgjinx/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/mgjinx/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/mgjinx/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 24 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/en/mgjinx/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 41 KiB | 
| @ -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") | ||||||
 Vetle Ledaal
						Vetle Ledaal