From 6defaacb09e0b8701dd1109de7330e6ef21c4653 Mon Sep 17 00:00:00 2001
From: FourTOne5 <59261191+FourTOne5@users.noreply.github.com>
Date: Wed, 13 Apr 2022 00:37:56 +0600
Subject: [PATCH] Zero Scans migrate to new site. (#11358)
* Initial Commit
* Update
* Add space in extension name.
* Review updates
---
.../multisrc/genkan/GenkanGenerator.kt | 1 -
src/en/zeroscans/AndroidManifest.xml | 2 +
src/en/zeroscans/CHANGELOG.md | 3 +
src/en/zeroscans/build.gradle | 12 +
.../zeroscans/res/mipmap-hdpi/ic_launcher.png | Bin
.../zeroscans/res/mipmap-mdpi/ic_launcher.png | Bin
.../res/mipmap-xhdpi/ic_launcher.png | Bin
.../res/mipmap-xxhdpi/ic_launcher.png | Bin
.../res/mipmap-xxxhdpi/ic_launcher.png | Bin
.../en}/zeroscans/res/web_hi_res_512.png | Bin
.../extension/en/zeroscans/ZeroScans.kt | 349 ++++++++++++++++++
.../extension/en/zeroscans/ZeroScansDto.kt | 116 ++++++
.../extension/en/zeroscans/ZeroScansHelper.kt | 122 ++++++
13 files changed, 604 insertions(+), 1 deletion(-)
create mode 100644 src/en/zeroscans/AndroidManifest.xml
create mode 100644 src/en/zeroscans/CHANGELOG.md
create mode 100644 src/en/zeroscans/build.gradle
rename {multisrc/overrides/genkan => src/en}/zeroscans/res/mipmap-hdpi/ic_launcher.png (100%)
rename {multisrc/overrides/genkan => src/en}/zeroscans/res/mipmap-mdpi/ic_launcher.png (100%)
rename {multisrc/overrides/genkan => src/en}/zeroscans/res/mipmap-xhdpi/ic_launcher.png (100%)
rename {multisrc/overrides/genkan => src/en}/zeroscans/res/mipmap-xxhdpi/ic_launcher.png (100%)
rename {multisrc/overrides/genkan => src/en}/zeroscans/res/mipmap-xxxhdpi/ic_launcher.png (100%)
rename {multisrc/overrides/genkan => src/en}/zeroscans/res/web_hi_res_512.png (100%)
create mode 100644 src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScans.kt
create mode 100644 src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansDto.kt
create mode 100644 src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansHelper.kt
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/genkan/GenkanGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/genkan/GenkanGenerator.kt
index fe3cc4382..668dc728d 100644
--- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/genkan/GenkanGenerator.kt
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/genkan/GenkanGenerator.kt
@@ -13,7 +13,6 @@ class GenkanGenerator : ThemeSourceGenerator {
override val sources = listOf(
SingleLang("Hunlight Scans", "https://hunlight-scans.info", "en"),
- SingleLang("ZeroScans", "https://zeroscans.com", "en"),
SingleLang("The Nonames Scans", "https://the-nonames.com", "en"),
SingleLang("Edelgarde Scans", "https://edelgardescans.com", "en"),
SingleLang("LynxScans", "https://lynxscans.com", "en", overrideVersionCode = 3),
diff --git a/src/en/zeroscans/AndroidManifest.xml b/src/en/zeroscans/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/en/zeroscans/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/en/zeroscans/CHANGELOG.md b/src/en/zeroscans/CHANGELOG.md
new file mode 100644
index 000000000..354ab5394
--- /dev/null
+++ b/src/en/zeroscans/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 1.2.4
+
+Migrated to the new site.
\ No newline at end of file
diff --git a/src/en/zeroscans/build.gradle b/src/en/zeroscans/build.gradle
new file mode 100644
index 000000000..91739ec68
--- /dev/null
+++ b/src/en/zeroscans/build.gradle
@@ -0,0 +1,12 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'Zero Scans'
+ pkgNameSuffix = 'en.zeroscans'
+ extClass = '.ZeroScans'
+ extVersionCode = 4
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/multisrc/overrides/genkan/zeroscans/res/mipmap-hdpi/ic_launcher.png b/src/en/zeroscans/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/genkan/zeroscans/res/mipmap-hdpi/ic_launcher.png
rename to src/en/zeroscans/res/mipmap-hdpi/ic_launcher.png
diff --git a/multisrc/overrides/genkan/zeroscans/res/mipmap-mdpi/ic_launcher.png b/src/en/zeroscans/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/genkan/zeroscans/res/mipmap-mdpi/ic_launcher.png
rename to src/en/zeroscans/res/mipmap-mdpi/ic_launcher.png
diff --git a/multisrc/overrides/genkan/zeroscans/res/mipmap-xhdpi/ic_launcher.png b/src/en/zeroscans/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/genkan/zeroscans/res/mipmap-xhdpi/ic_launcher.png
rename to src/en/zeroscans/res/mipmap-xhdpi/ic_launcher.png
diff --git a/multisrc/overrides/genkan/zeroscans/res/mipmap-xxhdpi/ic_launcher.png b/src/en/zeroscans/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/genkan/zeroscans/res/mipmap-xxhdpi/ic_launcher.png
rename to src/en/zeroscans/res/mipmap-xxhdpi/ic_launcher.png
diff --git a/multisrc/overrides/genkan/zeroscans/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/zeroscans/res/mipmap-xxxhdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/genkan/zeroscans/res/mipmap-xxxhdpi/ic_launcher.png
rename to src/en/zeroscans/res/mipmap-xxxhdpi/ic_launcher.png
diff --git a/multisrc/overrides/genkan/zeroscans/res/web_hi_res_512.png b/src/en/zeroscans/res/web_hi_res_512.png
similarity index 100%
rename from multisrc/overrides/genkan/zeroscans/res/web_hi_res_512.png
rename to src/en/zeroscans/res/web_hi_res_512.png
diff --git a/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScans.kt b/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScans.kt
new file mode 100644
index 000000000..e35807bbe
--- /dev/null
+++ b/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScans.kt
@@ -0,0 +1,349 @@
+package eu.kanade.tachiyomi.extension.en.zeroscans
+
+import android.util.Log
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.Filter
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.HttpSource
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import rx.Observable
+import rx.Single
+import rx.schedulers.Schedulers
+import uy.kohesive.injekt.injectLazy
+
+class ZeroScans : HttpSource() {
+
+ override val name: String = "Zero Scans"
+
+ override val lang: String = "en"
+
+ override val baseUrl: String = "https://beta.zeroscans.com"
+
+ override val supportsLatest: Boolean = true
+
+ private val json: Json by injectLazy()
+
+ private lateinit var comicList: List
+
+ private lateinit var rankings: ZeroScansRankingsDto
+
+ private val zsHelper = ZeroScansHelper()
+
+ override fun fetchLatestUpdates(page: Int): Observable {
+ if (page == 1) runCatching { updateComicsData() }
+ return super.fetchLatestUpdates(page)
+ }
+
+ override fun latestUpdatesRequest(page: Int): Request {
+ return GET("$baseUrl/$API_PATH/new-chapters")
+ }
+
+ override fun latestUpdatesParse(response: Response): MangasPage {
+ val newChapters = response.parseAs()
+
+ val titlesList = newChapters.all.mapNotNull {
+ comicList.firstOrNull { comic -> comic.slug == it.slug }
+ }.map { comic -> zsHelper.zsComicEntryToSManga(comic) }
+
+ return MangasPage(titlesList, false)
+ }
+
+ override fun fetchPopularManga(page: Int): Observable {
+ return fetchSearchManga(page, query = "", filters = getFilterList())
+ }
+
+ override fun fetchSearchManga(
+ page: Int,
+ query: String,
+ filters: FilterList
+ ): Observable {
+ if (page == 1) runCatching { updateComicsData() }
+
+ filters.filterIsInstance().firstOrNull()?.let { rankingFilter ->
+ val type = rankingList[rankingFilter.state!!.index].type
+ val ascending = rankingFilter.state!!.ascending
+ getRankingsIfNeeded(type, ascending)?.let {
+ return Observable.just(MangasPage(it, false))
+ }
+ }
+
+ var filteredComics = comicList
+
+ if (query.isNotBlank()) {
+ filteredComics = filteredComics.filter { it.name.contains(query, ignoreCase = true) }
+ }
+
+ filters.forEach { filter ->
+ when (filter) {
+ is StatusFilter -> {
+ filteredComics = filteredComics.filter { zsHelper.checkStatusFilter(filter, it) }
+ }
+ is GenreFilter -> {
+ filteredComics = filteredComics.filter { zsHelper.checkGenreFilter(filter, it) }
+ }
+ is SortFilter -> {
+ filter.state?.let {
+ val type = sortList[it.index].type
+ val ascending = it.ascending
+ filteredComics = zsHelper.applySortFilter(type, ascending, filteredComics)
+ }
+ }
+ else -> { /* Do Nothing */ }
+ }
+ }
+
+ // Get 20 comics at a time
+ val chunkedFilteredComics = filteredComics.chunked(20)
+ if (chunkedFilteredComics.isEmpty()) {
+ return Observable.just(MangasPage(emptyList(), false))
+ }
+
+ val comics = chunkedFilteredComics[page - 1].map { comic -> zsHelper.zsComicEntryToSManga(comic) }
+ val hasNextPage = page < chunkedFilteredComics.size
+
+ return Observable.just(MangasPage(comics, hasNextPage))
+ }
+
+ private fun getRankingsIfNeeded(type: String?, ascending: Boolean): List? {
+ if (type.isNullOrBlank()) return null
+
+ val rankingEntries = when (type) {
+ "weekly" -> {
+ if (!ascending) rankings.weekly.reversed()
+ else rankings.weekly
+ }
+ "monthly" -> {
+ if (!ascending) rankings.monthly.reversed()
+ else rankings.monthly
+ }
+ else -> {
+ if (!ascending) rankings.allTime.reversed()
+ else rankings.allTime
+ }
+ }
+
+ val titlesList = rankingEntries.mapNotNull { rankingEntry ->
+ comicList.firstOrNull { rankingEntry.slug == it.slug }
+ }.map { comic -> zsHelper.zsComicEntryToSManga(comic) }
+
+ return titlesList
+ }
+
+ override fun fetchMangaDetails(manga: SManga): Observable {
+ runCatching { updateComicsData() }
+ val mangaSlug = "$baseUrl${manga.url}".toHttpUrl().pathSegments[1]
+
+ try {
+ val comic = comicList.first { comic -> comic.slug == mangaSlug }
+ .let { zsHelper.zsComicEntryToSManga(it) }
+
+ return Observable.just(comic)
+ } catch (e: NoSuchElementException) {
+ throw Exception("Migrate from Zero Scans to Zero Scans")
+ }
+ }
+
+ override fun fetchChapterList(manga: SManga): Observable> {
+ try {
+ var page = 1
+
+ val response = client.newCall(zsChapterListRequest(page, manga)).execute()
+
+ val zsChapterPage = zsChapterListParse(response)
+
+ val zsChapters = zsChapterPage.chapters.toMutableList()
+
+ var hasMoreResult = zsChapterPage.hasNextPage
+
+ while (hasMoreResult) {
+ page++
+ val newResponse = client.newCall(zsChapterListRequest(page, manga)).execute()
+ val newZSChapterPage = zsChapterListParse(newResponse)
+ zsChapters.addAll(newZSChapterPage.chapters)
+ hasMoreResult = newZSChapterPage.hasNextPage
+ }
+
+ zsChapters.map { it.toSChapter(manga) }.let {
+ return Observable.just(it)
+ }
+ } catch (e: Exception) {
+ Log.e("Zero Scans", "Error parsing chapter list", e)
+ throw(e)
+ }
+ }
+
+ private fun zsChapterListRequest(page: Int, manga: SManga): Request {
+ val mangaId = "$baseUrl${manga.url}".toHttpUrl().queryParameter("id")
+ return GET("$baseUrl/$API_PATH/comic/$mangaId/chapters?sort=desc&page=$page")
+ }
+
+ private fun zsChapterListParse(response: Response): ZeroScansChapterPage {
+ return response.parseAs>()
+ .data.let {
+ ZeroScansChapterPage(
+ it.data,
+ it.currentPage < it.lastPage
+ )
+ }
+ }
+
+ class ZeroScansChapterPage(
+ val chapters: List,
+ val hasNextPage: Boolean
+ )
+
+ private fun ZeroScansChapterDto.toSChapter(manga: SManga): SChapter {
+ val comicSlug = "$baseUrl${manga.url}".toHttpUrl().pathSegments[1]
+ val zsChapter = this
+ return SChapter.create().apply {
+ name = "Chapter ${zsChapter.name}"
+ scanlator = zsChapter.group
+ chapter_number = zsChapter.name.toFloat()
+ date_upload = zsHelper.parseChapterUploadDate(zsChapter.createdAt)
+ url = "/comics/$comicSlug/${zsChapter.id}"
+ }
+ }
+
+ override fun pageListRequest(chapter: SChapter): Request {
+ val chapterUrlPaths = "$baseUrl${chapter.url}".toHttpUrl().pathSegments
+ val mangaSlug = chapterUrlPaths[1]
+ val chapterId = chapterUrlPaths[2]
+ return GET("$baseUrl/$API_PATH/comic/$mangaSlug/chapters/$chapterId")
+ }
+
+ override fun pageListParse(response: Response): List {
+ val allQualityZSPages = response.parseAs>().data.chapter
+
+ val highResZSPages = allQualityZSPages.highQuality.takeIf { it.isNotEmpty() } ?: allQualityZSPages.goodQuality
+ val pages = highResZSPages.mapIndexed { index, url ->
+ Page(index, imageUrl = url)
+ }
+
+ return pages
+ }
+
+ // Fetch Comics, Genres, Statuses and Rankings on creating source
+ init {
+ Single.fromCallable {
+ runCatching { updateComicsData() }
+ }.subscribeOn(Schedulers.io())
+ .observeOn(Schedulers.io())
+ .subscribe({}, {})
+ }
+
+ // Filters
+ override fun getFilterList(): FilterList {
+ val filters = mutableListOf>()
+ if (genreList.isNotEmpty()) {
+ filters.add(GenreFilter(genreList))
+ }
+ if (statusList.isNotEmpty()) {
+ filters.add(StatusFilter(statusList))
+ }
+ filters += listOf(
+ SortFilter(sortList),
+ RankingsHeader(),
+ RankingsHeader2(),
+ RankingsFilter(rankingList)
+ )
+
+ return FilterList(filters)
+ }
+
+ class GenreFilter(genres: List) : Filter.Group("Genre", genres)
+ class Genre(name: String, val id: Int) : Filter.TriState(name)
+
+ private var genreList: List = emptyList()
+
+ class StatusFilter(statuses: List) : Filter.Group("Status", statuses)
+ class Status(name: String, val id: Int) : Filter.TriState(name)
+
+ private var statusList: List = emptyList()
+
+ class SortFilter(sorts: List) :
+ Filter.Sort("Sort by", sorts.map { it.name }.toTypedArray(), Selection(3, false))
+
+ class Sort(val name: String, val type: String)
+
+ private val sortList = listOf(
+ Sort("Alphabetic", "alphabetic"),
+ Sort("Rating", "rating"),
+ Sort("Chapter Count", "chapter_count"),
+ Sort("Bookmark Count", "bookmark_count"),
+ Sort("View Count", "view_count")
+ )
+
+ class RankingsHeader :
+ Filter.Header("Note: Genre, Sort, Status filter and Search query")
+ class RankingsHeader2 :
+ Filter.Header("are not applied to rankings")
+
+ class RankingsFilter(rankings: List) :
+ Filter.Sort("Rankings", rankings.map { it.name }.toTypedArray(), Selection(0, false))
+
+ class Ranking(val name: String, val type: String? = null)
+
+ private val rankingList = listOf(
+ Ranking("None"),
+ Ranking("All Time", "all-time"),
+ Ranking("Weekly", "weekly"),
+ Ranking("Monthly", "monthly")
+ )
+
+ // Helpers
+ private inline fun Response.parseAs(): T = use {
+ json.decodeFromString(it.body?.string().orEmpty())
+ }
+
+ private fun comicsDataRequest(): Request {
+ return GET("$baseUrl/$API_PATH/comics")
+ }
+
+ private fun comicsDataParse(response: Response): ZeroScansComicsDataDto {
+ return response.parseAs>().data
+ }
+
+ private fun updateComicsData() {
+ val response = client.newCall(comicsDataRequest()).execute()
+ comicsDataParse(response).let {
+ genreList = it.genres.map { genreDto ->
+ Genre(genreDto.name, genreDto.id)
+ }
+ statusList = it.statuses.map { statusDto ->
+ Status(statusDto.name, statusDto.id)
+ }
+ comicList = it.comics
+ rankings = it.rankings
+ }
+ }
+
+ // Unused Stuff
+ override fun imageUrlParse(response: Response): String = ""
+
+ override fun popularMangaRequest(page: Int): Request = throw UnsupportedOperationException("Not Used")
+
+ override fun popularMangaParse(response: Response): MangasPage = throw UnsupportedOperationException("Not Used")
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
+ throw UnsupportedOperationException("Not Used")
+
+ override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException("Not Used")
+
+ override fun chapterListRequest(manga: SManga): Request = throw UnsupportedOperationException("Not Used")
+
+ override fun chapterListParse(response: Response): List = throw UnsupportedOperationException("Not Used")
+
+ override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException("Not Used")
+
+ companion object {
+ const val API_PATH = "swordflake"
+ }
+}
diff --git a/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansDto.kt b/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansDto.kt
new file mode 100644
index 000000000..e2914f1e5
--- /dev/null
+++ b/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansDto.kt
@@ -0,0 +1,116 @@
+package eu.kanade.tachiyomi.extension.en.zeroscans
+
+import eu.kanade.tachiyomi.source.model.Page
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonElement
+
+@Serializable
+data class NewChaptersResponseDto(
+ val all: List
+)
+
+@Serializable
+data class NewChaptersMangaDto(
+ val slug: String
+)
+
+@Serializable
+data class ZeroScansResponseDto(
+ val success: Boolean? = null,
+ val data: T,
+ val message: String? = null
+)
+
+@Serializable
+data class ZeroScansComicsDataDto(
+ val comics: List,
+ val genres: List,
+ val statuses: List,
+ val rankings: ZeroScansRankingsDto
+)
+
+@Serializable
+data class ZeroScansComicDto(
+ val name: String,
+ val slug: String,
+ val id: Int,
+ val cover: ZeroScansCoverDto,
+ val summary: String,
+ val statuses: List,
+ val genres: List,
+ @SerialName("chapter_count") val chapterCount: Int,
+ @SerialName("bookmark_count") val bookmarkCount: Int,
+ @SerialName("view_count") val viewCount: Int,
+ val rating: JsonElement
+) {
+ fun getRating(): Float {
+ return this.rating.toString().toFloatOrNull() ?: 0F
+ }
+}
+
+@Serializable
+data class ZeroScansGenreDto(
+ val name: String,
+ val id: Int
+)
+
+@Serializable
+data class ZeroScansStatusDto(
+ val name: String,
+ val id: Int
+)
+
+@Serializable
+data class ZeroScansRankingsDto(
+ @SerialName("all_time") val allTime: List,
+ @SerialName("weekly_comics") val weekly: List,
+ @SerialName("monthly_comics") val monthly: List
+)
+
+@Serializable
+data class ZeroScansRankingsEntryDto(
+ val slug: String
+)
+
+@Serializable
+data class ZeroScansCoverDto(
+ val horizontal: String? = null,
+ val vertical: String? = null,
+ val full: String? = null
+) {
+ fun getHighResCover(): String {
+ return when {
+ !this.full.isNullOrBlank() -> this.full
+ !this.horizontal.isNullOrBlank() -> this.horizontal.replace("-horizontal", "-full")
+ !this.vertical.isNullOrBlank() -> this.vertical.replace("-vertical", "-full")
+ else -> ""
+ }
+ }
+}
+
+@Serializable
+data class ZeroScansChaptersResponseDto(
+ val data: List = emptyList(),
+ @SerialName("current_page") val currentPage: Int,
+ @SerialName("last_page") val lastPage: Int
+)
+
+@Serializable
+data class ZeroScansChapterDto(
+ val id: Int,
+ val name: Int,
+ val group: String?,
+ @SerialName("created_at") val createdAt: String
+)
+
+@Serializable
+data class ZeroScansPageResponseDto(
+ val chapter: ZeroScansChapterPagesDto
+)
+
+@Serializable
+data class ZeroScansChapterPagesDto(
+ @SerialName("high_quality") val highQuality: List = emptyList(),
+ @SerialName("good_quality") val goodQuality: List = emptyList()
+)
diff --git a/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansHelper.kt b/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansHelper.kt
new file mode 100644
index 000000000..81e0f32c4
--- /dev/null
+++ b/src/en/zeroscans/src/eu/kanade/tachiyomi/extension/en/zeroscans/ZeroScansHelper.kt
@@ -0,0 +1,122 @@
+package eu.kanade.tachiyomi.extension.en.zeroscans
+
+import eu.kanade.tachiyomi.source.model.SManga
+import java.util.Calendar
+import java.util.Locale
+
+class ZeroScansHelper {
+
+ // Search Related
+ fun checkStatusFilter(
+ filter: ZeroScans.StatusFilter,
+ comic: ZeroScansComicDto
+ ): Boolean {
+ val includedStatusIds = filter.state.filter { it.isIncluded() }.map { it.id }
+ val excludedStatusIds = filter.state.filter { it.isExcluded() }.map { it.id }
+
+ val comicStatusesId = comic.statuses.map { it.id }
+
+ if (includedStatusIds.isEmpty() && excludedStatusIds.isEmpty()) return true
+
+ return includedStatusIds.any { it in comicStatusesId } && excludedStatusIds.any { it !in comicStatusesId }
+ }
+
+ fun checkGenreFilter(
+ filter: ZeroScans.GenreFilter,
+ comic: ZeroScansComicDto
+ ): Boolean {
+ val includedGenreIds = filter.state.filter { it.isIncluded() }.map { it.id }
+ val excludedGenreIds = filter.state.filter { it.isExcluded() }.map { it.id }
+
+ val comicStatusesId = comic.genres.map { it.id }
+
+ if (includedGenreIds.isEmpty() && excludedGenreIds.isEmpty()) return true
+
+ return includedGenreIds.any { it in comicStatusesId } && excludedGenreIds.any { it !in comicStatusesId }
+ }
+
+ fun applySortFilter(
+ type: String,
+ ascending: Boolean,
+ comics: List
+ ): List {
+ var sortedList = when (type) {
+ "alphabetic" -> comics.sortedBy { it.name.toLowerCase(Locale.ROOT) }
+ "rating" -> comics.sortedBy { it.getRating() }
+ "chapter_count" -> comics.sortedBy { it.chapterCount }
+ "bookmark_count" -> comics.sortedBy { it.bookmarkCount }
+ "view_count" -> comics.sortedBy { it.viewCount }
+ else -> comics
+ }
+
+ if (!ascending) {
+ sortedList = sortedList.reversed()
+ }
+
+ return sortedList
+ }
+
+ // Chapter Related
+ fun parseChapterUploadDate(date: String): Long {
+ val value = date.split(' ')[0].toInt()
+
+ return when (date.split(' ')[1].removeSuffix("s")) {
+ "sec" -> Calendar.getInstance().apply {
+ add(Calendar.SECOND, value * -1)
+ }.timeInMillis
+ "min" -> Calendar.getInstance().apply {
+ add(Calendar.MINUTE, value * -1)
+ }.timeInMillis
+ "hour" -> Calendar.getInstance().apply {
+ add(Calendar.HOUR_OF_DAY, value * -1)
+ }.timeInMillis
+ "day" -> Calendar.getInstance().apply {
+ add(Calendar.DATE, value * -1)
+ }.timeInMillis
+ "week" -> Calendar.getInstance().apply {
+ add(Calendar.DATE, value * 7 * -1)
+ }.timeInMillis
+ "month" -> Calendar.getInstance().apply {
+ add(Calendar.MONTH, value * -1)
+ }.timeInMillis
+ "year" -> Calendar.getInstance().apply {
+ add(Calendar.YEAR, value * -1)
+ }.timeInMillis
+ else -> {
+ return 0
+ }
+ }
+ }
+
+ // Manga Related
+ fun zsComicEntryToSManga(comic: ZeroScansComicDto): SManga {
+ var comicDescription = comic.summary
+ if (comic.statuses.any { it.id == 4 }) {
+ comicDescription = "The series has been dropped.\n\n$comicDescription"
+ }
+ return SManga.create().apply {
+ title = comic.name
+ url = "/comics/${comic.slug}?id=${comic.id}"
+ thumbnail_url = comic.cover.getHighResCover()
+ description = comicDescription
+ genre = comic.genres.joinToString { it.name }
+ status = comic.getTachiyomiStatus()
+ initialized = true
+ }
+ }
+
+ private fun ZeroScansComicDto.getTachiyomiStatus(): Int {
+ // 1 = New & 4 = Dropped
+ val compatibleStatus = statuses.filterNot { it.id in listOf(1, 4) }
+
+ // TODO Apply 6 to ON_HIATUS after ext-lib 1.3 merge
+ compatibleStatus.firstOrNull { it.id in listOf(5, 6) }
+ ?.also { return SManga.ONGOING }
+
+ compatibleStatus.firstOrNull { it.id == 3 }
+ ?.also { return SManga.COMPLETED }
+
+ // Nothing Matched
+ return SManga.UNKNOWN
+ }
+}