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 { ext {
extName = 'Kagane' extName = 'Kagane'
extClass = '.Kagane' extClass = '.Kagane'
extVersionCode = 9 extVersionCode = 10
isNsfw = true isNsfw = true
} }

View File

@ -56,7 +56,7 @@ class DetailsDto(
author = authors.joinToString() author = authors.joinToString()
description = desc.toString() description = desc.toString()
genre = genres.joinToString() genre = (listOf(source) + genres).joinToString()
status = this@DetailsDto.status.toStatus() status = this@DetailsDto.status.toStatus()
} }
@ -86,11 +86,13 @@ class ChapterDto(
@SerialName("number_sort") @SerialName("number_sort")
val number: Float, val number: Float,
) { ) {
fun toSChapter(): SChapter = SChapter.create().apply { fun toSChapter(useSourceChapterNumber: Boolean = false): SChapter = SChapter.create().apply {
url = "$seriesId;$id;$pagesCount" url = "$seriesId;$id;$pagesCount"
name = title name = title
date_upload = dateFormat.tryParse(releaseDate) 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( class FilterData(
val id: String, val id: String,
val name: String, val name: String,
@ -116,7 +114,7 @@ internal open class JsonMultiSelectFilter(
private val param: String, private val param: String,
genres: List<MultiSelectOption>, genres: List<MultiSelectOption>,
) : Filter.Group<MultiSelectOption>(name, genres), JsonFilter { ) : 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 } val whatToInclude = state.filter { it.state }.map { it.id }
if (whatToInclude.isNotEmpty()) { if (whatToInclude.isNotEmpty()) {
@ -134,9 +132,9 @@ internal open class JsonMultiSelectTriFilter(
private val param: String, private val param: String,
genres: List<MultiSelectTriOption>, genres: List<MultiSelectTriOption>,
) : Filter.Group<MultiSelectTriOption>(name, genres), JsonFilter { ) : 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 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) { with(builder) {
if (whatToInclude.isNotEmpty()) { if (whatToInclude.isNotEmpty()) {
@ -160,5 +158,74 @@ internal open class JsonMultiSelectTriFilter(
} }
internal interface JsonFilter { 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.WebChromeClient
import android.webkit.WebView import android.webkit.WebView
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
@ -125,7 +126,7 @@ class Kagane : HttpSource(), ConfigurableSource {
ContentRatingFilter( ContentRatingFilter(
preferences.contentRating.toSet(), preferences.contentRating.toSet(),
), ),
ScanlationsFilter(), GenresFilter(emptyList()),
), ),
) )
@ -142,7 +143,7 @@ class Kagane : HttpSource(), ConfigurableSource {
ContentRatingFilter( ContentRatingFilter(
preferences.contentRating.toSet(), preferences.contentRating.toSet(),
), ),
ScanlationsFilter(), GenresFilter(emptyList()),
), ),
) )
@ -154,6 +155,9 @@ class Kagane : HttpSource(), ConfigurableSource {
val body = buildJsonObject { val body = buildJsonObject {
filters.forEach { filter -> filters.forEach { filter ->
when (filter) { when (filter) {
is GenresFilter -> {
filter.addToJsonObject(this, preferences.excludedGenres.toList())
}
is JsonFilter -> { is JsonFilter -> {
filter.addToJsonObject(this) filter.addToJsonObject(this)
} }
@ -175,15 +179,17 @@ class Kagane : HttpSource(), ConfigurableSource {
is SortFilter -> { is SortFilter -> {
filter.toUriPart().takeIf { it.isNotEmpty() } filter.toUriPart().takeIf { it.isNotEmpty() }
?.let { uriPart -> addQueryParameter("sort", uriPart) } ?.let { uriPart -> addQueryParameter("sort", uriPart) }
} ?: run {
if (query.isBlank()) {
is ScanlationsFilter -> { addQueryParameter("sort", "updated_at,desc")
addQueryParameter("scanlations", filter.state.toString()) }
}
} }
else -> {} else -> {}
} }
} }
addQueryParameter("scanlations", preferences.showScanlations.toString())
} }
return POST(url.toString(), headers, body) return POST(url.toString(), headers, body)
@ -203,7 +209,11 @@ class Kagane : HttpSource(), ConfigurableSource {
} }
override fun mangaDetailsRequest(manga: SManga): Request { 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 { override fun getMangaUrl(manga: SManga): String {
@ -213,8 +223,25 @@ class Kagane : HttpSource(), ConfigurableSource {
// ============================== Chapters ============================== // ============================== Chapters ==============================
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val seriesId = response.request.url.toString()
.substringAfterLast("/")
val dto = response.parseAs<ChapterDto>() 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 { override fun chapterListRequest(manga: SManga): Request {
@ -430,6 +457,12 @@ class Kagane : HttpSource(), ConfigurableSource {
return CONTENT_RATINGS.slice(0..index.coerceAtLeast(0)) 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 private val SharedPreferences.dataSaver
get() = this.getBoolean(DATA_SAVER, false) get() = this.getBoolean(DATA_SAVER, false)
@ -443,6 +476,27 @@ class Kagane : HttpSource(), ConfigurableSource {
setDefaultValue(CONTENT_RATING_DEFAULT) setDefaultValue(CONTENT_RATING_DEFAULT)
}.let(screen::addPreference) }.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 { SwitchPreferenceCompat(screen.context).apply {
key = DATA_SAVER key = DATA_SAVER
title = "Data saver" title = "Data saver"
@ -462,6 +516,10 @@ class Kagane : HttpSource(), ConfigurableSource {
"pornographic", "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" private const val DATA_SAVER = "data_saver_default"
} }
@ -486,7 +544,6 @@ class Kagane : HttpSource(), ConfigurableSource {
// TagsFilter(), // TagsFilter(),
// SourcesFilter(), // SourcesFilter(),
Filter.Separator(), Filter.Separator(),
ScanlationsFilter(),
) )
val response = metadataClient.newCall( val response = metadataClient.newCall(