Kagane: Add excluded genres & Fix chapter number (#11537)

* Don't use DTO chapter number as it is , not actual chapter number

* Add Exclude Genres preference

* pump version

* Move 'Show scanlations' to preferences

* Add source to tags so it can be searched with filter by clicking on it

* Don't sort by relevant if filtering without query string

* Some sources prefer 'number_sort'

* catching error

* optimize
This commit is contained in:
Cuong-Tran 2025-11-13 02:47:20 +07:00 committed by Draff
parent 9194e31208
commit b8154e7698
Signed by: Draff
GPG Key ID: E8A89F3211677653
4 changed files with 145 additions and 19 deletions

View File

@ -1,7 +1,7 @@
ext {
extName = 'Kagane'
extClass = '.Kagane'
extVersionCode = 9
extVersionCode = 10
isNsfw = true
}

View File

@ -56,7 +56,7 @@ class DetailsDto(
author = authors.joinToString()
description = desc.toString()
genre = genres.joinToString()
genre = (listOf(source) + genres).joinToString()
status = this@DetailsDto.status.toStatus()
}
@ -86,11 +86,13 @@ class ChapterDto(
@SerialName("number_sort")
val number: Float,
) {
fun toSChapter(): SChapter = SChapter.create().apply {
fun toSChapter(useSourceChapterNumber: Boolean = false): SChapter = SChapter.create().apply {
url = "$seriesId;$id;$pagesCount"
name = title
date_upload = dateFormat.tryParse(releaseDate)
chapter_number = number
if (useSourceChapterNumber) {
chapter_number = number
}
}
}

View File

@ -102,8 +102,6 @@ internal class SourcesFilter(
},
)
internal class ScanlationsFilter() : Filter.CheckBox("Show scanlations", true)
class FilterData(
val id: String,
val name: String,
@ -116,7 +114,7 @@ internal open class JsonMultiSelectFilter(
private val param: String,
genres: List<MultiSelectOption>,
) : Filter.Group<MultiSelectOption>(name, genres), JsonFilter {
override fun addToJsonObject(builder: JsonObjectBuilder) {
override fun addToJsonObject(builder: JsonObjectBuilder, additionExcludeList: List<String>) {
val whatToInclude = state.filter { it.state }.map { it.id }
if (whatToInclude.isNotEmpty()) {
@ -134,9 +132,9 @@ internal open class JsonMultiSelectTriFilter(
private val param: String,
genres: List<MultiSelectTriOption>,
) : Filter.Group<MultiSelectTriOption>(name, genres), JsonFilter {
override fun addToJsonObject(builder: JsonObjectBuilder) {
override fun addToJsonObject(builder: JsonObjectBuilder, additionExcludeList: List<String>) {
val whatToInclude = state.filter { it.state == TriState.STATE_INCLUDE }.map { it.id }
val whatToExclude = state.filter { it.state == TriState.STATE_EXCLUDE }.map { it.id }
val whatToExclude = state.filter { it.state == TriState.STATE_EXCLUDE }.map { it.id } + additionExcludeList
with(builder) {
if (whatToInclude.isNotEmpty()) {
@ -160,5 +158,74 @@ internal open class JsonMultiSelectTriFilter(
}
internal interface JsonFilter {
fun addToJsonObject(builder: JsonObjectBuilder)
fun addToJsonObject(builder: JsonObjectBuilder, additionExcludeList: List<String> = emptyList())
}
internal val GenresList = arrayOf(
"Romance",
"Drama",
"Manhwa",
"Fantasy",
"Manga",
"Comedy",
"Action",
"Mature",
"LGBTQIA+",
"Shoujo",
"Josei",
"Shounen",
"Supernatural",
"Boys' Love",
"Slice of Life",
"Seinen",
"Adventure",
"Manhua",
"School Life",
"Smut",
"Yaoi",
"Hentai",
"Historical",
"Isekai",
"Mystery",
"Psychological",
"Tragedy",
"Harem",
"Martial Arts",
"Science Fiction",
"Shounen Ai",
"Ecchi",
"Horror",
"Girls' Love",
"Anime",
"Thriller",
"Yuri",
"Coming of Age",
"Sports",
"OEL",
"Gender Bender",
"Suspense",
"Music",
"Shoujo Ai",
"Award Winning",
"Cooking",
"Crime",
"Doujinshi",
"Mecha",
"Oneshot",
"Philosophical",
"Magical Girls",
"Anthology",
"Wuxia",
"Medical",
"official colored",
"family life",
"parody",
"Superhero",
"4-Koma",
"educational",
"self-published",
"Animals",
"Magic",
"fan colored",
"monsters",
)

View File

@ -13,6 +13,7 @@ import android.webkit.PermissionRequest
import android.webkit.WebChromeClient
import android.webkit.WebView
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
@ -125,7 +126,7 @@ class Kagane : HttpSource(), ConfigurableSource {
ContentRatingFilter(
preferences.contentRating.toSet(),
),
ScanlationsFilter(),
GenresFilter(emptyList()),
),
)
@ -142,7 +143,7 @@ class Kagane : HttpSource(), ConfigurableSource {
ContentRatingFilter(
preferences.contentRating.toSet(),
),
ScanlationsFilter(),
GenresFilter(emptyList()),
),
)
@ -154,6 +155,9 @@ class Kagane : HttpSource(), ConfigurableSource {
val body = buildJsonObject {
filters.forEach { filter ->
when (filter) {
is GenresFilter -> {
filter.addToJsonObject(this, preferences.excludedGenres.toList())
}
is JsonFilter -> {
filter.addToJsonObject(this)
}
@ -175,15 +179,17 @@ class Kagane : HttpSource(), ConfigurableSource {
is SortFilter -> {
filter.toUriPart().takeIf { it.isNotEmpty() }
?.let { uriPart -> addQueryParameter("sort", uriPart) }
}
is ScanlationsFilter -> {
addQueryParameter("scanlations", filter.state.toString())
?: run {
if (query.isBlank()) {
addQueryParameter("sort", "updated_at,desc")
}
}
}
else -> {}
}
}
addQueryParameter("scanlations", preferences.showScanlations.toString())
}
return POST(url.toString(), headers, body)
@ -203,7 +209,11 @@ class Kagane : HttpSource(), ConfigurableSource {
}
override fun mangaDetailsRequest(manga: SManga): Request {
return GET("$apiUrl/api/v1/series/${manga.url}", apiHeaders)
return mangaDetailsRequest(manga.url)
}
private fun mangaDetailsRequest(seriesId: String): Request {
return GET("$apiUrl/api/v1/series/$seriesId", apiHeaders)
}
override fun getMangaUrl(manga: SManga): String {
@ -213,8 +223,25 @@ class Kagane : HttpSource(), ConfigurableSource {
// ============================== Chapters ==============================
override fun chapterListParse(response: Response): List<SChapter> {
val seriesId = response.request.url.toString()
.substringAfterLast("/")
val dto = response.parseAs<ChapterDto>()
return dto.content.map { it -> it.toSChapter() }.reversed()
val source = runCatching {
client.newCall(mangaDetailsRequest(seriesId))
.execute()
.parseAs<DetailsDto>()
.source
}.getOrDefault("")
val useSourceChapterNumber = source in setOf(
"Dark Horse Comics",
"Flame Comics",
"MangaDex",
"Square Enix Manga",
)
return dto.content.map { it -> it.toSChapter(useSourceChapterNumber) }.reversed()
}
override fun chapterListRequest(manga: SManga): Request {
@ -430,6 +457,12 @@ class Kagane : HttpSource(), ConfigurableSource {
return CONTENT_RATINGS.slice(0..index.coerceAtLeast(0))
}
private val SharedPreferences.excludedGenres: Set<String>
get() = this.getStringSet(GENRES_PREF, emptySet()) ?: emptySet()
private val SharedPreferences.showScanlations: Boolean
get() = this.getBoolean(SHOW_SCANLATIONS, SHOW_SCANLATIONS_DEFAULT)
private val SharedPreferences.dataSaver
get() = this.getBoolean(DATA_SAVER, false)
@ -443,6 +476,27 @@ class Kagane : HttpSource(), ConfigurableSource {
setDefaultValue(CONTENT_RATING_DEFAULT)
}.let(screen::addPreference)
MultiSelectListPreference(screen.context).apply {
key = GENRES_PREF
title = "Exclude Genres"
entries = GenresList.map { it.replaceFirstChar { c -> c.uppercase() } }.toTypedArray()
entryValues = GenresList
summary = preferences.excludedGenres.joinToString { it.replaceFirstChar { c -> c.uppercase() } }
setDefaultValue(emptySet<String>())
setOnPreferenceChangeListener { _, values ->
val selected = values as Set<String>
this.summary = selected.joinToString { it.replaceFirstChar { c -> c.uppercase() } }
true
}
}.let(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_SCANLATIONS
title = "Show scanlations"
setDefaultValue(SHOW_SCANLATIONS_DEFAULT)
}.let(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = DATA_SAVER
title = "Data saver"
@ -462,6 +516,10 @@ class Kagane : HttpSource(), ConfigurableSource {
"pornographic",
)
private const val GENRES_PREF = "pref_genres_exclude"
private const val SHOW_SCANLATIONS = "pref_show_scanlations"
private const val SHOW_SCANLATIONS_DEFAULT = true
private const val DATA_SAVER = "data_saver_default"
}
@ -486,7 +544,6 @@ class Kagane : HttpSource(), ConfigurableSource {
// TagsFilter(),
// SourcesFilter(),
Filter.Separator(),
ScanlationsFilter(),
)
val response = metadataClient.newCall(