diff --git a/CHANGELOG.md b/CHANGELOG.md index f253028b7..b3f891a79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co ## [Unreleased] ### Added - Add `id:` prefix search to remaining trackers (AniList, Bangumi, Kitsu, MangaUpdates, Shikimori, and Hikka) ([@MajorTanya](https://github.com/MajorTanya)) ([#3776](https://github.com/mihonapp/mihon/pull/3776)) +- Add support for using the user's chosen rating system for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#3818](https://github.com/mihonapp/mihon/pull/3818)) ### Improved - Show updates and upcoming filter icon as active for categories ([@Secozzi](https://github.com/Secozzi)) ([#3772](https://github.com/mihonapp/mihon/pull/3772)) diff --git a/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt b/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt index 03dd4cdbc..6af7d2d08 100644 --- a/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/track/service/TrackPreferences.kt @@ -6,6 +6,7 @@ import dev.zacsweers.metro.SingleIn import eu.kanade.domain.track.model.AutoTrackState import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.anilist.Anilist +import eu.kanade.tachiyomi.data.track.kitsu.Kitsu import eu.kanade.tachiyomi.data.track.mangabaka.MangaBaka import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.PreferenceStore @@ -47,6 +48,8 @@ class TrackPreferences( val anilistScoreType: Preference = preferenceStore.getString("anilist_score_type", Anilist.POINT_10) + val kitsuScoreType: Preference = preferenceStore.getString("kitsu_score_type", Kitsu.RATING_ADVANCED) + val mangabakaScoreType: Preference = preferenceStore.getString("mangabaka_score_type", MangaBaka.STEP_1) val autoUpdateTrack: Preference = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt index edd21e15f..8a5a35f0a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt @@ -8,6 +8,8 @@ import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth import eu.kanade.tachiyomi.data.track.model.TrackSearch import kotlinx.serialization.json.Json +import logcat.LogPriority +import tachiyomi.core.common.util.system.logcat import tachiyomi.i18n.MR import uy.kohesive.injekt.injectLazy import java.text.DecimalFormat @@ -15,6 +17,12 @@ import tachiyomi.domain.track.model.Track as DomainTrack class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { + private data class RatingSystem( + val name: String, + val scoreList: List, + val twentyScale: List, + ) + companion object { const val READING = 1L const val COMPLETED = 2L @@ -22,6 +30,33 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { const val DROPPED = 4L const val PLAN_TO_READ = 5L + const val RATING_SIMPLE = "simple" + const val RATING_REGULAR = "regular" + const val RATING_ADVANCED = "advanced" + + private val ratingSystems = mapOf( + // Smileys + RATING_SIMPLE to RatingSystem( + name = RATING_SIMPLE, + scoreList = listOf("-", "😡", "😐", "😊", "😀"), + twentyScale = (2..20 step 6).toList(), // 2, 8, 14, 20 + ), + // DecimalFormatter is not thread safe, so new formatters for each map instead of extracted val attribute + // to not incite reuse + // Stars (0.5-5 step 0.5) + RATING_REGULAR to RatingSystem( + name = RATING_REGULAR, + scoreList = (0..10).map { it / 2.0 }.map(DecimalFormat("0.#")::format).map { "$it ★" }, + twentyScale = (2..20 step 2).toList(), // 2, 4, ..., 18, 20 + ), + // 10 point decimal (step 0.5, starting at 1) + 0 for our "not rated" placeholder + RATING_ADVANCED to RatingSystem( + name = RATING_ADVANCED, + scoreList = listOf("0") + (2..20).map { it / 2.0 }.map(DecimalFormat("0.#")::format), + twentyScale = (2..20).toList(), // 2, 3, ..., 19, 20 + ), + ) + private const val SEARCH_ID_PREFIX = "id:" } @@ -35,6 +70,8 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { private val api by lazy { KitsuApi(client, interceptor) } + private val scorePreference by lazy { trackPreferences.kitsuScoreType } + override fun getLogo() = R.drawable.brand_kitsu override fun getStatusList(): List { @@ -56,18 +93,29 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { override fun getCompletionStatus(): Long = COMPLETED - override fun getScoreList(): List { - val df = DecimalFormat("0.#") - return (listOf("0") + IntRange(2, 20).map { df.format(it / 2f) }) + private fun getCurrentRatingSystem(): RatingSystem { + val ratingSystem = scorePreference.get() + return ratingSystems[ratingSystem] ?: throw Exception("Unknown score type $ratingSystem") + } + + override fun getScoreList(): List = getCurrentRatingSystem().scoreList + + override fun get10PointScore(track: DomainTrack): Double { + // score is stored in Kitsu's native 2-20 scale + return track.score / 2.0 } override fun indexToScore(index: Int): Double { - return if (index > 0) (index + 1) / 2.0 else 0.0 + if (index == 0) return 0.0 + return getCurrentRatingSystem().twentyScale[index - 1].toDouble() } override fun displayScore(track: DomainTrack): String { - val df = DecimalFormat("0.#") - return df.format(track.score) + val ratingSystem = getCurrentRatingSystem() + // Since Kitsu's valid score range is 2-20, unset values of -1.0 or 0.0 will both return -1 from indexOfLast + // which is turned into index 0 of the scoreList, giving us the "unset" display score (- or 0). + // Proper scores are "rounded down" to the nearest value of the scale (also what Kitsu's website does) + return ratingSystem.scoreList[ratingSystem.twentyScale.indexOfLast { it <= track.score } + 1] } private suspend fun add(track: Track): Track { @@ -136,6 +184,14 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { val token = api.login(username, password) interceptor.newAuth(token) val currentUser = api.getCurrentUser() + + val ratingSystem = currentUser.attributes.ratingSystem + if (ratingSystem in listOf(RATING_SIMPLE, RATING_REGULAR, RATING_ADVANCED)) { + scorePreference.set(ratingSystem) + } else { + logcat(LogPriority.ERROR) { "Unsupported Kitsu score type: $ratingSystem" } + scorePreference.set(RATING_ADVANCED) + } saveDisplayUsername(currentUser.attributes.name) saveCredentials(username, currentUser.id) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index 2c5b28de7..b8b1b32dd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -96,7 +96,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) putJsonObject("attributes") { put("status", track.toApiStatus()) put("progress", track.last_chapter_read.toInt()) - put("ratingTwenty", track.toApiScore()) + put("ratingTwenty", track.score.takeIf { it > 0 }?.toInt()) put("startedAt", KitsuDateHelper.convert(track.started_reading_date)) put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date)) put("private", track.private) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt index 02a88e09c..8cea94bc3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt @@ -10,7 +10,3 @@ fun Track.toApiStatus() = when (status) { Kitsu.PLAN_TO_READ -> "planned" else -> throw Exception("Unknown status") } - -fun Track.toApiScore(): String? { - return if (score > 0) (score * 2).toInt().toString() else null -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt index 0fd42ce0b..d5143530c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuListSearch.kt @@ -41,7 +41,7 @@ data class KitsuListSearchResult( "planned" -> Kitsu.PLAN_TO_READ else -> throw Exception("Unknown status") } - score = userDataAttrs.ratingTwenty?.let { it / 2.0 } ?: 0.0 + score = userDataAttrs.ratingTwenty?.toDouble() ?: 0.0 last_chapter_read = userDataAttrs.progress.toDouble() private = userDataAttrs.private } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt index 075931372..547db25ba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt @@ -16,4 +16,5 @@ data class KitsuUser( @Serializable data class KitsuUserAttributes( val name: String, + val ratingSystem: String, ) diff --git a/data/src/main/sqldelight/tachiyomi/migrations/14.sqm b/data/src/main/sqldelight/tachiyomi/migrations/14.sqm new file mode 100644 index 000000000..1519cd665 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/14.sqm @@ -0,0 +1,5 @@ +-- rescale Kitsu (sync_id 3) scores to native 2-20 int scale +-- old scores were rescaled from Kitsu's 2-20 down to 1, 1.5, ..., 10 at the API boundary +UPDATE manga_sync +SET score = score * 2 +WHERE sync_id = 3;