feat(comix): Add rating score, NSFW toggle, and filter fix (#11682)

* bump

* refactor: fix **type** filter param to match api

* add configurable rating score display

* rearrange filter

* add preference to hide NSFW content
This commit is contained in:
mrtear 2025-11-17 05:13:37 -07:00 committed by Draff
parent f59060dc92
commit 6eae846dc8
Signed by: Draff
GPG Key ID: E8A89F3211677653
4 changed files with 114 additions and 28 deletions

View File

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

View File

@ -39,12 +39,18 @@ class Comix : HttpSource(), ConfigurableSource {
/******************************* POPULAR MANGA ************************************/ /******************************* POPULAR MANGA ************************************/
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
val url = apiUrl.toHttpUrl().newBuilder() val url = apiUrl.toHttpUrl().newBuilder().apply {
.addPathSegment("manga") addPathSegment("manga")
.addQueryParameter("order[views_30d]", "desc") addQueryParameter("order[views_30d]", "desc")
.addQueryParameter("limit", "50") addQueryParameter("limit", "50")
.addQueryParameter("page", page.toString()) addQueryParameter("page", page.toString())
.build()
if (preferences.hideNsfw()) {
NSFW_GENRE_IDS.forEach {
addQueryParameter("genres[]", "-$it")
}
}
}.build()
return GET(url, headers) return GET(url, headers)
} }
@ -54,12 +60,18 @@ class Comix : HttpSource(), ConfigurableSource {
/******************************* LATEST MANGA ************************************/ /******************************* LATEST MANGA ************************************/
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
val url = apiUrl.toHttpUrl().newBuilder() val url = apiUrl.toHttpUrl().newBuilder().apply {
.addPathSegment("manga") addPathSegment("manga")
.addQueryParameter("order[chapter_updated_at]", "desc") addQueryParameter("order[chapter_updated_at]", "desc")
.addQueryParameter("limit", "50") addQueryParameter("limit", "50")
.addQueryParameter("page", page.toString()) addQueryParameter("page", page.toString())
.build()
if (preferences.hideNsfw()) {
NSFW_GENRE_IDS.forEach {
addQueryParameter("genres[]", "-$it")
}
}
}.build()
return GET(url, headers) return GET(url, headers)
} }
@ -71,23 +83,30 @@ class Comix : HttpSource(), ConfigurableSource {
override fun getFilterList() = ComixFilters().getFilterList() override fun getFilterList() = ComixFilters().getFilterList()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = apiUrl.toHttpUrl().newBuilder() val url = apiUrl.toHttpUrl().newBuilder().apply {
.addPathSegment("manga") addPathSegment("manga")
filters.filterIsInstance<ComixFilters.UriFilter>() filters.filterIsInstance<ComixFilters.UriFilter>()
.forEach { it.addToUri(url) } .forEach { it.addToUri(this) }
// Make searches accurate // Make searches accurate
if (query.isNotBlank()) { if (query.isNotBlank()) {
url.addQueryParameter("keyword", query) addQueryParameter("keyword", query)
url.removeAllQueryParameters("order[views_30d]") removeAllQueryParameters("order[views_30d]")
url.setQueryParameter("order[relevance]", "desc") setQueryParameter("order[relevance]", "desc")
} }
url.addQueryParameter("limit", "50") if (preferences.hideNsfw()) {
.addQueryParameter("page", page.toString()) NSFW_GENRE_IDS.forEach {
addQueryParameter("genres[]", "-$it")
}
}
return GET(url.build(), headers) addQueryParameter("limit", "50")
addQueryParameter("page", page.toString())
}.build()
return GET(url, headers)
} }
override fun searchMangaParse(response: Response): MangasPage { override fun searchMangaParse(response: Response): MangasPage {
@ -121,6 +140,7 @@ class Comix : HttpSource(), ConfigurableSource {
return mangaResponse.result.toSManga( return mangaResponse.result.toSManga(
preferences.posterQuality(), preferences.posterQuality(),
preferences.alternativeNamesInDescription(), preferences.alternativeNamesInDescription(),
preferences.scorePosition(),
) )
} }
@ -253,6 +273,13 @@ class Comix : HttpSource(), ConfigurableSource {
setDefaultValue("large") setDefaultValue("large")
}.let(screen::addPreference) }.let(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = NSFW_PREF
title = "Hide NSFW content"
summary = "Hides NSFW content from popular, latest, and search lists."
setDefaultValue(false)
}.let(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply { SwitchPreferenceCompat(screen.context).apply {
key = DEDUPLICATE_CHAPTERS key = DEDUPLICATE_CHAPTERS
title = "Deduplicate Chapters" title = "Deduplicate Chapters"
@ -269,6 +296,15 @@ class Comix : HttpSource(), ConfigurableSource {
setDefaultValue(false) setDefaultValue(false)
}.let(screen::addPreference) }.let(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_SCORE_POSITION
title = "Score display position"
summary = "%s"
entries = arrayOf("Top of description", "Bottom of description", "Don't show")
entryValues = arrayOf("top", "bottom", "none")
setDefaultValue("top")
}.let(screen::addPreference)
} }
private fun SharedPreferences.posterQuality() = private fun SharedPreferences.posterQuality() =
@ -280,9 +316,19 @@ class Comix : HttpSource(), ConfigurableSource {
private fun SharedPreferences.alternativeNamesInDescription() = private fun SharedPreferences.alternativeNamesInDescription() =
getBoolean(ALTERNATIVE_NAMES_IN_DESCRIPTION, false) getBoolean(ALTERNATIVE_NAMES_IN_DESCRIPTION, false)
private fun SharedPreferences.scorePosition() =
getString(PREF_SCORE_POSITION, "top") ?: "top"
private fun SharedPreferences.hideNsfw() =
getBoolean(NSFW_PREF, false)
companion object { companion object {
private const val PREF_POSTER_QUALITY = "pref_poster_quality" private const val PREF_POSTER_QUALITY = "pref_poster_quality"
private const val NSFW_PREF = "nsfw_pref"
private const val DEDUPLICATE_CHAPTERS = "pref_deduplicate_chapters" private const val DEDUPLICATE_CHAPTERS = "pref_deduplicate_chapters"
private const val ALTERNATIVE_NAMES_IN_DESCRIPTION = "pref_alt_names_in_description" private const val ALTERNATIVE_NAMES_IN_DESCRIPTION = "pref_alt_names_in_description"
private const val PREF_SCORE_POSITION = "pref_score_position"
private val NSFW_GENRE_IDS = listOf("87264", "8", "87265", "13", "87266", "87268")
} }
} }

View File

@ -4,6 +4,8 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.math.BigDecimal
import java.math.RoundingMode
@Serializable @Serializable
data class Term( data class Term(
@ -33,6 +35,8 @@ class Manga(
private val genre: List<Term>?, private val genre: List<Term>?,
private val theme: List<Term>?, private val theme: List<Term>?,
private val demographic: List<Term>?, private val demographic: List<Term>?,
@SerialName("rated_avg")
private val ratedAvg: Double = 0.0,
) { ) {
@Serializable @Serializable
class Poster( class Poster(
@ -47,15 +51,44 @@ class Manga(
} }
} }
private val fancyScore: String
get() {
if (ratedAvg == 0.0) return ""
val score = ratedAvg.toBigDecimal()
val stars = score.div(BigDecimal(2))
.setScale(0, RoundingMode.HALF_UP).toInt()
val scoreString = if (score.scale() == 0) {
score.toPlainString()
} else {
score.stripTrailingZeros().toPlainString()
}
return buildString {
append("".repeat(stars))
if (stars < 5) append("".repeat(5 - stars))
append(" $scoreString")
}
}
fun toSManga( fun toSManga(
posterQuality: String?, posterQuality: String?,
altTitlesInDesc: Boolean = false, altTitlesInDesc: Boolean = false,
scorePosition: String,
) = SManga.create().apply { ) = SManga.create().apply {
url = "/$hashId" url = "/$hashId"
title = this@Manga.title title = this@Manga.title
author = this@Manga.author.takeUnless { it.isNullOrEmpty() }?.joinToString { it.title } author = this@Manga.author.takeUnless { it.isNullOrEmpty() }?.joinToString { it.title }
artist = this@Manga.artist.takeUnless { it.isNullOrEmpty() }?.joinToString { it.title } artist = this@Manga.artist.takeUnless { it.isNullOrEmpty() }?.joinToString { it.title }
description = buildString { description = buildString {
if (scorePosition == "top") {
fancyScore.takeIf { it.isNotEmpty() }?.let {
append(it)
append("\n\n")
}
}
synopsis.takeUnless { it.isNullOrEmpty() } synopsis.takeUnless { it.isNullOrEmpty() }
?.let { append(it) } ?.let { append(it) }
altTitles.takeIf { altTitlesInDesc && it.isNotEmpty() } altTitles.takeIf { altTitlesInDesc && it.isNotEmpty() }
@ -64,6 +97,13 @@ class Manga(
append("Alternative Names:\n") append("Alternative Names:\n")
append(altName.joinToString("\n")) append(altName.joinToString("\n"))
} }
if (scorePosition == "bottom") {
fancyScore.takeIf { it.isNotEmpty() }?.let {
if (isNotEmpty()) append("\n\n")
append(it)
}
}
} }
initialized = true initialized = true
status = when (this@Manga.status) { status = when (this@Manga.status) {

View File

@ -105,10 +105,10 @@ class ComixFilters {
fun getFilterList() = FilterList( fun getFilterList() = FilterList(
SortFilter(getSortables()), SortFilter(getSortables()),
StatusFilter(), StatusFilter(),
MinChapterFilter(),
GenreFilter(getGenres()), GenreFilter(getGenres()),
TypeFilter(), TypeFilter(),
DemographicFilter(getDemographics()), DemographicFilter(getDemographics()),
MinChapterFilter(),
Filter.Separator(), Filter.Separator(),
Filter.Header("Release Year"), Filter.Header("Release Year"),
YearFromFilter(), YearFromFilter(),
@ -180,7 +180,7 @@ class ComixFilters {
private class TypeFilter : UriMultiSelectFilter( private class TypeFilter : UriMultiSelectFilter(
"Type", "Type",
"type", "types[]",
arrayOf( arrayOf(
Pair("Manga", "manga"), Pair("Manga", "manga"),
Pair("Manhwa", "manhwa"), Pair("Manhwa", "manhwa"),