diff --git a/api/anilist/src/main/graphql/fragments/User.graphql b/api/anilist/src/main/graphql/fragments/User.graphql index 315eae8f..0d8e1dcb 100644 --- a/api/anilist/src/main/graphql/fragments/User.graphql +++ b/api/anilist/src/main/graphql/fragments/User.graphql @@ -38,6 +38,7 @@ fragment User on User { } } mediaListOptions { + scoreFormat animeList { sectionOrder } diff --git a/api/anilist/src/main/graphql/mutations/UpdateUser.graphql b/api/anilist/src/main/graphql/mutations/UpdateUser.graphql index 5aedbd48..f9ecd580 100644 --- a/api/anilist/src/main/graphql/mutations/UpdateUser.graphql +++ b/api/anilist/src/main/graphql/mutations/UpdateUser.graphql @@ -1,10 +1,12 @@ mutation UpdateUser( $profileColor: String, + $scoreFormat: ScoreFormat, $animeListOptions: MediaListOptionsInput, $mangaListOptions: MediaListOptionsInput, ) { UpdateUser( profileColor: $profileColor, + scoreFormat: $scoreFormat, animeListOptions: $animeListOptions mangaListOptions: $mangaListOptions ) { @@ -12,6 +14,7 @@ mutation UpdateUser( profileColor } mediaListOptions { + scoreFormat animeList { sectionOrder } diff --git a/api/anilist/src/main/graphql/queries/UserQuery.graphql b/api/anilist/src/main/graphql/queries/UserQuery.graphql index c39558e1..5993e3b1 100644 --- a/api/anilist/src/main/graphql/queries/UserQuery.graphql +++ b/api/anilist/src/main/graphql/queries/UserQuery.graphql @@ -22,14 +22,14 @@ query Viewer { } } -query UserMediaListQuery($userId: Int, $type: MediaType) { +query UserMediaListQuery($userId: Int, $type: MediaType, $scoreFormat: ScoreFormat) { mediaListCollection: MediaListCollection(userId: $userId, type: $type) { lists { name entries { status progress - score(format: POINT_10_DECIMAL) + score(format: $scoreFormat) media { ...MediaTracking } diff --git a/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistUserRepository.kt b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistUserRepository.kt index 26871280..80ad35bf 100644 --- a/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistUserRepository.kt +++ b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/AnilistUserRepository.kt @@ -8,6 +8,7 @@ import com.imashnake.animite.api.anilist.sanitize.media.Media import com.imashnake.animite.api.anilist.sanitize.profile.User import com.imashnake.animite.api.anilist.type.MediaListOptionsInput import com.imashnake.animite.api.anilist.type.MediaType +import com.imashnake.animite.api.anilist.type.ScoreFormat import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter @@ -42,12 +43,14 @@ class AnilistUserRepository( type: MediaType?, useNetwork: Boolean, language: Media.Language = Media.Language.DEFAULT, + scoreFormat: ScoreFormat, mediaListOrder: List ): Flow> { return apolloClient.query( UserMediaListQuery( userId = Optional.presentIfNotNull(id), - type = Optional.presentIfNotNull(type) + type = Optional.presentIfNotNull(type), + scoreFormat = Optional.presentIfNotNull(scoreFormat) ) ) .fetchPolicy( @@ -57,11 +60,12 @@ class AnilistUserRepository( ) .toFlow() .filter { it.exception == null } - .asResult { User.MediaCollection(it, type, language, mediaListOrder) } + .asResult { User.MediaCollection(it, type, language, scoreFormat, mediaListOrder) } } fun updateUser( profileColor: String? = null, + scoreFormat: ScoreFormat? = null, animeSectionOrder: List? = null, mangaSectionOrder: List? = null, ): Flow> { @@ -69,6 +73,7 @@ class AnilistUserRepository( .mutation( UpdateUserMutation( profileColor = Optional.presentIfNotNull(profileColor), + scoreFormat = Optional.presentIfNotNull(scoreFormat), animeListOptions = Optional.presentIfNotNull( MediaListOptionsInput( sectionOrder = Optional.presentIfNotNull(animeSectionOrder) diff --git a/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/sanitize/media/Media.kt b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/sanitize/media/Media.kt index 2871489d..13234e17 100644 --- a/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/sanitize/media/Media.kt +++ b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/sanitize/media/Media.kt @@ -24,6 +24,7 @@ import com.imashnake.animite.api.anilist.type.MediaSeason import com.imashnake.animite.api.anilist.type.MediaSort import com.imashnake.animite.api.anilist.type.MediaSource import com.imashnake.animite.api.anilist.type.MediaStatus +import com.imashnake.animite.api.anilist.type.ScoreFormat import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -32,6 +33,7 @@ import kotlinx.datetime.format.MonthNames import java.util.Locale import kotlin.collections.mapNotNull import kotlin.collections.orEmpty +import kotlin.math.roundToInt import kotlin.time.Clock import kotlin.time.Instant @@ -737,6 +739,7 @@ data class Media( query: MediaTracking, progress: Int?, score: Float?, + scoreFormat: ScoreFormat, status: TrackingStatus, language: Language ) : this( @@ -755,28 +758,46 @@ data class Media( format = query.format?.sanitize(), segments = query.episodes ?: query.nextAiringEpisode?.episode ?: query.chapters, progress = progress, - score = score?.let { Score(it) }, + score = score?.let { Score(it, scoreFormat) }, status = status ) } + @Immutable data class Score( val value: Float, - val color: Long, + val format: ScoreFormat, + val color: Long ) { companion object { private const val RED = 0xffff7770 private const val ORANGE = 0xffffc863 private const val LIME = 0xffb8ff70 private const val GREEN = 0xff63ff88 + + fun getColor(normalizedScore: Float) = when { + normalizedScore < 5f -> RED + normalizedScore < 7f -> ORANGE + normalizedScore < 9f -> LIME + else -> GREEN + } } - internal constructor(score: Float) : this( + internal constructor(score: Float, scoreFormat: ScoreFormat) : this( value = score, - color = when { - score < 5f -> RED - score < 7f -> ORANGE - score < 9f -> LIME + format = scoreFormat, + color = when(scoreFormat) { + ScoreFormat.POINT_100 -> getColor(score / 10f) + ScoreFormat.POINT_10_DECIMAL, + ScoreFormat.POINT_10 -> getColor(score) + ScoreFormat.POINT_5 -> getColor(score * 2f) + ScoreFormat.POINT_3 -> when (score.roundToInt()) { + 0 -> RED + 1 -> ORANGE + 2 -> LIME + 3 -> GREEN + else -> GREEN + } else -> GREEN } ) diff --git a/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/sanitize/profile/Viewer.kt b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/sanitize/profile/Viewer.kt index f1bcd746..f7708d09 100644 --- a/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/sanitize/profile/Viewer.kt +++ b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/sanitize/profile/Viewer.kt @@ -11,6 +11,7 @@ import com.imashnake.animite.api.anilist.sanitize.profile.User.TrackingStatus.Co import com.imashnake.animite.api.anilist.sanitize.profile.User.TrackingStatus.Companion.toTrackingStatus import com.imashnake.animite.api.anilist.type.MediaListStatus import com.imashnake.animite.api.anilist.type.MediaType +import com.imashnake.animite.api.anilist.type.ScoreFormat import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlin.time.Duration.Companion.minutes @@ -44,6 +45,8 @@ data class User( val banner: String?, /** @see User.Options.profileColor */ val color: String?, + /** @see User.MediaListOptions.scoreFormat */ + val scoreFormat: ScoreFormat?, // region About /** User Stats */ @@ -96,6 +99,7 @@ data class User( ) { internal constructor( query: UserMediaListQuery.List, + scoreFormat: ScoreFormat, language: Language ) : this( name = query.name, @@ -105,6 +109,7 @@ data class User( query = it?.media?.mediaTracking ?: return@mapNotNull null, progress = it.progress, score = it.score?.toFloat(), + scoreFormat = scoreFormat, language = language, status = it.status.toTrackingStatus( type = it.media.mediaTracking.type?.name?.let { type -> @@ -120,14 +125,15 @@ data class User( query: UserMediaListQuery.Data, type: MediaType?, language: Language, + scoreFormat: ScoreFormat, mediaListOrder: List ) : this( type = type?.name?.let { Type.valueOf(it) } ?: Type.UNKNOWN, namedLists = query.mediaListCollection?.lists.orEmpty().sortedBy { mediaListOrder.indexOf(it?.name) }.mapNotNull { - NamedTrackingList(it ?: return@mapNotNull null, language) - }.toImmutableList() + NamedTrackingList(it ?: return@mapNotNull null, scoreFormat, language) + }.toImmutableList(), ) } @@ -180,6 +186,7 @@ data class User( avatar = query.avatar?.large, banner = query.bannerImage, color = query.options?.profileColor, + scoreFormat = query.mediaListOptions?.scoreFormat, // TODO: Replace with string resources. stats = listOfNotNull( query.statistics?.anime?.count?.toString()?.let { @@ -247,7 +254,7 @@ data class User( fun String?.sanitize() = safeValueOf(this) - fun MediaListStatus?.toTrackingStatus(type: Type): TrackingStatus = when (this) { + fun MediaListStatus?.toTrackingStatus(type: Type) = when (this) { MediaListStatus.CURRENT -> if (type == Type.ANIME) WATCHING else READING MediaListStatus.PLANNING -> if (type == Type.ANIME) PLAN_TO_WATCH else PLAN_TO_READ MediaListStatus.COMPLETED -> COMPLETED diff --git a/api/preferences/src/commonMain/kotlin/com/imashnake/animite/api/preferences/PreferencesRepository.kt b/api/preferences/src/commonMain/kotlin/com/imashnake/animite/api/preferences/PreferencesRepository.kt index 3e3c5dc8..230efa30 100644 --- a/api/preferences/src/commonMain/kotlin/com/imashnake/animite/api/preferences/PreferencesRepository.kt +++ b/api/preferences/src/commonMain/kotlin/com/imashnake/animite/api/preferences/PreferencesRepository.kt @@ -22,6 +22,7 @@ private const val SHOW_USER_DESCRIPTION = true private const val USE_PROFILE_COLOR = true private const val USE_EXPRESSIVE_PROGRESS_INDICATOR = true private const val PROFILE_COLOR = "blue" +private const val SCORE_FORMAT = "POINT_10_DECIMAL" private const val IS_DEV_OPTIONS_ENABLED = false const val DEFAULT_MEDIA_LIST_ORDER = "[]" @@ -112,6 +113,8 @@ class PreferencesRepository internal constructor( // endregion // region settings + + // TODO: Expose and set enum value. private val themeKey = stringPreferencesKey("theme") val theme = dataStore.getValue(themeKey, DEFAULT_THEME_KEY) suspend fun setTheme(theme: String) { @@ -130,6 +133,7 @@ class PreferencesRepository internal constructor( dataStore.setValue(isAmoledKey, isAmoled) } + // TODO: Expose and set enum value. private val densityKey = stringPreferencesKey("density") val density = dataStore.getValue(densityKey, DEFAULT_DENSITY_KEY) suspend fun setDensity(density: String) { @@ -142,6 +146,7 @@ class PreferencesRepository internal constructor( dataStore.setValue(isNsfwEnabledKey, isNsfwEnabled) } + // TODO: Expose and set enum value. private val languageKey = stringPreferencesKey("language") val language = dataStore.getValue(languageKey, DEFAULT_LANGUAGE_KEY) suspend fun setLanguage(language: String) { @@ -178,6 +183,13 @@ class PreferencesRepository internal constructor( dataStore.setValue(profileColorKey, profileColor) } + // TODO: Expose and set enum value. + private val scoreFormatKey = stringPreferencesKey("score_format") + val scoreFormat = dataStore.getValue(scoreFormatKey, SCORE_FORMAT) + suspend fun setScoreFormat(scoreFormat: String?) { + dataStore.setValue(scoreFormatKey, scoreFormat) + } + // region developer options private val isDevOptionsEnabledKey = booleanPreferencesKey("dev_options_enabled") val isDevOptionsEnabled = dataStore.getValue(isDevOptionsEnabledKey, IS_DEV_OPTIONS_ENABLED) diff --git a/profile/build.gradle.kts b/profile/build.gradle.kts index ad1569d8..4a4be0d2 100644 --- a/profile/build.gradle.kts +++ b/profile/build.gradle.kts @@ -31,6 +31,7 @@ tasks.withType().configureEach { ) } +// TODO: Remove unused deps, dependencies { implementation(projects.api.anilist) implementation(projects.api.preferences) diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileViewModel.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileViewModel.kt index ed633ad6..814261ae 100644 --- a/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileViewModel.kt +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileViewModel.kt @@ -7,6 +7,7 @@ import androidx.navigation.toRoute import com.imashnake.animite.api.anilist.AnilistUserRepository import com.imashnake.animite.api.anilist.sanitize.media.Media import com.imashnake.animite.api.anilist.type.MediaType +import com.imashnake.animite.api.anilist.type.ScoreFormat import com.imashnake.animite.api.preferences.PreferencesRepository import com.imashnake.animite.core.resource.Resource import com.imashnake.animite.core.resource.Resource.Companion.asResource @@ -49,6 +50,11 @@ class ProfileViewModel @Inject constructor( val useProfileColor = preferencesRepository.useProfileColor.filterNotNull() val profileColor = preferencesRepository.profileColor.filterNotNull() + val scoreFormat = preferencesRepository + .scoreFormat + .filterNotNull() + .map { ScoreFormat.safeValueOf(it) } + val useExpressiveProgressIndicator = preferencesRepository.useExpressiveProgressIndicator.filterNotNull() val viewer = combine( @@ -63,6 +69,7 @@ class ProfileViewModel @Inject constructor( // Don't update with cache if (useNetwork) { preferencesRepository.setProfileColor(user.color) + preferencesRepository.setScoreFormat(user.scoreFormat?.name) preferencesRepository.setAnimeListOrder(user.animeListOrder) preferencesRepository.setMangaListOrder(user.mangaListOrder) } @@ -78,7 +85,8 @@ class ProfileViewModel @Inject constructor( flow2 = combine( preferencesRepository.viewerId.filterNotNull(), preferencesRepository.language.filterNotNull(), - ::Pair + scoreFormat, + ::Triple ), flow3 = preferencesRepository.animeListOrder.filterNotNull().map { Json.decodeFromString>(it) @@ -90,7 +98,8 @@ class ProfileViewModel @Inject constructor( type = MediaType.ANIME, useNetwork = useNetwork, language = Media.Language.valueOf(it.second.second), - mediaListOrder = it.third + scoreFormat = it.second.third, + mediaListOrder = it.third, ) }.asResource().stateIn( scope = viewModelScope, @@ -103,7 +112,8 @@ class ProfileViewModel @Inject constructor( flow2 = combine( preferencesRepository.viewerId.filterNotNull(), preferencesRepository.language.filterNotNull(), - ::Pair + scoreFormat, + ::Triple ), flow3 = preferencesRepository.mangaListOrder.filterNotNull().map { Json.decodeFromString>(it) @@ -115,6 +125,7 @@ class ProfileViewModel @Inject constructor( type = MediaType.MANGA, useNetwork = useNetwork, language = Media.Language.valueOf(it.second.second), + scoreFormat = it.second.third, mediaListOrder = it.third ) }.asResource().stateIn( diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/tabs/Media.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/tabs/Media.kt index 67fdf90c..d8951b4a 100644 --- a/profile/src/main/kotlin/com/imashnake/animite/profile/tabs/Media.kt +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/tabs/Media.kt @@ -25,9 +25,9 @@ import kotlinx.collections.immutable.ImmutableList fun MediaTab( mediaCollection: User.MediaCollection?, listVisibility: SnapshotStateMap, + useExpressiveProgressIndicator: Boolean, updateMediaListsOrder: (List) -> Unit, onNavigateToMediaItem: (MediaPage) -> Unit, - useExpressiveProgressIndicator: Boolean, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(), ) { @@ -65,9 +65,9 @@ private fun UserMediaLists( type: Media.Small.Type, lists: ImmutableList, listVisibility: SnapshotStateMap, + useExpressiveProgressIndicator: Boolean, updateMediaListsOrder: (List) -> Unit, onNavigateToMediaItem: (MediaPage) -> Unit, - useExpressiveProgressIndicator: Boolean, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(), ) { diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/ui/MediaTrackingList.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/ui/MediaTrackingList.kt index c509d402..672409d2 100644 --- a/profile/src/main/kotlin/com/imashnake/animite/profile/ui/MediaTrackingList.kt +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/ui/MediaTrackingList.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth @@ -57,9 +58,11 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.util.fastMapNotNull +import androidx.compose.ui.util.fastRoundToInt import com.imashnake.animite.api.anilist.sanitize.media.Media import com.imashnake.animite.api.anilist.sanitize.profile.User import com.imashnake.animite.api.anilist.sanitize.profile.User.TrackingStatus.Companion.sanitize +import com.imashnake.animite.api.anilist.type.ScoreFormat import com.imashnake.animite.core.ui.LocalPaddings import com.imashnake.animite.core.ui.component.Divider import com.imashnake.animite.core.ui.component.DropDownIcon @@ -72,6 +75,10 @@ import kotlinx.collections.immutable.ImmutableList import sh.calvin.reorderable.ReorderableCollectionItemScope import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState +import kotlin.math.roundToInt +import com.imashnake.animite.settings.R as settingsR + +private const val TOTAL_STARS = 5 @Composable fun MediaTrackingLists( @@ -406,40 +413,51 @@ private fun MediaTrackingItem( maxLines = 1, ) - Row(verticalAlignment = Alignment.CenterVertically) { - item.format?.let { - Text( - text = stringResource(it.res), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.labelSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + item.format?.let { + Text( + text = stringResource(it.res), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } - if (item.season != null && item.format != null) { - Divider(shape = MaterialShapes.Triangle.toShape()) + if (item.season != null && item.format != null) { + Divider(shape = MaterialShapes.Triangle.toShape()) + } + + item.season?.let { + Text( + text = stringResource(it.res) + + " ${item.seasonYear?.toString().orEmpty()}", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelSmall, + maxLines = 1 + ) + } } - item.season?.let { - Text( - text = stringResource(it.res) + - " ${item.seasonYear?.toString().orEmpty()}", - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.labelSmall, - maxLines = 1 - ) + item.score?.let { score -> + if (score.format == ScoreFormat.POINT_5) { + val filledStars = score.value.fastRoundToInt() + Row { + repeat(filledStars) { Star(filled = true) } + repeat(TOTAL_STARS - filledStars) { Star(filled = false) } + } + } } } } item.score?.let { score -> - Text( - text = score.value.toString(), - style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold), - color = Color(score.color).copy(alpha = 0.6f), - modifier = Modifier.align(Alignment.CenterVertically).padding(top = 5.dp) - ) + Score(score) } } @@ -498,3 +516,65 @@ private fun MediaTrackingItem( ) } } + +@Composable +fun RowScope.Score( + score: Media.Score, + modifier: Modifier = Modifier +) { + AnimatedContent( + targetState = score, + modifier = modifier.align(Alignment.CenterVertically) + ) { score -> + when (score.format) { + ScoreFormat.POINT_100, + ScoreFormat.POINT_10_DECIMAL, + ScoreFormat.POINT_10 -> { + Text( + text = when (score.format) { + ScoreFormat.POINT_100, + ScoreFormat.POINT_10 -> score.value.roundToInt() + + ScoreFormat.POINT_10_DECIMAL -> score.value + else -> "" + }.toString(), + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold), + color = Color(score.color).copy(alpha = 0.6f), + modifier = modifier.padding(top = 5.dp) + ) + } + + ScoreFormat.POINT_5 -> {} + ScoreFormat.POINT_3 -> { + Icon( + imageVector = ImageVector.vectorResource( + when (score.value.fastRoundToInt()) { + 0 -> R.drawable.dead + 1 -> R.drawable.weary + 2 -> R.drawable.neutral + else -> settingsR.drawable.smile + } + ), + contentDescription = null, + tint = Color(score.color).copy(alpha = 0.6f), + modifier = modifier.size(dimensionResource(R.dimen.smiley_icon_size)) + ) + } + + else -> {} + } + } +} + +@Composable +private fun Star( + filled: Boolean, + modifier: Modifier = Modifier +) { + Icon( + imageVector = ImageVector.vectorResource(settingsR.drawable.star), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary.copy(alpha = if (!filled) 0.2f else 1f), + modifier = modifier.size(dimensionResource(R.dimen.star_icon_size)) + ) +} diff --git a/profile/src/main/res/drawable/dead.xml b/profile/src/main/res/drawable/dead.xml new file mode 100644 index 00000000..ff201878 --- /dev/null +++ b/profile/src/main/res/drawable/dead.xml @@ -0,0 +1,24 @@ + + + + diff --git a/profile/src/main/res/drawable/neutral.xml b/profile/src/main/res/drawable/neutral.xml new file mode 100644 index 00000000..8038334f --- /dev/null +++ b/profile/src/main/res/drawable/neutral.xml @@ -0,0 +1,24 @@ + + + + diff --git a/profile/src/main/res/drawable/weary.xml b/profile/src/main/res/drawable/weary.xml new file mode 100644 index 00000000..89b2d18e --- /dev/null +++ b/profile/src/main/res/drawable/weary.xml @@ -0,0 +1,24 @@ + + + + diff --git a/profile/src/main/res/values/dimens.xml b/profile/src/main/res/values/dimens.xml index bff11c62..10837258 100644 --- a/profile/src/main/res/values/dimens.xml +++ b/profile/src/main/res/values/dimens.xml @@ -11,4 +11,6 @@ 80dp 12dp + 24dp + 14dp diff --git a/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt b/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt index 636668d5..c676c55c 100644 --- a/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt +++ b/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt @@ -92,6 +92,7 @@ import androidx.compose.ui.util.fastRoundToInt import androidx.core.graphics.drawable.toBitmap import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.imashnake.animite.api.anilist.sanitize.media.Media +import com.imashnake.animite.api.anilist.type.ScoreFormat import com.imashnake.animite.banner.BannerLayout import com.imashnake.animite.banner.MountFuji import com.imashnake.animite.core.ui.DayPart @@ -104,6 +105,7 @@ import com.imashnake.animite.core.ui.ext.horizontalOnly import com.imashnake.animite.core.ui.layout.TranslucentStatusBarLayout import com.imashnake.animite.core.ui.rememberDefaultPaddings import com.imashnake.animite.media.ext.res +import com.imashnake.animite.settings.ext.title import com.materialkolor.ktx.darken import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -151,6 +153,7 @@ fun SettingsPage( val useProfileColor by viewModel.useProfileColor.collectAsState(initial = true) val useExpressiveProgressIndicator by viewModel.useExpressiveProgressIndicator.collectAsState(initial = true) val profileColor by viewModel.profileColor.collectAsState(initial = "blue") + val scoreFormat by viewModel.scoreFormat.collectAsState(initial = ScoreFormat.POINT_10_DECIMAL.name) val isDevOptionsEnabled by viewModel.isDevOptionsEnabled.collectAsState(initial = false) @@ -569,6 +572,11 @@ fun SettingsPage( label = R.string.use_expressive_progress_indicator, orientation = Item.Orientation.HORIZONTAL ), + Item( + icon = R.drawable.review, + label = R.string.score_format, + orientation = Item.Orientation.VERTICAL + ), ), onItemClick = { index -> when (index) { @@ -762,6 +770,58 @@ fun SettingsPage( }, ) } + + 3 -> { + Row( + horizontalArrangement = Arrangement.spacedBy( + ButtonGroupDefaults.ConnectedSpaceBetween + ) + ) { + // TODO: Maybe sanitize. + ScoreFormat.entries.minus(ScoreFormat.UNKNOWN__).fastForEach { format -> + ToggleButton( + checked = scoreFormat == format.name, + onCheckedChange = { + viewModel.updateScoreFormat(format) + haptic.performHapticFeedback(HapticFeedbackType.SegmentTick) + }, + shapes = when (format) { + ScoreFormat.POINT_100 -> ButtonGroupDefaults.connectedLeadingButtonShapes() + ScoreFormat.POINT_3 -> ButtonGroupDefaults.connectedTrailingButtonShapes() + else -> ButtonGroupDefaults.connectedMiddleButtonShapes() + }, + colors = ToggleButtonDefaults.toggleButtonColors( + containerColor = MaterialTheme.colorScheme.background + ), + modifier = Modifier.weight(1f) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(LocalPaddings.current.tiny), + ) { + Text(format.title, maxLines = 1) + when(format) { + ScoreFormat.POINT_5 -> { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.star), + contentDescription = null, + modifier = Modifier.size(16.dp).padding(bottom = 1.dp), + ) + } + ScoreFormat.POINT_3 -> { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.smile), + contentDescription = null, + modifier = Modifier.size(16.dp).padding(bottom = 1.dp), + ) + } + else -> {} + } + } + } + } + } + } } } } diff --git a/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsViewModel.kt b/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsViewModel.kt index 16605a80..097ba299 100644 --- a/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsViewModel.kt +++ b/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.imashnake.animite.api.anilist.AnilistUserRepository import com.imashnake.animite.api.anilist.sanitize.media.Media +import com.imashnake.animite.api.anilist.type.ScoreFormat import com.imashnake.animite.api.preferences.PreferencesRepository import com.imashnake.animite.core.model.AnimeList import com.imashnake.animite.core.model.MangaList @@ -67,6 +68,7 @@ class SettingsViewModel @Inject constructor( val useProfileColor = preferencesRepository.useProfileColor.filterNotNull() val useExpressiveProgressIndicator = preferencesRepository.useExpressiveProgressIndicator.filterNotNull() val profileColor = preferencesRepository.profileColor.filterNotNull() + val scoreFormat = preferencesRepository.scoreFormat.filterNotNull() fun updateProfileColor(profileColor: String) = viewModelScope.launch(Dispatchers.IO) { userRepository.updateUser(profileColor = profileColor).collect { @@ -74,6 +76,12 @@ class SettingsViewModel @Inject constructor( } } + fun updateScoreFormat(scoreFormat: ScoreFormat) = viewModelScope.launch(Dispatchers.IO) { + userRepository.updateUser(scoreFormat = scoreFormat).collect { + preferencesRepository.setScoreFormat(it.getOrNull()?.mediaListOptions?.scoreFormat?.name) + } + } + fun setTheme(theme: Theme) = viewModelScope.launch(Dispatchers.IO) { preferencesRepository.setTheme(theme.name) } diff --git a/settings/src/main/kotlin/com/imashnake/animite/settings/ext/EnumExt.kt b/settings/src/main/kotlin/com/imashnake/animite/settings/ext/EnumExt.kt new file mode 100644 index 00000000..813cb55d --- /dev/null +++ b/settings/src/main/kotlin/com/imashnake/animite/settings/ext/EnumExt.kt @@ -0,0 +1,12 @@ +package com.imashnake.animite.settings.ext + +import com.imashnake.animite.api.anilist.type.ScoreFormat + +val ScoreFormat.title get() = when(this) { + ScoreFormat.POINT_100 -> "100" + ScoreFormat.POINT_10_DECIMAL -> "10.0" + ScoreFormat.POINT_10 -> "10" + ScoreFormat.POINT_5 -> "5" + ScoreFormat.POINT_3 -> "3" + ScoreFormat.UNKNOWN__ -> "" +} diff --git a/settings/src/main/res/drawable/review.xml b/settings/src/main/res/drawable/review.xml new file mode 100644 index 00000000..9e70bb6f --- /dev/null +++ b/settings/src/main/res/drawable/review.xml @@ -0,0 +1,24 @@ + + + + diff --git a/settings/src/main/res/drawable/smile.xml b/settings/src/main/res/drawable/smile.xml new file mode 100644 index 00000000..749c4e6e --- /dev/null +++ b/settings/src/main/res/drawable/smile.xml @@ -0,0 +1,24 @@ + + + + diff --git a/settings/src/main/res/drawable/star.xml b/settings/src/main/res/drawable/star.xml new file mode 100644 index 00000000..829cf879 --- /dev/null +++ b/settings/src/main/res/drawable/star.xml @@ -0,0 +1,24 @@ + + + + diff --git a/settings/src/main/res/values/strings.xml b/settings/src/main/res/values/strings.xml index 8a8c00d0..6366ab5c 100644 --- a/settings/src/main/res/values/strings.xml +++ b/settings/src/main/res/values/strings.xml @@ -25,6 +25,7 @@ Set profile color Use expressive progress indicator mlue + Score format About