ComickLive | Add option to manually input tags (#11448)

* Add option to manually input tags

* Update Comick.kt

* Apply AwkwardPeak's Suggestions

* comma

* transforming serializer on property

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
KenjieDec 2025-11-12 12:36:56 +07:00 committed by Draff
parent f817f7b049
commit 91115ac93f
Signed by: Draff
GPG Key ID: E8A89F3211677653
4 changed files with 104 additions and 28 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Comick (Unoriginal)' extName = 'Comick (Unoriginal)'
extClass = '.ComickFactory' extClass = '.ComickFactory'
extVersionCode = 2 extVersionCode = 3
isNsfw = true isNsfw = true
} }

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension.all.comicklive
import android.util.Log import android.util.Log
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
@ -114,38 +115,50 @@ class Comick(
private var nextCursor: String? = null private var nextCursor: String? = null
private val spaceSlashRegex = Regex("[ /]")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (page == 1) { if (page == 1) {
nextCursor = null nextCursor = null
} }
val url = "$baseUrl/api/search".toHttpUrl().newBuilder().apply { val url = "$baseUrl/api/search".toHttpUrl().newBuilder().apply {
addQueryParameter("order_by", filters.firstInstance<SortFilter>().selected) filters.firstInstance<SortFilter>().let {
addQueryParameter("order_direction", "desc") addQueryParameter("order_by", it.selected)
addQueryParameter("order_direction", if (it.state!!.ascending) "asc" else "desc")
}
filters.firstInstanceOrNull<GenreFilter>()?.let { genre -> filters.firstInstanceOrNull<GenreFilter>()?.let { genre ->
genre.included.forEach { genre.included.forEach {
addQueryParameter("genres[]", it) addQueryParameter("genres", it)
} }
genre.excluded.forEach { genre.excluded.forEach {
addQueryParameter("excludes[]", it) addQueryParameter("excludes", it)
}
}
filters.firstInstanceOrNull<TagFilterText>()?.let { text ->
text.state.split(",").filter(String::isNotBlank).forEach {
val value = it.trim().lowercase().replace(spaceSlashRegex, "-")
addQueryParameter(
if (value.startsWith("-")) "excluded_tags" else "tags",
value.replaceFirst("-", ""),
)
} }
} }
filters.firstInstanceOrNull<TagFilter>()?.let { tag -> filters.firstInstanceOrNull<TagFilter>()?.let { tag ->
tag.included.forEach { tag.included.forEach {
addQueryParameter("tags[]", it) addQueryParameter("tags", it)
} }
tag.excluded.forEach { tag.excluded.forEach {
addQueryParameter("excluded_tags[]", it) addQueryParameter("excluded_tags", it)
} }
} }
filters.firstInstance<DemographicFilter>().checked.forEach { filters.firstInstance<DemographicFilter>().checked.forEach {
addQueryParameter("demographic[]", it) addQueryParameter("demographic", it)
} }
filters.firstInstance<CreatedAtFilter>().selected?.let { filters.firstInstance<CreatedAtFilter>().selected?.let {
addQueryParameter("time", it) addQueryParameter("time", it)
} }
filters.firstInstance<TypeFilter>().checked.forEach { filters.firstInstance<TypeFilter>().checked.forEach {
addQueryParameter("country[]", it) addQueryParameter("country", it)
} }
filters.firstInstance<MinimumChaptersFilter>().state.let { filters.firstInstance<MinimumChaptersFilter>().state.let {
if (it.isNotBlank()) { if (it.isNotBlank()) {
@ -208,8 +221,8 @@ class Comick(
val filters: MutableList<Filter<*>> = mutableListOf( val filters: MutableList<Filter<*>> = mutableListOf(
SortFilter(), SortFilter(),
DemographicFilter(), DemographicFilter(),
CreatedAtFilter(),
TypeFilter(), TypeFilter(),
CreatedAtFilter(),
MinimumChaptersFilter(), MinimumChaptersFilter(),
StatusFilter(), StatusFilter(),
ContentRatingFilter(), ContentRatingFilter(),
@ -221,7 +234,16 @@ class Comick(
GET("$baseUrl/api/metadata", headers, CacheControl.FORCE_CACHE), GET("$baseUrl/api/metadata", headers, CacheControl.FORCE_CACHE),
).await() ).await()
// the cache only request fails if it was not cached already val getTags = preferences.getBoolean(GET_TAGS, true)
val textTags: List<Filter<*>> = listOf(
Filter.Separator(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
TagFilterText(),
Filter.Separator(),
)
if (!response.isSuccessful) { if (!response.isSuccessful) {
metadataClient.newCall( metadataClient.newCall(
GET("$baseUrl/api/metadata", headers, CacheControl.FORCE_NETWORK), GET("$baseUrl/api/metadata", headers, CacheControl.FORCE_NETWORK),
@ -236,10 +258,16 @@ class Comick(
}, },
) )
if (!getTags) {
filters.addAll(
index = 2,
textTags,
)
}
filters.addAll( filters.addAll(
index = 0, index = 0,
listOf( listOf(
Filter.Header("Press 'reset' to load genres and tags"), Filter.Header("Press 'reset' to load genres ${if (getTags) "and tags" else ""}"),
Filter.Separator(), Filter.Separator(),
), ),
) )
@ -251,23 +279,37 @@ class Comick(
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e(name, "Unable to parse filters", e) Log.e(name, "Unable to parse filters", e)
if (!getTags) {
filters.addAll(
index = 2,
textTags,
)
}
filters.addAll( filters.addAll(
index = 0, index = 0,
listOf( listOf(
Filter.Header("Failed to parse genres and tags"), Filter.Header("Failed to parse genres ${if (getTags) "and tags" else ""}"),
Filter.Separator(), Filter.Separator(),
), ),
) )
return@runBlocking FilterList(filters) return@runBlocking FilterList(filters)
} }
filters.addAll( filters.add(
index = 1, index = 3,
listOf(
GenreFilter(data.genres), GenreFilter(data.genres),
TagFilter(data.tags),
),
) )
if (!getTags) {
filters.addAll(
index = 4,
textTags,
)
} else {
filters.add(
index = 4,
TagFilter(data.tags),
)
}
return@runBlocking FilterList(filters) return@runBlocking FilterList(filters)
} }
@ -300,7 +342,7 @@ class Comick(
if (data.titles.isNotEmpty()) { if (data.titles.isNotEmpty()) {
append("\n\n Alternative Titles: \n") append("\n\n Alternative Titles: \n")
data.titles.forEach { data.titles.forEach {
append(it.title, "\n") append("- ", it.title.trim(), "\n")
} }
} }
}.trim() }.trim()
@ -383,8 +425,17 @@ class Comick(
summary = "%s" summary = "%s"
setDefaultValue("0") setDefaultValue("0")
}.also(screen::addPreference) }.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = GET_TAGS
title = "Tags Input Type"
summaryOn = "Tags will be in a form of scrollable list"
summaryOff = "Tags will need to be inputted manually"
setDefaultValue(true)
}.also(screen::addPreference)
} }
} }
private val domains = arrayOf("https://comick.live", "https://comick.art") private val domains = arrayOf("https://comick.live", "https://comick.art")
private const val DOMAIN_PREF = "domain_pref" private const val DOMAIN_PREF = "domain_pref"
private const val GET_TAGS = "get_tags"

View File

@ -3,6 +3,11 @@ package eu.kanade.tachiyomi.extension.all.comicklive
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 kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonTransformingSerializer
@Serializable @Serializable
class Data<T>( class Data<T>(
@ -60,6 +65,7 @@ class ComicData(
@SerialName("md_comic_md_genres") @SerialName("md_comic_md_genres")
val genres: List<Genres>, val genres: List<Genres>,
@SerialName("md_titles") @SerialName("md_titles")
@Serializable(with = TitleTransform::class)
val titles: List<Title>, val titles: List<Title>,
) { ) {
@Serializable @Serializable
@ -79,6 +85,15 @@ class ComicData(
) )
} }
object TitleTransform : JsonTransformingSerializer<List<ComicData.Title>>(
ListSerializer(ComicData.Title.serializer()),
) {
override fun transformDeserialize(element: JsonElement): JsonElement {
if (element !is JsonObject) return element
return JsonArray(element.values.toList())
}
}
@Serializable @Serializable
class ChapterList( class ChapterList(
val data: List<Chapter>, val data: List<Chapter>,

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.all.comicklive
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import java.util.Calendar import java.util.Calendar
import kotlin.collections.filter
abstract class SelectFilter( abstract class SelectFilter(
name: String, name: String,
@ -38,16 +39,21 @@ abstract class TriStateGroupFilter(
val excluded get() = state.filter { it.isExcluded() }.map { it.slug } val excluded get() = state.filter { it.isExcluded() }.map { it.slug }
} }
class SortFilter : SelectFilter( private val getSortsList = listOf(
name = "Sort",
options = listOf(
"Latest" to "created_at", "Latest" to "created_at",
"Popular" to "user_follow_count", "Popular" to "user_follow_count",
"Highest Rating" to "rating", "Highest Rating" to "rating",
"Last Uploaded" to "uploaded", "Last Uploaded" to "uploaded",
),
) )
class SortFilter : Filter.Sort(
name = "Sort",
values = getSortsList.map { it.first }.toTypedArray(),
state = Selection(0, false),
) {
val selected get() = state?.let { getSortsList[it.index] }?.second.takeIf { it?.isNotEmpty() ?: false }
}
class GenreFilter(genres: List<Metadata.Name>) : TriStateGroupFilter( class GenreFilter(genres: List<Metadata.Name>) : TriStateGroupFilter(
name = "Genre", name = "Genre",
options = genres.map { it.name to it.slug }, options = genres.map { it.name to it.slug },
@ -58,6 +64,10 @@ class TagFilter(tags: List<Metadata.Name>) : TriStateGroupFilter(
options = tags.map { it.name to it.slug }, options = tags.map { it.name to it.slug },
) )
class TagFilterText : Filter.Text(
name = "Tags",
)
class DemographicFilter : CheckBoxGroup( class DemographicFilter : CheckBoxGroup(
name = "Demographic", name = "Demographic",
options = listOf( options = listOf(