diff --git a/api/anilist/src/main/graphql/fragments/MediaTracking.graphql b/api/anilist/src/main/graphql/fragments/MediaTracking.graphql index f56e7332..7b8a2e5a 100644 --- a/api/anilist/src/main/graphql/fragments/MediaTracking.graphql +++ b/api/anilist/src/main/graphql/fragments/MediaTracking.graphql @@ -1,8 +1,11 @@ fragment MediaTracking on Media { id + type coverImage { large + color } + bannerImage title { userPreferred romaji diff --git a/api/anilist/src/main/graphql/fragments/User.graphql b/api/anilist/src/main/graphql/fragments/User.graphql index 9b27d82e..315eae8f 100644 --- a/api/anilist/src/main/graphql/fragments/User.graphql +++ b/api/anilist/src/main/graphql/fragments/User.graphql @@ -21,17 +21,17 @@ fragment User on User { } } favourites { - anime(page: 0, perPage: 10) { + anime(page: 0, perPage: 25) { nodes { ...MediaSmall } } - manga(page: 0, perPage: 10) { + manga(page: 0, perPage: 25) { nodes { ...MediaSmall } } - characters(page: 0, perPage: 10) { + characters(page: 0, perPage: 25) { nodes { ...CharacterSmall } diff --git a/api/anilist/src/main/graphql/queries/UserQuery.graphql b/api/anilist/src/main/graphql/queries/UserQuery.graphql index bd9e3e1b..c39558e1 100644 --- a/api/anilist/src/main/graphql/queries/UserQuery.graphql +++ b/api/anilist/src/main/graphql/queries/UserQuery.graphql @@ -27,6 +27,7 @@ query UserMediaListQuery($userId: Int, $type: MediaType) { lists { name entries { + status progress score(format: POINT_10_DECIMAL) media { 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 874cf93a..2871489d 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 @@ -15,6 +15,7 @@ import com.imashnake.animite.api.anilist.sanitize.media.Media.Relation.Companion import com.imashnake.animite.api.anilist.sanitize.media.Media.Season.Companion.sanitize import com.imashnake.animite.api.anilist.sanitize.media.Media.Source.Companion.sanitize import com.imashnake.animite.api.anilist.sanitize.media.Media.Status.Companion.sanitize +import com.imashnake.animite.api.anilist.sanitize.profile.User.TrackingStatus import com.imashnake.animite.api.anilist.type.CharacterRole import com.imashnake.animite.api.anilist.type.MediaFormat import com.imashnake.animite.api.anilist.type.MediaRankType @@ -721,6 +722,8 @@ data class Media( data class Tracking( val id: Int, val coverImage: String?, + val bannerImage: String?, + val color: String?, val title: String?, val season: Season?, val seasonYear: Int?, @@ -728,15 +731,19 @@ data class Media( val segments: Int?, val progress: Int?, val score: Score?, + val status: TrackingStatus, ) { internal constructor( query: MediaTracking, progress: Int?, score: Float?, + status: TrackingStatus, language: Language ) : this( id = query.id, coverImage = query.coverImage?.large, + bannerImage = query.bannerImage, + color = query.coverImage?.color, title = when (language) { Language.DEFAULT -> query.title?.userPreferred Language.ROMAJI -> query.title?.romaji @@ -748,7 +755,8 @@ 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) }, + status = status ) } 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 7a8b193a..f1bcd746 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 @@ -7,7 +7,9 @@ import com.imashnake.animite.api.anilist.fragment.User import com.imashnake.animite.api.anilist.sanitize.media.Media import com.imashnake.animite.api.anilist.sanitize.media.Media.Language import com.imashnake.animite.api.anilist.sanitize.media.Media.Small.Type -import com.imashnake.animite.api.anilist.sanitize.profile.User.ListNames.Companion.sanitize +import com.imashnake.animite.api.anilist.sanitize.profile.User.TrackingStatus.Companion.sanitize +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 kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -89,7 +91,7 @@ data class User( @Immutable data class NamedTrackingList( val name: String?, - val existingListName: ListNames?, + val existingListName: TrackingStatus?, val list: ImmutableList, ) { internal constructor( @@ -103,7 +105,12 @@ data class User( query = it?.media?.mediaTracking ?: return@mapNotNull null, progress = it.progress, score = it.score?.toFloat(), - language = language + language = language, + status = it.status.toTrackingStatus( + type = it.media.mediaTracking.type?.name?.let { type -> + Type.valueOf(type) + } ?: Type.UNKNOWN + ) ) }.toImmutableList() ) @@ -212,31 +219,43 @@ data class User( Characters, } - enum class ListNames { + enum class TrackingStatus { // Anime lists WATCHING, COMPLETED, PAUSED, DROPPED, REWATCHING, - PLANNING, + PLAN_TO_WATCH, // Manga lists READING, REREADING, PLAN_TO_READ, + PLANNING, + CUSTOM_OR_UNKNOWN; companion object { - fun safeValueOf(rawValue: String?): ListNames = try { - ListNames.valueOf(rawValue?.uppercase().toString()) + fun safeValueOf(rawValue: String?): TrackingStatus = try { + TrackingStatus.valueOf(rawValue?.uppercase().toString()) } catch (e: IllegalArgumentException) { Log.e(EXCEPTION_TAG, "safeValueOf: $e; Format $rawValue not found.") CUSTOM_OR_UNKNOWN } fun String?.sanitize() = safeValueOf(this) + + fun MediaListStatus?.toTrackingStatus(type: Type): TrackingStatus = 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 + MediaListStatus.DROPPED -> DROPPED + MediaListStatus.PAUSED -> PAUSED + MediaListStatus.REPEATING -> if (type == Type.ANIME) REWATCHING else REREADING + else -> CUSTOM_OR_UNKNOWN + } } } diff --git a/app/src/main/kotlin/com/imashnake/animite/theme/Theme.kt b/app/src/main/kotlin/com/imashnake/animite/theme/Theme.kt index 68e661f2..f71d1df7 100644 --- a/app/src/main/kotlin/com/imashnake/animite/theme/Theme.kt +++ b/app/src/main/kotlin/com/imashnake/animite/theme/Theme.kt @@ -2,9 +2,6 @@ package com.imashnake.animite.theme import android.os.Build import androidx.compose.animation.core.tween -import androidx.compose.material.ripple.RippleAlpha -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.LocalRippleConfiguration import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MaterialTheme @@ -32,12 +29,7 @@ import com.materialkolor.ktx.animateColorScheme import com.materialkolor.rememberDynamicColorScheme import kotlinx.datetime.LocalTime import kotlin.math.floor -import kotlin.time.ExperimentalTime -@OptIn( - ExperimentalMaterial3Api::class, - ExperimentalMaterial3ExpressiveApi::class, ExperimentalTime::class, -) @Composable fun AnimiteTheme( paddings: Paddings = rememberDefaultPaddings(), @@ -92,15 +84,7 @@ fun AnimiteTheme( }.modify(useDarkTheme, isAmoled) } - val animiteRippleTheme = RippleConfiguration( - color = MaterialTheme.colorScheme.primary, - rippleAlpha = RippleAlpha( - draggedAlpha = 0.16f, - focusedAlpha = 0.12f, - hoveredAlpha = 0.08f, - pressedAlpha = 0.12f, - ) - ) + val animiteRippleTheme = RippleConfiguration(color = MaterialTheme.colorScheme.primary) MaterialExpressiveTheme( colorScheme = animateColorScheme(animiteColorScheme, animationSpec = { tween(500) }), diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileScreen.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileScreen.kt index e972a7ed..f259d716 100644 --- a/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileScreen.kt +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/ProfileScreen.kt @@ -101,6 +101,7 @@ import com.imashnake.animite.profile.tabs.AboutTab import com.imashnake.animite.profile.tabs.FavouritesTab import com.imashnake.animite.profile.tabs.MediaTab import com.imashnake.animite.profile.tabs.ProfileTab +import com.imashnake.animite.profile.ui.Login import com.imashnake.animite.settings.SettingsPage import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl import com.mikepenz.markdown.m3.Markdown diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/dev/EnumExt.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/dev/EnumExt.kt index b1c251f1..501a82a8 100644 --- a/profile/src/main/kotlin/com/imashnake/animite/profile/dev/EnumExt.kt +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/dev/EnumExt.kt @@ -1,21 +1,39 @@ package com.imashnake.animite.profile.dev -import com.imashnake.animite.api.anilist.sanitize.profile.User.ListNames +import com.imashnake.animite.api.anilist.sanitize.profile.User.TrackingStatus import com.imashnake.animite.profile.R import com.imashnake.animite.media.R as mediaR import com.imashnake.animite.navigation.R as navigationR -val ListNames.res get() = when(this) { - ListNames.WATCHING -> navigationR.drawable.anime - ListNames.COMPLETED -> mediaR.drawable.finished - ListNames.PAUSED -> mediaR.drawable.hiatus - ListNames.DROPPED -> mediaR.drawable.cancelled - ListNames.REWATCHING -> R.drawable.rewatch_list - ListNames.PLANNING -> mediaR.drawable.not_yet_released +val TrackingStatus.res get() = when(this) { + TrackingStatus.WATCHING -> navigationR.drawable.anime + TrackingStatus.COMPLETED -> mediaR.drawable.finished + TrackingStatus.PAUSED -> mediaR.drawable.hiatus + TrackingStatus.DROPPED -> mediaR.drawable.cancelled + TrackingStatus.REWATCHING -> R.drawable.rewatch_list + TrackingStatus.PLAN_TO_WATCH -> mediaR.drawable.not_yet_released - ListNames.READING -> R.drawable.reading_list - ListNames.REREADING -> R.drawable.rewatch_list - ListNames.PLAN_TO_READ -> mediaR.drawable.not_yet_released + TrackingStatus.READING -> R.drawable.reading_list + TrackingStatus.REREADING -> R.drawable.rewatch_list + TrackingStatus.PLAN_TO_READ -> mediaR.drawable.not_yet_released - ListNames.CUSTOM_OR_UNKNOWN -> R.drawable.custom_list + TrackingStatus.PLANNING -> mediaR.drawable.not_yet_released + + TrackingStatus.CUSTOM_OR_UNKNOWN -> R.drawable.custom_list +} + +val TrackingStatus.title get() = when(this) { + TrackingStatus.WATCHING -> R.string.watching + TrackingStatus.COMPLETED -> R.string.completed + TrackingStatus.PAUSED -> R.string.paused + TrackingStatus.DROPPED -> R.string.dropped + TrackingStatus.REWATCHING -> R.string.rewatching + TrackingStatus.PLAN_TO_WATCH -> R.string.plan_to_watch + + TrackingStatus.READING -> R.string.reading + TrackingStatus.REREADING -> R.string.rereading + TrackingStatus.PLAN_TO_READ -> R.string.plan_to_read + + TrackingStatus.PLANNING, + TrackingStatus.CUSTOM_OR_UNKNOWN -> R.string.unknown } diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/Login.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/ui/Login.kt similarity index 95% rename from profile/src/main/kotlin/com/imashnake/animite/profile/Login.kt rename to profile/src/main/kotlin/com/imashnake/animite/profile/ui/Login.kt index 32e5dce1..2e0291fe 100644 --- a/profile/src/main/kotlin/com/imashnake/animite/profile/Login.kt +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/ui/Login.kt @@ -1,4 +1,4 @@ -package com.imashnake.animite.profile +package com.imashnake.animite.profile.ui import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -17,6 +17,7 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.imashnake.animite.core.ui.LocalPaddings +import com.imashnake.animite.profile.R import com.imashnake.animite.profile.dev.ANILIST_AUTH_URL @Composable 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 1d9bc82a..b07116c5 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 @@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -58,7 +59,7 @@ import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.util.fastMapNotNull 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.ListNames.Companion.sanitize +import com.imashnake.animite.api.anilist.sanitize.profile.User.TrackingStatus.Companion.sanitize import com.imashnake.animite.core.ui.LocalPaddings import com.imashnake.animite.core.ui.component.Divider import com.imashnake.animite.core.ui.component.DropDownIcon @@ -358,13 +359,21 @@ private fun HeaderPill( @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun MediaTrackingItem( - listName: User.ListNames, + listName: User.TrackingStatus, item: Media.Tracking, useExpressiveProgressIndicator: Boolean, onClick: (Int, String?) -> Unit, modifier: Modifier = Modifier ) { - Row(modifier = modifier.clickable { onClick(item.id, item.title) }) { + var isUpdateEntryDialogVisible by remember { mutableStateOf(false) } + + Row( + modifier = modifier.combinedClickable( + onClick = { onClick(item.id, item.title) }, + // TODO: Add this after the point system is corrected. +// onLongClick = { isUpdateEntryDialogVisible = true } + ) + ) { MediaTrackingCard( image = item.coverImage, tag = null, @@ -439,7 +448,7 @@ private fun MediaTrackingItem( if (segments != null && progress != null) { val formattedProgress = progress - .takeUnless { listName == User.ListNames.COMPLETED } + .takeUnless { listName == User.TrackingStatus.COMPLETED } ?.let { "$it/$segments" } ?: "$segments" @@ -456,10 +465,10 @@ private fun MediaTrackingItem( ) when (listName) { - User.ListNames.WATCHING, - User.ListNames.REWATCHING, - User.ListNames.READING, - User.ListNames.REREADING -> if (useExpressiveProgressIndicator) { + User.TrackingStatus.WATCHING, + User.TrackingStatus.REWATCHING, + User.TrackingStatus.READING, + User.TrackingStatus.REREADING -> if (useExpressiveProgressIndicator) { LinearWavyProgressIndicator( progress = { progress.toFloat() / segments }, amplitude = { if (it <= 0.1f || it >= 0.95f) 0f else 0.5f }, @@ -481,4 +490,11 @@ private fun MediaTrackingItem( } } } + + if (isUpdateEntryDialogVisible) { + UpdateEntryDialog( + item = item, + onDismissRequest = { isUpdateEntryDialogVisible = false } + ) + } } diff --git a/profile/src/main/kotlin/com/imashnake/animite/profile/ui/UpdateEntryDialog.kt b/profile/src/main/kotlin/com/imashnake/animite/profile/ui/UpdateEntryDialog.kt new file mode 100644 index 00000000..2d90cf92 --- /dev/null +++ b/profile/src/main/kotlin/com/imashnake/animite/profile/ui/UpdateEntryDialog.kt @@ -0,0 +1,205 @@ +package com.imashnake.animite.profile.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialShapes +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.toShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.core.graphics.toColorInt +import coil3.compose.AsyncImage +import com.imashnake.animite.api.anilist.sanitize.media.Media +import com.imashnake.animite.api.anilist.sanitize.profile.User +import com.imashnake.animite.core.ui.LocalPaddings +import com.imashnake.animite.core.ui.component.Divider +import com.imashnake.animite.core.ui.component.DropDownIcon +import com.imashnake.animite.core.ui.component.MediaMediumCard +import com.imashnake.animite.core.ui.ext.crossfadeModel +import com.imashnake.animite.media.ext.res +import com.imashnake.animite.profile.R +import com.imashnake.animite.profile.dev.res +import com.imashnake.animite.profile.dev.title +import com.imashnake.animite.media.R as mediaR + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun UpdateEntryDialog( + item: Media.Tracking, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier +) { + Dialog(onDismissRequest = onDismissRequest) { + Card( + shape = RoundedCornerShape(18.dp + LocalPaddings.current.large), + modifier = modifier.fillMaxWidth() + ) { + // region header + Box { + Column { + // TODO: Reuse. + AsyncImage( + model = crossfadeModel(item.bannerImage), + contentDescription = null, + error = painterResource(mediaR.drawable.background), + fallback = painterResource(mediaR.drawable.background), + contentScale = ContentScale.Crop, + alignment = Alignment.Center, + colorFilter = ColorFilter.tint( + color = item.color?.toColorInt()?.let { Color(it) }?.copy(alpha = 0.25f) ?: Color.Transparent, + blendMode = BlendMode.SrcAtop + ), + modifier = Modifier.fillMaxWidth().aspectRatio(19f / 4) + ) + + Column( + verticalArrangement = Arrangement.spacedBy(LocalPaddings.current.tiny), + modifier = Modifier.padding( + start = LocalPaddings.current.large + 96.dp + LocalPaddings.current.medium, + top = LocalPaddings.current.medium, + end = LocalPaddings.current.medium + ) + ) { + Text( + text = item.title.orEmpty(), + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold), + maxLines = 2 + ) + + 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()) + } + + item.season?.let { + Text( + text = stringResource(it.res) + + " ${item.seasonYear?.toString().orEmpty()}", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelSmall, + maxLines = 1 + ) + } + } + } + } + + MediaMediumCard( + image = item.coverImage, + tag = null, + label = null, + onClick = {}, + tagMinLines = 1, + modifier = Modifier + .align(Alignment.BottomStart) + .padding( + top = LocalPaddings.current.large, + start = LocalPaddings.current.large + ) + ) + } + // endregion + + // region entry options + Column( + modifier = Modifier.padding( + horizontal = LocalPaddings.current.large, + vertical = LocalPaddings.current.medium) + ) { + Row(horizontalArrangement = Arrangement.spacedBy(LocalPaddings.current.large)) { + StatusDropDown(status = item.status) + + item.score?.let { score -> + Text( + text = score.value.toString(), + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold), + color = Color(score.color).copy(alpha = 0.8f), + modifier = Modifier.align(Alignment.CenterVertically).padding(top = 5.dp) + ) + } + } + } + // endregion + } + } +} + +// TODO: This should be an actual dropdown. +@Composable +fun StatusDropDown( + status: User.TrackingStatus, + modifier: Modifier = Modifier +) { + val iconPadding = + (dimensionResource(R.dimen.tracking_list_header_height) + - dimensionResource(R.dimen.tracking_list_header_icon_size)) / 2 + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(LocalPaddings.current.small), + modifier = modifier + .height(dimensionResource(R.dimen.tracking_list_header_height)) + .clip(CircleShape) + .clickable {} + .background( + MaterialTheme.colorScheme.surfaceContainerHigh.copy( + alpha = 0.95f + ) + ) + .padding(iconPadding) + ) { + Icon( + imageVector = ImageVector.vectorResource(status.res), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(dimensionResource(R.dimen.tracking_list_header_icon_size)) + ) + Text( + text = stringResource(status.title), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium.copy(baselineShift = null), + ) + DropDownIcon(isDroppedDown = false) + } +} + diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml index 18150abc..f0606697 100644 --- a/profile/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/strings.xml @@ -21,6 +21,17 @@ Genres + Watching + Completed + Paused + Dropped + Rewatching + Plan to watch + Reading + Rereading + Plan to read + Unknown + Expand All Collapse All Reorder 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 b18c45bd..636668d5 100644 --- a/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt +++ b/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsPage.kt @@ -131,6 +131,8 @@ fun SettingsPage( val scrollState = rememberScrollState() val haptic = LocalHapticFeedback.current + val isLoggedIn by viewModel.isLoggedIn.collectAsState(initial = false) + val selectedTheme by viewModel.theme.collectAsState(initial = Theme.DEVICE_THEME.name) val useSystemColorScheme by viewModel.useSystemColorScheme.collectAsState(initial = true) val isAmoled by viewModel.isAmoled.collectAsState(initial = false) @@ -541,202 +543,225 @@ fun SettingsPage( } } - Column(verticalArrangement = Arrangement.spacedBy(LocalPaddings.current.medium)) { - Text( - text = stringResource(R.string.profile_settings), - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.titleLarge, - overflow = TextOverflow.Ellipsis, - ) + if (isLoggedIn) { + Column(verticalArrangement = Arrangement.spacedBy(LocalPaddings.current.medium)) { + Text( + text = stringResource(R.string.profile_settings), + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.titleLarge, + overflow = TextOverflow.Ellipsis, + ) - Items( - items = persistentListOf( - Item( - icon = R.drawable.user_description, - label = R.string.show_description, - orientation = Item.Orientation.HORIZONTAL - ), - Item( - icon = R.drawable.palette, - label = R.string.profile_color, - orientation = Item.Orientation.HORIZONTAL - ), - Item( - icon = R.drawable.animation, - label = R.string.use_expressive_progress_indicator, - orientation = Item.Orientation.HORIZONTAL + Items( + items = persistentListOf( + Item( + icon = R.drawable.user_description, + label = R.string.show_description, + orientation = Item.Orientation.HORIZONTAL + ), + Item( + icon = R.drawable.palette, + label = R.string.profile_color, + orientation = Item.Orientation.HORIZONTAL + ), + Item( + icon = R.drawable.animation, + label = R.string.use_expressive_progress_indicator, + orientation = Item.Orientation.HORIZONTAL + ), ), - ), - onItemClick = { index -> - when (index) { - 0 -> { - viewModel.setShowUserDescription(!showUserDescription) - haptic.performHapticFeedback( - hapticFeedbackType = if (showUserDescription) { - HapticFeedbackType.ToggleOff - } else HapticFeedbackType.ToggleOn - ) - } - 2 -> { - viewModel.setUseExpressiveProgressIndicator(!useExpressiveProgressIndicator) - haptic.performHapticFeedback( - hapticFeedbackType = if (useExpressiveProgressIndicator) { - HapticFeedbackType.ToggleOff - } else HapticFeedbackType.ToggleOn - ) + onItemClick = { index -> + when (index) { + 0 -> { + viewModel.setShowUserDescription(!showUserDescription) + haptic.performHapticFeedback( + hapticFeedbackType = if (showUserDescription) { + HapticFeedbackType.ToggleOff + } else HapticFeedbackType.ToggleOn + ) + } + + 2 -> { + viewModel.setUseExpressiveProgressIndicator(!useExpressiveProgressIndicator) + haptic.performHapticFeedback( + hapticFeedbackType = if (useExpressiveProgressIndicator) { + HapticFeedbackType.ToggleOff + } else HapticFeedbackType.ToggleOn + ) + } } - } - }, - onItemLongClick = {}, - itemSubContent = { - when (it) { - 1 -> { - Row( - horizontalArrangement = Arrangement.spacedBy( - ButtonGroupDefaults.ConnectedSpaceBetween - ), - modifier = Modifier.fillMaxWidth() - ) { - ProfileColor.entries.fastForEachIndexed { index, entry -> - ToggleButton( - checked = profileColor.equals(entry.name, ignoreCase = true) && useProfileColor, - onCheckedChange = { - viewModel.updateProfileColor(entry.name.lowercase()) - haptic.performHapticFeedback(HapticFeedbackType.SegmentTick) - }, - shapes = when (index) { - 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes() - ProfileColor.entries.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes() - else -> ButtonGroupDefaults.connectedMiddleButtonShapes() - }, - enabled = useProfileColor, - colors = ToggleButtonDefaults.toggleButtonColors( - containerColor = entry.color, - contentColor = entry.color.darken(4f), - checkedContainerColor = entry.color, - checkedContentColor = entry.color.darken(4f), - ), - modifier = Modifier.weight(1f) - ) { - if (profileColor.equals(entry.name, ignoreCase = true) && useProfileColor) { - entry.label?.let { id -> - Text( - text = stringResource(id), - fontSize = 7.sp, - maxLines = 1 + }, + onItemLongClick = {}, + itemSubContent = { + when (it) { + 1 -> { + Row( + horizontalArrangement = Arrangement.spacedBy( + ButtonGroupDefaults.ConnectedSpaceBetween + ), + modifier = Modifier.fillMaxWidth() + ) { + ProfileColor.entries.fastForEachIndexed { index, entry -> + ToggleButton( + checked = profileColor.equals( + entry.name, + ignoreCase = true + ) && useProfileColor, + onCheckedChange = { + viewModel.updateProfileColor(entry.name.lowercase()) + haptic.performHapticFeedback( + HapticFeedbackType.SegmentTick ) - } ?: Icon( - imageVector = Icons.Rounded.Check, - contentDescription = null, - modifier = Modifier.size(SwitchDefaults.IconSize), - ) + }, + shapes = when (index) { + 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes() + ProfileColor.entries.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes() + else -> ButtonGroupDefaults.connectedMiddleButtonShapes() + }, + enabled = useProfileColor, + colors = ToggleButtonDefaults.toggleButtonColors( + containerColor = entry.color, + contentColor = entry.color.darken(4f), + checkedContainerColor = entry.color, + checkedContentColor = entry.color.darken( + 4f + ), + ), + modifier = Modifier.weight(1f) + ) { + if (profileColor.equals( + entry.name, + ignoreCase = true + ) && useProfileColor + ) { + entry.label?.let { id -> + Text( + text = stringResource(id), + fontSize = 7.sp, + maxLines = 1 + ) + } ?: Icon( + imageVector = Icons.Rounded.Check, + contentDescription = null, + modifier = Modifier.size( + SwitchDefaults.IconSize + ), + ) + } } } } } - } - 2 -> { - val amplitude by animateFloatAsState( - if (useExpressiveProgressIndicator) 0.5f else 0f - ) - val progress = 67 - Row( - horizontalArrangement = Arrangement.spacedBy(LocalPaddings.current.small), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "$progress/100", - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f), - style = MaterialTheme.typography.labelSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis + 2 -> { + val amplitude by animateFloatAsState( + if (useExpressiveProgressIndicator) 0.5f else 0f ) - key(amplitude) { - LinearWavyProgressIndicator( - progress = { progress.toFloat() / 100 }, - amplitude = { amplitude }, - waveSpeed = 15.dp, - modifier = Modifier - .fillMaxWidth() - .graphicsLayer { alpha = 0.6f } + val progress = 67 + + Row( + horizontalArrangement = Arrangement.spacedBy( + LocalPaddings.current.small + ), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "$progress/100", + color = MaterialTheme.colorScheme.primary.copy( + alpha = 0.7f + ), + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) + key(amplitude) { + LinearWavyProgressIndicator( + progress = { progress.toFloat() / 100 }, + amplitude = { amplitude }, + waveSpeed = 15.dp, + modifier = Modifier + .fillMaxWidth() + .graphicsLayer { alpha = 0.6f } + ) + } } } } - } - }, - isDarkMode = isDarkMode, - ) { index -> - when (index) { - 0 -> { - Switch( - checked = showUserDescription, - onCheckedChange = { - viewModel.setShowUserDescription(it) - haptic.performHapticFeedback( - hapticFeedbackType = if (!it) { - HapticFeedbackType.ToggleOff - } else HapticFeedbackType.ToggleOn - ) - }, - thumbContent = { - if (showUserDescription) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.eye), - contentDescription = null, - modifier = Modifier.size(SwitchDefaults.IconSize), - tint = MaterialTheme.colorScheme.primary + }, + isDarkMode = isDarkMode, + ) { index -> + when (index) { + 0 -> { + Switch( + checked = showUserDescription, + onCheckedChange = { + viewModel.setShowUserDescription(it) + haptic.performHapticFeedback( + hapticFeedbackType = if (!it) { + HapticFeedbackType.ToggleOff + } else HapticFeedbackType.ToggleOn ) - } - }, - ) - } - 1 -> { - Switch( - checked = useProfileColor, - onCheckedChange = { - viewModel.setUseProfileColor(it) - haptic.performHapticFeedback( - hapticFeedbackType = if (!it) { - HapticFeedbackType.ToggleOff - } else HapticFeedbackType.ToggleOn - ) - }, - thumbContent = { - if (useProfileColor) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.fill_bucket), - contentDescription = null, - modifier = Modifier.size(SwitchDefaults.IconSize), - tint = MaterialTheme.colorScheme.primary + }, + thumbContent = { + if (showUserDescription) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.eye), + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + tint = MaterialTheme.colorScheme.primary + ) + } + }, + ) + } + + 1 -> { + Switch( + checked = useProfileColor, + onCheckedChange = { + viewModel.setUseProfileColor(it) + haptic.performHapticFeedback( + hapticFeedbackType = if (!it) { + HapticFeedbackType.ToggleOff + } else HapticFeedbackType.ToggleOn ) - } - }, - ) - } - 2 -> { - Switch( - checked = useExpressiveProgressIndicator, - onCheckedChange = { - viewModel.setUseExpressiveProgressIndicator(!useExpressiveProgressIndicator) - haptic.performHapticFeedback( - hapticFeedbackType = if (useExpressiveProgressIndicator) { - HapticFeedbackType.ToggleOff - } else HapticFeedbackType.ToggleOn - ) - }, - thumbContent = { - if (useExpressiveProgressIndicator) { - Icon( - imageVector = Icons.Rounded.Check, - contentDescription = null, - modifier = Modifier.size(SwitchDefaults.IconSize), - tint = MaterialTheme.colorScheme.primary + }, + thumbContent = { + if (useProfileColor) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.fill_bucket), + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + tint = MaterialTheme.colorScheme.primary + ) + } + }, + ) + } + + 2 -> { + Switch( + checked = useExpressiveProgressIndicator, + onCheckedChange = { + viewModel.setUseExpressiveProgressIndicator(!useExpressiveProgressIndicator) + haptic.performHapticFeedback( + hapticFeedbackType = if (useExpressiveProgressIndicator) { + HapticFeedbackType.ToggleOff + } else HapticFeedbackType.ToggleOn ) - } - }, - ) + }, + thumbContent = { + if (useExpressiveProgressIndicator) { + Icon( + imageVector = Icons.Rounded.Check, + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + tint = MaterialTheme.colorScheme.primary + ) + } + }, + ) + } } } } 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 6d15b02b..16605a80 100644 --- a/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsViewModel.kt +++ b/settings/src/main/kotlin/com/imashnake/animite/settings/SettingsViewModel.kt @@ -23,6 +23,11 @@ class SettingsViewModel @Inject constructor( private val userRepository: AnilistUserRepository, private val preferencesRepository: PreferencesRepository, ) : ViewModel() { + + val isLoggedIn = preferencesRepository + .accessToken + .map { !it.isNullOrEmpty() } + val theme = preferencesRepository.theme.filterNotNull() val useSystemColorScheme = preferencesRepository.useSystemColorScheme.filterNotNull() val isAmoled = preferencesRepository.isAmoled.filterNotNull()