From a170d57b71d01fde46874a8eed00869acb2843e0 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Sat, 13 Jul 2024 20:08:12 +0530 Subject: [PATCH 1/5] Updated charting library --- .../android/ui/components/YimGraph.kt | 104 +++++++++--------- 1 file changed, 55 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/components/YimGraph.kt b/app/src/main/java/org/listenbrainz/android/ui/components/YimGraph.kt index fb3ce81e..28b2d72f 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/components/YimGraph.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/components/YimGraph.kt @@ -6,73 +6,79 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.patrykandpatrick.vico.compose.axis.horizontal.rememberBottomAxis -import com.patrykandpatrick.vico.compose.axis.vertical.rememberStartAxis -import com.patrykandpatrick.vico.compose.chart.Chart -import com.patrykandpatrick.vico.compose.chart.column.columnChart -import com.patrykandpatrick.vico.core.component.shape.LineComponent -import com.patrykandpatrick.vico.core.component.text.textComponent -import com.patrykandpatrick.vico.core.entry.ChartEntry -import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer -import com.patrykandpatrick.vico.core.entry.entryOf +import androidx.compose.ui.unit.sp +import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottomAxis +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberStartAxis +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberColumnCartesianLayer +import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart +import com.patrykandpatrick.vico.compose.common.component.rememberLineComponent +import com.patrykandpatrick.vico.compose.common.component.rememberTextComponent +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer +import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter +import com.patrykandpatrick.vico.core.cartesian.data.columnSeries +import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianLayer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext @Composable fun YimGraph (yearListens : List>) { - val chartEntries: MutableList = remember { mutableListOf() } - - for (i in 1..yearListens.size) { - chartEntries.add( - entryOf( - yearListens[i - 1].first.toInt().toFloat(), - yearListens[i - 1].second - ) - ) + val modelProducer = remember { + CartesianChartModelProducer() } - val chartEntryModelProducer = ChartEntryModelProducer(chartEntries) + LaunchedEffect(Unit) { + withContext(Dispatchers.Default) { + while(isActive){ + modelProducer.runTransaction { columnSeries { + series(x = yearListens.map { it.first.toInt() }, y = yearListens.map { it.second }) + } } + } + } + } - Chart( + CartesianChartHost( modifier = Modifier .padding(start = 11.dp, end = 11.dp) .height(250.dp) .clip(RoundedCornerShape(10.dp)) .background(Color(0xFFe0e5de)), - chart = columnChart( - spacing = 1.dp, - columns = List(yearListens.size) { - LineComponent( - color = 0xFFe36b3c.toInt(), - thicknessDp = 25f, - ) - }, - ), - chartModelProducer = chartEntryModelProducer, - startAxis = rememberStartAxis( - valueFormatter = { value, _ -> - value.toInt().toString() - } - ), - bottomAxis = rememberBottomAxis( - label = textComponent { - this.ellipsize = TextUtils.TruncateAt.MARQUEE - this.textSizeSp = 11f - }, - guideline = null, - valueFormatter = { value, _ -> - if (value.toInt() % 5 == 0) { - value.toInt().toString() - } else { - "" + chart = rememberCartesianChart( + rememberColumnCartesianLayer( + ColumnCartesianLayer.ColumnProvider.series( + rememberLineComponent( + color = Color(0xFFe36b3c), + thickness = 25.dp, + ) + ), + spacing = 1.dp + ), + startAxis = rememberStartAxis(), + bottomAxis = rememberBottomAxis( + label = rememberTextComponent ( + ellipsize = TextUtils.TruncateAt.MARQUEE, + textSize = 11.sp + ), + guideline = null, + valueFormatter = CartesianValueFormatter { value, chartValues, verticalAxisPosition -> + if(value.toInt() % 5 == 0){ + value.toString() + } + else{ + "" + } } - }, + ), ), + modelProducer = modelProducer + ) - - ) } \ No newline at end of file From a56573efcd4262cedd59528cd06068f831a8b502 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Mon, 15 Jul 2024 22:22:32 +0530 Subject: [PATCH 2/5] Initial setup for Stats Tab --- app/build.gradle | 2 +- .../listenbrainz/android/model/user/Artist.kt | 8 +- .../android/model/user/ListeningActivity.kt | 12 + .../model/user/ListeningActivityPayload.kt | 12 + .../android/model/user/TopArtistsPayload.kt | 20 +- .../model/user/UserListeningActivity.kt | 5 + .../android/repository/user/UserRepository.kt | 4 +- .../repository/user/UserRepositoryImpl.kt | 15 +- .../android/service/UserService.kt | 8 +- .../ui/screens/profile/BaseProfileScreen.kt | 2 +- .../ui/screens/profile/ProfileUiState.kt | 5 + .../ui/screens/profile/stats/CategoryEnum.kt | 5 + .../ui/screens/profile/stats/StatsRange.kt | 11 + .../ui/screens/profile/stats/StatsScreen.kt | 408 +++++++++++++++++- .../ui/screens/profile/stats/UserGlobal.kt | 6 + .../android/viewmodel/ProfileViewModel.kt | 127 +++++- app/src/main/res/drawable/globe.xml | 9 + 17 files changed, 634 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/ListeningActivity.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/ListeningActivityPayload.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/UserListeningActivity.kt create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/CategoryEnum.kt create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsRange.kt create mode 100644 app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/UserGlobal.kt create mode 100644 app/src/main/res/drawable/globe.xml diff --git a/app/build.gradle b/app/build.gradle index 28e48125..6a22795b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -259,5 +259,5 @@ dependencies { implementation 'com.github.akshaaatt:Logger-Android:1.0.0' //Charting Library (Vico) - implementation('com.patrykandpatrick.vico:compose:1.15.0') + implementation('com.patrykandpatrick.vico:compose:2.0.0-alpha.22') } diff --git a/app/src/main/java/org/listenbrainz/android/model/user/Artist.kt b/app/src/main/java/org/listenbrainz/android/model/user/Artist.kt index 131d879f..ee723710 100644 --- a/app/src/main/java/org/listenbrainz/android/model/user/Artist.kt +++ b/app/src/main/java/org/listenbrainz/android/model/user/Artist.kt @@ -1,7 +1,9 @@ package org.listenbrainz.android.model.user +import com.google.gson.annotations.SerializedName + data class Artist( - val artist_mbid: String, - val artist_name: String, - val listen_count: Int + @SerializedName("artist_mbid") val artistMbid: String, + @SerializedName("artist_name") val artistName: String, + @SerializedName("listen_count") val listenCount: Int ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/user/ListeningActivity.kt b/app/src/main/java/org/listenbrainz/android/model/user/ListeningActivity.kt new file mode 100644 index 00000000..0026573b --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/ListeningActivity.kt @@ -0,0 +1,12 @@ +package org.listenbrainz.android.model.user + +import com.google.gson.annotations.SerializedName + +data class ListeningActivity( + @SerializedName("from_ts") val fromTs: Int? = null, + @SerializedName("listen_count") val listenCount: Int? = null, + @SerializedName("time_range") val timeRange: String? = null, + @SerializedName("to_ts") val toTs: Int? = null, + var componentIndex: Int? = null, + var color: Int? = null, +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/user/ListeningActivityPayload.kt b/app/src/main/java/org/listenbrainz/android/model/user/ListeningActivityPayload.kt new file mode 100644 index 00000000..87e8ea2d --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/ListeningActivityPayload.kt @@ -0,0 +1,12 @@ +package org.listenbrainz.android.model.user + +import com.google.gson.annotations.SerializedName + +data class ListeningActivityPayload( + @SerializedName("from_ts") val fromTs: Int? = null, + @SerializedName("last_updated") val lastUpdated: Int? = null, + @SerializedName("listening_activity") val listeningActivity: List? = null, + val range: String? = null, + @SerializedName("to_ts") val toTs: Int? = null, + @SerializedName("user_id") val userId: String? = null, +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/user/TopArtistsPayload.kt b/app/src/main/java/org/listenbrainz/android/model/user/TopArtistsPayload.kt index bd67cc7a..ea30cee9 100644 --- a/app/src/main/java/org/listenbrainz/android/model/user/TopArtistsPayload.kt +++ b/app/src/main/java/org/listenbrainz/android/model/user/TopArtistsPayload.kt @@ -1,13 +1,15 @@ package org.listenbrainz.android.model.user +import com.google.gson.annotations.SerializedName + data class TopArtistsPayload( - val artists: List, - val count: Int, - val from_ts: Int, - val last_updated: Int, - val offset: Int, - val range: String, - val to_ts: Int, - val total_artist_count: Int, - val user_id: String + val artists: List, + val count: Int, + @SerializedName("from_ts") val fromTs: Int, + @SerializedName("last_updated") val lastUpdated: Int, + val offset: Int, + val range: String, + @SerializedName("to_ts") val toTs: Int, + @SerializedName("total_artist_count") val totalArtistCount: Int, + @SerializedName("user_id") val userId: String ) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/user/UserListeningActivity.kt b/app/src/main/java/org/listenbrainz/android/model/user/UserListeningActivity.kt new file mode 100644 index 00000000..8067f4ee --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/UserListeningActivity.kt @@ -0,0 +1,5 @@ +package org.listenbrainz.android.model.user + +data class UserListeningActivity( + val payload: ListeningActivityPayload? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/user/UserRepository.kt b/app/src/main/java/org/listenbrainz/android/repository/user/UserRepository.kt index 70ab20ad..5ee2bb84 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/user/UserRepository.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/user/UserRepository.kt @@ -5,6 +5,7 @@ import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.user.AllPinnedRecordings import org.listenbrainz.android.model.user.TopArtists import org.listenbrainz.android.model.user.UserFeedback +import org.listenbrainz.android.model.user.UserListeningActivity import org.listenbrainz.android.model.user.UserSimilarityPayload import org.listenbrainz.android.util.Resource @@ -14,6 +15,7 @@ interface UserRepository { suspend fun fetchUserCurrentPins(username: String?) : Resource suspend fun fetchUserPins(username: String?) : Resource //TODO: Move to artists VM once implemented - suspend fun getTopArtists(username: String?): Resource + suspend fun getTopArtists(username: String?, rangeString: String = "all_time", count: Int = 25): Resource suspend fun getUserFeedback(username: String?, score: Int?): Resource + suspend fun getUserListeningActivity(username: String?, rangeString: String = "all_time"): Resource } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/user/UserRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/user/UserRepositoryImpl.kt index 27d01bd4..c2a3a487 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/user/UserRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/user/UserRepositoryImpl.kt @@ -6,6 +6,7 @@ import org.listenbrainz.android.model.ResponseError import org.listenbrainz.android.model.user.AllPinnedRecordings import org.listenbrainz.android.model.user.TopArtists import org.listenbrainz.android.model.user.UserFeedback +import org.listenbrainz.android.model.user.UserListeningActivity import org.listenbrainz.android.model.user.UserSimilarityPayload import org.listenbrainz.android.service.UserService import org.listenbrainz.android.util.Resource @@ -37,10 +38,12 @@ class UserRepositoryImpl @Inject constructor( } override suspend fun getTopArtists( - username: String? + username: String?, + rangeString: String, + count: Int ): Resource = parseResponse { if(username.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() - service.getTopArtistsOfUser(username) + service.getTopArtistsOfUser(username, rangeString, count) } override suspend fun getUserFeedback(username: String?, score: Int?): Resource = parseResponse { @@ -48,5 +51,13 @@ class UserRepositoryImpl @Inject constructor( service.getUserFeedback(username, score) } + override suspend fun getUserListeningActivity( + username: String?, + rangeString: String + ): Resource = parseResponse { + if(username.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() + service.getUserListeningActivity(username,rangeString) + } + } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/service/UserService.kt b/app/src/main/java/org/listenbrainz/android/service/UserService.kt index 17364a08..36dfe321 100644 --- a/app/src/main/java/org/listenbrainz/android/service/UserService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/UserService.kt @@ -5,6 +5,7 @@ import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.user.AllPinnedRecordings import org.listenbrainz.android.model.user.TopArtists import org.listenbrainz.android.model.user.UserFeedback +import org.listenbrainz.android.model.user.UserListeningActivity import org.listenbrainz.android.model.user.UserSimilarityPayload import retrofit2.Response import retrofit2.http.GET @@ -24,9 +25,12 @@ interface UserService { @GET("{user_name}/pins") suspend fun getUserPins(@Path("user_name") username: String?) : Response - @GET("stats/user/{user_name}/artists?count=100") - suspend fun getTopArtistsOfUser(@Path("user_name") username: String?) : Response + @GET("stats/user/{user_name}/artists") + suspend fun getTopArtistsOfUser(@Path("user_name") username: String?, @Query("range") rangeString: String?, @Query("count") count: Int = 25) : Response @GET("feedback/user/{user_name}/get-feedback?metadata=true") suspend fun getUserFeedback(@Path("user_name") username: String?, @Query("score") score: Int?) : Response + + @GET("stats/user/{user_name}/listening-activity") + suspend fun getUserListeningActivity(@Path("user_name") username: String?, @Query("range") rangeString: String?): Response } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/BaseProfileScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/BaseProfileScreen.kt index 2cc83352..669f4f27 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/BaseProfileScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/BaseProfileScreen.kt @@ -207,7 +207,7 @@ fun BaseProfileScreen( username = username ) ProfileScreenTab.STATS -> StatsScreen( - + username = username ) ProfileScreenTab.TASTE -> TasteScreen( snackbarState = snackbarState, diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileUiState.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileUiState.kt index aa18aafd..87291fb4 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileUiState.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileUiState.kt @@ -6,8 +6,11 @@ import org.listenbrainz.android.model.ListenBitmap import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.SimilarUser import org.listenbrainz.android.model.user.AllPinnedRecordings +import org.listenbrainz.android.model.user.ListeningActivity import org.listenbrainz.android.model.user.UserFeedback import org.listenbrainz.android.ui.screens.profile.listens.ListeningNowUiState +import org.listenbrainz.android.ui.screens.profile.stats.StatsRange +import org.listenbrainz.android.ui.screens.profile.stats.UserGlobal data class ProfileUiState( val isSelf: Boolean = false, @@ -41,6 +44,8 @@ data class TasteTabUIState ( data class StatsTabUIState( val isLoading: Boolean = true, + val userListeningActivity: Map, List> = mapOf(), + val sortedListeningActivity: List? = listOf() ) data class ListeningNowUiState( diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/CategoryEnum.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/CategoryEnum.kt new file mode 100644 index 00000000..a69fc178 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/CategoryEnum.kt @@ -0,0 +1,5 @@ +enum class CategoryState { + ARTISTS, + ALBUMS, + SONGS +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsRange.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsRange.kt new file mode 100644 index 00000000..b0c4c53e --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsRange.kt @@ -0,0 +1,11 @@ +package org.listenbrainz.android.ui.screens.profile.stats + +enum class StatsRange (val rangeString: String, val apiIdenfier: String){ + THIS_WEEK("This Week", "this_week"), + THIS_MONTH("This Month", "this_month"), + THIS_YEAR("This Year", "this_year"), + LAST_WEEK("Tast Week", "week"), + LAST_MONTH("Last Month", "month"), + LAST_YEAR("Last Year", "year"), + ALL_TIME("All Time", "all_time"), +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt index b9e8677c..3091c793 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt @@ -1,17 +1,421 @@ package org.listenbrainz.android.ui.screens.profile.stats +import CategoryState +import android.text.TextUtils +import android.util.Log +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ElevatedSuggestionChip +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SuggestionChipDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottomAxis +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberStartAxis +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberColumnCartesianLayer +import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart +import com.patrykandpatrick.vico.compose.common.component.rememberTextComponent +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer +import com.patrykandpatrick.vico.core.cartesian.data.columnSeries +import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianLayer +import com.patrykandpatrick.vico.core.common.component.LineComponent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext +import org.listenbrainz.android.R +import org.listenbrainz.android.ui.screens.profile.ProfileUiState +import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.ui.theme.lb_purple_night import org.listenbrainz.android.viewmodel.ProfileViewModel @Composable fun StatsScreen( + username: String?, viewModel: ProfileViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsState() - Text(text = uiState.listensTabUiState.listenCount.toString(), color = Color.White) -} \ No newline at end of file + val statsRangeState: MutableState = remember { + mutableStateOf(StatsRange.THIS_WEEK) + } + val userGlobalState: MutableState = remember { + mutableStateOf(UserGlobal.USER) + } + + StatsScreen( + username = username, + uiState = uiState, + statsRangeState = statsRangeState.value, + setStatsRange = { + range -> statsRangeState.value = range + }, + userGlobalState = userGlobalState.value, + setUserGlobal = { + selection -> userGlobalState.value = selection + } + ) +} + + +fun weekFormatter ( + value: Int +): String { + val weekDay = when(value){ + 0 -> "Mon" + 1 -> "Tue" + 2 -> "Wed" + 3 -> "Thu" + 4 -> "Fri" + 5 -> "Sat" + 6 -> "Sun" + else -> "" + } + return weekDay +} + +fun yearFormatter( + value: Int +): String { + val month = when(value){ + 0 -> "Jan" + 1 -> "Feb" + 2 -> "Mar" + 3 -> "Apr" + 4 -> "May" + 5 -> "Jun" + 6 -> "Jul" + 7 -> "Aug" + 8 -> "Sep" + 9 -> "Oct" + 10 -> "Nov" + 11 -> "Dec" + else -> "" + } + return month +} + + +@Composable +fun StatsScreen( + username: String?, + uiState: ProfileUiState, + statsRangeState: StatsRange, + setStatsRange: (StatsRange) -> Unit, + userGlobalState: UserGlobal, + setUserGlobal: (UserGlobal) -> Unit, +) { + + val currentTabSelection: MutableState = remember { + mutableStateOf(CategoryState.ARTISTS) + } + + LazyColumn { + item { + RangeBar( + statsRangeState = statsRangeState, + onClick = { + range -> + setStatsRange(range) + } + ) + } + item { + UserGlobalBar( + userGlobalState = userGlobalState, + onUserGlobalChange = setUserGlobal, + username = username + ) + } + item { + Spacer(modifier = Modifier.height(10.dp)) + Box(modifier = Modifier.padding(start = 10.dp)){ + Column { + Text("Listening activity", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Spacer(modifier = Modifier.height(15.dp)) + val data = uiState.statsTabUIState.userListeningActivity[Pair(userGlobalState, statsRangeState)] + ?: listOf() + if(data.isNotEmpty()){ + val modelProducer = remember { + CartesianChartModelProducer() + } + + LaunchedEffect(data) { + withContext(Dispatchers.Default) { + while(isActive){ + modelProducer.runTransaction { columnSeries { + series(y = data.map { (it?.listenCount ?: 0).toInt() }) + } } + } + } + } + + val columnProvider = { + val columnAttributes = + data.map { + LineComponent( + color = it?.color ?: Color.Unspecified.toArgb(), + thicknessDp = 25f, + ) + } + data.forEach{ + Log.v("pranav", it?.timeRange.toString()) + Log.v("pranav", it?.componentIndex.toString()) + Log.v("pranav", it?.color.toString()) + } + ColumnCartesianLayer.ColumnProvider.series( + columns = columnAttributes + ) + } + + CartesianChartHost( + modifier = Modifier + .padding(start = 11.dp, end = 11.dp) + .height(250.dp) + .clip(RoundedCornerShape(10.dp)) + .background(Color(0xFFe0e5de)), + chart = rememberCartesianChart( + rememberColumnCartesianLayer( + columnProvider = columnProvider(), + spacing = 25.dp, + mergeMode = { ColumnCartesianLayer.MergeMode.Grouped }, + ), + startAxis = rememberStartAxis(), + bottomAxis = rememberBottomAxis( + label = rememberTextComponent ( + ellipsize = TextUtils.TruncateAt.MARQUEE, + textSize = 11.sp + ), + guideline = null, + valueFormatter = { value, chartValues, verticalAxisPosition -> + if(value.toInt() % 5 == 0){ + value.toString() + } + else{ + "" + } + } + ), + ), + modelProducer = modelProducer + ) + + } + else{ + Text("There are no statistics available for this user for this period", color = ListenBrainzTheme.colorScheme.textColor) + } + + } + } + } + + item { + Spacer(modifier = Modifier.height(10.dp)) + Column (modifier = Modifier + .padding(start = 10.dp) + .background( + ListenBrainzTheme.colorScheme.userPageGradient + )) { + Text("Top ...", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Row { + repeat(3){ + position -> + val reqdState = when(position){ + 0 -> currentTabSelection.value == CategoryState.ARTISTS + 1 -> currentTabSelection.value == CategoryState.ALBUMS + 2 -> currentTabSelection.value == CategoryState.SONGS + else -> true + } + val label = when(position){ + 0 -> "Artists" + 1 -> "Albums" + 2 -> "Songs" + else -> "" + } + ElevatedSuggestionChip( + onClick = { + when(position){ + 0 -> currentTabSelection.value = CategoryState.ARTISTS + 1 -> currentTabSelection.value = CategoryState.ALBUMS + 2 -> currentTabSelection.value = CategoryState.SONGS + } + }, + label = { + Text(label, color = when(reqdState){ + true -> ListenBrainzTheme.colorScheme.followerChipUnselected + false -> ListenBrainzTheme.colorScheme.followerChipSelected + }, style = ListenBrainzTheme.textStyles.chips) + }, + shape = RoundedCornerShape(12.dp), + border = when(reqdState){ + true -> null + false -> BorderStroke(1.dp, lb_purple_night) + }, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + if (reqdState) { + ListenBrainzTheme.colorScheme.followerChipSelected + } else { + ListenBrainzTheme.colorScheme.followerChipUnselected + } + ), + ) + Spacer(modifier = Modifier.width(10.dp)) + } + } + } + + } + + } +} + +@Composable +private fun RangeBar( + statsRangeState: StatsRange, + onClick: (StatsRange) -> Unit +){ + LazyRow { + repeat(7){ + position -> + val rangeAtIndex = when (position){ + 0 -> StatsRange.THIS_WEEK + 1 -> StatsRange.THIS_MONTH + 2 -> StatsRange.THIS_YEAR + 3 -> StatsRange.LAST_WEEK + 4 -> StatsRange.LAST_MONTH + 5 -> StatsRange.LAST_YEAR + 6 -> StatsRange.ALL_TIME + else -> StatsRange.ALL_TIME + } + item { + if(position == 0){ + Spacer(modifier = Modifier.width(10.dp)) + } + ElevatedSuggestionChip( + onClick = { + onClick(rangeAtIndex) + }, + label = { + Text(rangeAtIndex.rangeString, color = when(statsRangeState == rangeAtIndex){ + true -> ListenBrainzTheme.colorScheme.followerChipUnselected + false -> ListenBrainzTheme.colorScheme.followerChipSelected + }, style = ListenBrainzTheme.textStyles.chips) + }, + shape = RoundedCornerShape(10.dp), + border = when(statsRangeState == rangeAtIndex){ + true -> null + false -> BorderStroke(1.dp, lb_purple_night) + }, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + if (statsRangeState == rangeAtIndex) { + ListenBrainzTheme.colorScheme.followerChipSelected + } else { + ListenBrainzTheme.colorScheme.followerChipUnselected + } + ), + ) + Spacer(modifier = Modifier.width(10.dp)) + } + + } + } +} + +@Composable +private fun UserGlobalBar( + userGlobalState: UserGlobal, + onUserGlobalChange: (UserGlobal) -> Unit, + username: String? +){ + LazyRow { + item { + val reqdState = userGlobalState == UserGlobal.USER + Spacer(modifier = Modifier.width(10.dp)) + ElevatedSuggestionChip( + onClick = { + onUserGlobalChange(UserGlobal.USER) + }, + label = { + Text((username ?: "").toString(), color = when(reqdState){ + true -> ListenBrainzTheme.colorScheme.followerChipUnselected + false -> ListenBrainzTheme.colorScheme.followerChipSelected + }, style = ListenBrainzTheme.textStyles.chips) + }, + shape = RoundedCornerShape(10.dp), + border = when(reqdState){ + true -> null + false -> BorderStroke(1.dp, lb_purple_night) + }, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + if (reqdState) { + ListenBrainzTheme.colorScheme.followerChipSelected + } else { + ListenBrainzTheme.colorScheme.followerChipUnselected + } + ), + ) + Spacer(modifier = Modifier.width(10.dp)) + } + item { + val reqdState = userGlobalState == UserGlobal.GLOBAL + ElevatedSuggestionChip( + onClick = { + onUserGlobalChange(UserGlobal.GLOBAL) + }, + label = { + Row (verticalAlignment = Alignment.CenterVertically) { + Text("Global", color = when(reqdState){ + true -> ListenBrainzTheme.colorScheme.followerChipUnselected + false -> ListenBrainzTheme.colorScheme.followerChipSelected + }, style = ListenBrainzTheme.textStyles.chips) + Spacer(modifier = Modifier.width(4.dp)) + Icon(painter = painterResource(id = R.drawable.globe), contentDescription = "", modifier = Modifier.height(25.dp), tint = when(reqdState){ + true -> ListenBrainzTheme.colorScheme.followerChipUnselected + false -> ListenBrainzTheme.colorScheme.followerChipSelected + }) + } + + }, + shape = RoundedCornerShape(10.dp), + border = when(reqdState){ + true -> null + false -> BorderStroke(1.dp, lb_purple_night) + }, + colors = SuggestionChipDefaults.elevatedSuggestionChipColors( + if (reqdState) { + ListenBrainzTheme.colorScheme.followerChipSelected + } else { + ListenBrainzTheme.colorScheme.followerChipUnselected + } + ), + ) + Spacer(modifier = Modifier.width(10.dp)) + } + } +} + diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/UserGlobal.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/UserGlobal.kt new file mode 100644 index 00000000..8301abe6 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/UserGlobal.kt @@ -0,0 +1,6 @@ +package org.listenbrainz.android.ui.screens.profile.stats + +enum class UserGlobal { + USER, + GLOBAL +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt index 56d5d8f7..4e420472 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt @@ -23,6 +23,8 @@ import org.listenbrainz.android.ui.screens.profile.ListensTabUiState import org.listenbrainz.android.ui.screens.profile.ProfileUiState import org.listenbrainz.android.ui.screens.profile.StatsTabUIState import org.listenbrainz.android.ui.screens.profile.TasteTabUIState +import org.listenbrainz.android.ui.screens.profile.stats.StatsRange +import org.listenbrainz.android.ui.screens.profile.stats.UserGlobal import org.listenbrainz.android.util.Constants.Strings.STATUS_LOGGED_OUT import javax.inject.Inject @@ -55,15 +57,15 @@ class ProfileViewModel @Inject constructor( private suspend fun getSimilarArtists(username: String?) : List { val currentUsername = appPreferences.username.get() - val currentUserTopArtists = userRepository.getTopArtists(currentUsername) - val userTopArtists = userRepository.getTopArtists(username) + val currentUserTopArtists = userRepository.getTopArtists(currentUsername, count = 100) + val userTopArtists = userRepository.getTopArtists(username, count = 100) val similarArtists = mutableListOf() currentUserTopArtists.data?.payload?.artists?.map { currentUserTopArtist -> userTopArtists.data?.payload?.artists?.map{ userTopArtist -> - if(currentUserTopArtist.artist_name == userTopArtist.artist_name){ - similarArtists.add(currentUserTopArtist.artist_name) + if(currentUserTopArtist.artistName == userTopArtist.artistName){ + similarArtists.add(currentUserTopArtist.artistName) } } } @@ -150,8 +152,125 @@ class ProfileViewModel @Inject constructor( listenStateFlow.emit(listensTabState) } + private fun extractDayAndMonth(timeRange: String): Pair { + val monthOrder = mapOf( + "January" to 1, + "February" to 2, + "March" to 3, + "April" to 4, + "May" to 5, + "June" to 6, + "July" to 7, + "August" to 8, + "September" to 9, + "October" to 10, + "November" to 11, + "December" to 12 + ) + val parts = timeRange.split(" ") + val month = parts[1] + val day = parts[0].toIntOrNull() ?: 0 + return Pair(day, monthOrder[month] ?: 0) + } + + private fun extractMonthAndYear(timeRange: String): Pair { + val monthOrder = mapOf( + "January" to 1, + "February" to 2, + "March" to 3, + "April" to 4, + "May" to 5, + "June" to 6, + "July" to 7, + "August" to 8, + "September" to 9, + "October" to 10, + "November" to 11, + "December" to 12 + ) + val parts = timeRange.split(" ") + val month = parts[0] + val year = parts[1].toIntOrNull() ?: 0 + return Pair(monthOrder[month] ?: 0, year) + } + private suspend fun getUserStatsData(inputUsername: String?) { + val dayOrder = mapOf( + "Monday" to 1, + "Tuesday" to 2, + "Wednesday" to 3, + "Thursday" to 4, + "Friday" to 5, + "Saturday" to 6, + "Sunday" to 7 + ) + val userThisWeekListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.THIS_WEEK.apiIdenfier).data?.payload?.listeningActivity?.sortedBy { + val day = (it?.timeRange?:"").split(" ")[0] + dayOrder[day] ?: 0 + } ?: listOf() + var index = 0 + var i = 1 + while((i < userThisWeekListeningActivity.size)){ + val condn = userThisWeekListeningActivity[i]?.timeRange?.split(" ") + ?.get(0) == (userThisWeekListeningActivity[i - 1]?.timeRange?.split(" ") + ?.get(0) ?: false) + if(condn && userThisWeekListeningActivity[i]?.color == null) + { + userThisWeekListeningActivity[i]?.componentIndex = index + userThisWeekListeningActivity[i-1]?.componentIndex = index + userThisWeekListeningActivity[i-1]?.color = (0xFF353070).toInt() + userThisWeekListeningActivity[i]?.color = (0xFFEB743B).toInt() + } + else{ + userThisWeekListeningActivity[i]?.componentIndex = index + if(userThisWeekListeningActivity[i]?.color == null){ + userThisWeekListeningActivity[i]?.color = (0xFFEB743B).toInt() + } + } + i ++ + index = index.inc() + + } + val userThisMonthListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.THIS_MONTH.apiIdenfier).data?.payload?.listeningActivity?.sortedWith(compareBy( + { extractDayAndMonth(it?.timeRange ?: "").first }, + { extractDayAndMonth(it?.timeRange ?: "").second } + )) + val userThisYearListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.THIS_YEAR.apiIdenfier).data?.payload?.listeningActivity?.sortedWith(compareBy( + { extractMonthAndYear(it?.timeRange ?: "").first }, + { extractMonthAndYear(it?.timeRange ?: "").second } + )) + val userLastWeekListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.LAST_WEEK.apiIdenfier).data?.payload?.listeningActivity?.sortedBy { + val day = (it?.timeRange?:"").split(" ")[0] + dayOrder[day] ?: 0 + } + val userLastMonthListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.LAST_MONTH.apiIdenfier).data?.payload?.listeningActivity?.sortedWith(compareBy( + { extractDayAndMonth(it?.timeRange ?: "").first }, + { extractDayAndMonth(it?.timeRange ?: "").second } + )) + val userLastYearListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.LAST_YEAR.apiIdenfier).data?.payload?.listeningActivity?.sortedWith(compareBy( + { extractMonthAndYear(it?.timeRange ?: "").first }, + { extractMonthAndYear(it?.timeRange ?: "").second } + )) + val userAllTimeListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.ALL_TIME.apiIdenfier).data?.payload?.listeningActivity?.sortedBy { + it?.timeRange + } + val userListeningActivityMap = mapOf( + Pair(UserGlobal.USER, StatsRange.THIS_WEEK) to (userThisWeekListeningActivity ?: listOf()), + Pair(UserGlobal.USER, StatsRange.THIS_MONTH) to (userThisMonthListeningActivity ?: listOf()), + Pair(UserGlobal.USER, StatsRange.THIS_YEAR) to (userThisYearListeningActivity ?: listOf()), + Pair(UserGlobal.USER, StatsRange.LAST_WEEK) to (userLastWeekListeningActivity ?: listOf()), + Pair(UserGlobal.USER, StatsRange.LAST_MONTH) to (userLastMonthListeningActivity ?: listOf()), + Pair(UserGlobal.USER, StatsRange.LAST_YEAR) to (userLastYearListeningActivity ?: listOf()), + Pair(UserGlobal.USER, StatsRange.ALL_TIME) to (userAllTimeListeningActivity ?: listOf()), + ) + + val userTopArtists = userRepository.getTopArtists(inputUsername, rangeString = StatsRange.THIS_YEAR.apiIdenfier).data + val statsTabState = StatsTabUIState( + isLoading = false, + userListeningActivity = userListeningActivityMap, + ) + statsStateFlow.emit(statsTabState) } private suspend fun getUserTasteData(inputUsername: String?) { diff --git a/app/src/main/res/drawable/globe.xml b/app/src/main/res/drawable/globe.xml new file mode 100644 index 00000000..9aead180 --- /dev/null +++ b/app/src/main/res/drawable/globe.xml @@ -0,0 +1,9 @@ + + + From 174576710358c0de0f0afde83f2bbfbeffe64607 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:39:23 +0530 Subject: [PATCH 3/5] Added charts for user and global stats, markers left --- app/build.gradle.kts | 2 +- .../android/repository/user/UserRepository.kt | 1 + .../repository/user/UserRepositoryImpl.kt | 3 + .../android/service/UserService.kt | 4 + .../ui/screens/profile/stats/StatsScreen.kt | 105 ++++++++++++------ .../android/viewmodel/ProfileViewModel.kt | 94 +++++----------- gradle/libs.versions.toml | 2 +- 7 files changed, 113 insertions(+), 98 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f4ef6442..15f428a8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -210,7 +210,7 @@ dependencies { implementation(libs.logger.android) // Charts - implementation("com.patrykandpatrick.vico:compose:2.0.0-alpha.22") + implementation(libs.vico.compose) // Testing testImplementation(libs.junit) diff --git a/app/src/main/java/org/listenbrainz/android/repository/user/UserRepository.kt b/app/src/main/java/org/listenbrainz/android/repository/user/UserRepository.kt index 5ee2bb84..42d9a0fa 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/user/UserRepository.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/user/UserRepository.kt @@ -18,4 +18,5 @@ interface UserRepository { suspend fun getTopArtists(username: String?, rangeString: String = "all_time", count: Int = 25): Resource suspend fun getUserFeedback(username: String?, score: Int?): Resource suspend fun getUserListeningActivity(username: String?, rangeString: String = "all_time"): Resource + suspend fun getGlobalListeningActivity(rangeString: String = "all_time"): Resource } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/user/UserRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/user/UserRepositoryImpl.kt index c2a3a487..c249c2cd 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/user/UserRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/user/UserRepositoryImpl.kt @@ -59,5 +59,8 @@ class UserRepositoryImpl @Inject constructor( service.getUserListeningActivity(username,rangeString) } + override suspend fun getGlobalListeningActivity(rangeString: String): Resource = parseResponse { + service.getGlobalListeningActivity(rangeString) + } } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/service/UserService.kt b/app/src/main/java/org/listenbrainz/android/service/UserService.kt index 36dfe321..3ef84bea 100644 --- a/app/src/main/java/org/listenbrainz/android/service/UserService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/UserService.kt @@ -33,4 +33,8 @@ interface UserService { @GET("stats/user/{user_name}/listening-activity") suspend fun getUserListeningActivity(@Path("user_name") username: String?, @Query("range") rangeString: String?): Response + + @GET("stats/sitewide/listening-activity") + suspend fun getGlobalListeningActivity(@Query("range") rangeString: String?) : Response + } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt index 3091c793..39277223 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt @@ -2,7 +2,6 @@ package org.listenbrainz.android.ui.screens.profile.stats import CategoryState import android.text.TextUtils -import android.util.Log import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -31,7 +30,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -41,11 +39,11 @@ import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottomAxis import com.patrykandpatrick.vico.compose.cartesian.axis.rememberStartAxis import com.patrykandpatrick.vico.compose.cartesian.layer.rememberColumnCartesianLayer import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart +import com.patrykandpatrick.vico.compose.common.component.rememberLineComponent import com.patrykandpatrick.vico.compose.common.component.rememberTextComponent import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.columnSeries import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianLayer -import com.patrykandpatrick.vico.core.common.component.LineComponent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext @@ -154,9 +152,9 @@ fun StatsScreen( } item { Spacer(modifier = Modifier.height(10.dp)) - Box(modifier = Modifier.padding(start = 10.dp)){ + Box(){ Column { - Text("Listening activity", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Text("Listening activity", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(start = 10.dp)) Spacer(modifier = Modifier.height(15.dp)) val data = uiState.statsTabUIState.userListeningActivity[Pair(userGlobalState, statsRangeState)] ?: listOf() @@ -165,33 +163,51 @@ fun StatsScreen( CartesianChartModelProducer() } + val splitIndex = when(statsRangeState){ + StatsRange.THIS_WEEK -> 7 + StatsRange.LAST_WEEK -> 7 + StatsRange.THIS_MONTH -> 30 + StatsRange.LAST_MONTH -> 30 + StatsRange.LAST_YEAR -> 12 + StatsRange.THIS_YEAR -> 12 + StatsRange.ALL_TIME -> 0 + } + val listenCountsFirstPart = data.subList(0, minOf(splitIndex, data.size)).mapNotNull { it?.listenCount } + val listenCountsSecondPart = if (data.size > splitIndex) { + data.subList(splitIndex, data.size).mapNotNull { it?.listenCount } + } else { + emptyList() + } + LaunchedEffect(data) { withContext(Dispatchers.Default) { while(isActive){ modelProducer.runTransaction { columnSeries { - series(y = data.map { (it?.listenCount ?: 0).toInt() }) + if(listenCountsFirstPart.isNotEmpty()){ + series(y = listenCountsFirstPart) + } + if(listenCountsSecondPart.isNotEmpty()){ + series(y = listenCountsSecondPart) + } } } } } } - val columnProvider = { - val columnAttributes = - data.map { - LineComponent( - color = it?.color ?: Color.Unspecified.toArgb(), - thicknessDp = 25f, - ) - } - data.forEach{ - Log.v("pranav", it?.timeRange.toString()) - Log.v("pranav", it?.componentIndex.toString()) - Log.v("pranav", it?.color.toString()) - } + val columnProvider = ColumnCartesianLayer.ColumnProvider.series( - columns = columnAttributes + listOf( + rememberLineComponent( + color = Color(0xFF353070), + thickness = 25.dp, + ), + rememberLineComponent( + color = Color(0xFFEB743B), + thickness = 25.dp, + ) + ) ) - } + CartesianChartHost( modifier = Modifier @@ -201,7 +217,7 @@ fun StatsScreen( .background(Color(0xFFe0e5de)), chart = rememberCartesianChart( rememberColumnCartesianLayer( - columnProvider = columnProvider(), + columnProvider = columnProvider, spacing = 25.dp, mergeMode = { ColumnCartesianLayer.MergeMode.Grouped }, ), @@ -213,21 +229,16 @@ fun StatsScreen( ), guideline = null, valueFormatter = { value, chartValues, verticalAxisPosition -> - if(value.toInt() % 5 == 0){ - value.toString() - } - else{ - "" - } - } + valueFormatter(value.toInt(), statsRangeState) + }, ), ), - modelProducer = modelProducer + modelProducer = modelProducer, ) } else{ - Text("There are no statistics available for this user for this period", color = ListenBrainzTheme.colorScheme.textColor) + Text("There are no statistics available for this user for this period", color = ListenBrainzTheme.colorScheme.textColor, modifier = Modifier.padding(start = 10.dp)) } } @@ -419,3 +430,35 @@ private fun UserGlobalBar( } } +private fun valueFormatter(value: Int, statsRange: StatsRange) : String { + val label : String = when(statsRange){ + StatsRange.THIS_WEEK, StatsRange.LAST_WEEK -> when(value % 7){ + 0 -> "Mon" + 1 -> "Tue" + 2 -> "Wed" + 3 -> "Thu" + 4 -> "Fri" + 5 -> "Sat" + 6 -> "Sun" + else -> "" + } + StatsRange.THIS_MONTH, StatsRange.LAST_MONTH -> value.toString() + StatsRange.THIS_YEAR, StatsRange.LAST_YEAR -> when(value % 12){ + 0 -> "Jan" + 1 -> "Feb" + 2 -> "Mar" + 3 -> "Apr" + 4 -> "May" + 5 -> "Jun" + 6 -> "Jul" + 7 -> "Aug" + 8 -> "Sep" + 9 -> "Oct" + 10 -> "Nov" + 11 -> "Dec" + else -> "" + } + StatsRange.ALL_TIME -> (value + 2002).toString() + } + return label +} \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt index 4e420472..476231b9 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt @@ -195,74 +195,38 @@ class ProfileViewModel @Inject constructor( } private suspend fun getUserStatsData(inputUsername: String?) { - val dayOrder = mapOf( - "Monday" to 1, - "Tuesday" to 2, - "Wednesday" to 3, - "Thursday" to 4, - "Friday" to 5, - "Saturday" to 6, - "Sunday" to 7 - ) - val userThisWeekListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.THIS_WEEK.apiIdenfier).data?.payload?.listeningActivity?.sortedBy { - val day = (it?.timeRange?:"").split(" ")[0] - dayOrder[day] ?: 0 - } ?: listOf() - var index = 0 - var i = 1 - while((i < userThisWeekListeningActivity.size)){ - val condn = userThisWeekListeningActivity[i]?.timeRange?.split(" ") - ?.get(0) == (userThisWeekListeningActivity[i - 1]?.timeRange?.split(" ") - ?.get(0) ?: false) - if(condn && userThisWeekListeningActivity[i]?.color == null) - { - userThisWeekListeningActivity[i]?.componentIndex = index - userThisWeekListeningActivity[i-1]?.componentIndex = index - userThisWeekListeningActivity[i-1]?.color = (0xFF353070).toInt() - userThisWeekListeningActivity[i]?.color = (0xFFEB743B).toInt() - } - else{ - userThisWeekListeningActivity[i]?.componentIndex = index - if(userThisWeekListeningActivity[i]?.color == null){ - userThisWeekListeningActivity[i]?.color = (0xFFEB743B).toInt() - } + val userThisWeekListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.THIS_WEEK.apiIdenfier).data?.payload?.listeningActivity ?: listOf() + val userThisMonthListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.THIS_MONTH.apiIdenfier).data?.payload?.listeningActivity ?: listOf() + val userThisYearListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.THIS_YEAR.apiIdenfier).data?.payload?.listeningActivity ?: listOf() + val userLastWeekListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.LAST_WEEK.apiIdenfier).data?.payload?.listeningActivity ?: listOf() + val userLastMonthListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.LAST_MONTH.apiIdenfier).data?.payload?.listeningActivity ?: listOf() + val userLastYearListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.LAST_YEAR.apiIdenfier).data?.payload?.listeningActivity ?: listOf() + val userAllTimeListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.ALL_TIME.apiIdenfier).data?.payload?.listeningActivity ?: listOf() - } - i ++ - index = index.inc() + val globalThisWeekListeningActivity = userRepository.getGlobalListeningActivity(StatsRange.THIS_WEEK.apiIdenfier).data?.payload?.listeningActivity ?: listOf() + val globalThisMonthListeningActivity = userRepository.getGlobalListeningActivity(StatsRange.THIS_MONTH.apiIdenfier).data?.payload?.listeningActivity ?: listOf() + val globalThisYearListeningActivity = userRepository.getGlobalListeningActivity(StatsRange.THIS_YEAR.apiIdenfier).data?.payload?.listeningActivity ?: listOf() + val globalLastWeekListeningActivity = userRepository.getGlobalListeningActivity(StatsRange.LAST_WEEK.apiIdenfier).data?.payload?.listeningActivity ?: listOf() + val globalLastMonthListeningActivity = userRepository.getGlobalListeningActivity(StatsRange.LAST_MONTH.apiIdenfier).data?.payload?.listeningActivity ?: listOf() + val globalLastYearListeningActivity = userRepository.getGlobalListeningActivity(StatsRange.LAST_YEAR.apiIdenfier).data?.payload?.listeningActivity ?: listOf() + val globalAllTimeListeningActivity = userRepository.getGlobalListeningActivity(StatsRange.ALL_TIME.apiIdenfier).data?.payload?.listeningActivity ?: listOf() - } - val userThisMonthListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.THIS_MONTH.apiIdenfier).data?.payload?.listeningActivity?.sortedWith(compareBy( - { extractDayAndMonth(it?.timeRange ?: "").first }, - { extractDayAndMonth(it?.timeRange ?: "").second } - )) - val userThisYearListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.THIS_YEAR.apiIdenfier).data?.payload?.listeningActivity?.sortedWith(compareBy( - { extractMonthAndYear(it?.timeRange ?: "").first }, - { extractMonthAndYear(it?.timeRange ?: "").second } - )) - val userLastWeekListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.LAST_WEEK.apiIdenfier).data?.payload?.listeningActivity?.sortedBy { - val day = (it?.timeRange?:"").split(" ")[0] - dayOrder[day] ?: 0 - } - val userLastMonthListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.LAST_MONTH.apiIdenfier).data?.payload?.listeningActivity?.sortedWith(compareBy( - { extractDayAndMonth(it?.timeRange ?: "").first }, - { extractDayAndMonth(it?.timeRange ?: "").second } - )) - val userLastYearListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.LAST_YEAR.apiIdenfier).data?.payload?.listeningActivity?.sortedWith(compareBy( - { extractMonthAndYear(it?.timeRange ?: "").first }, - { extractMonthAndYear(it?.timeRange ?: "").second } - )) - val userAllTimeListeningActivity = userRepository.getUserListeningActivity(inputUsername, StatsRange.ALL_TIME.apiIdenfier).data?.payload?.listeningActivity?.sortedBy { - it?.timeRange - } val userListeningActivityMap = mapOf( - Pair(UserGlobal.USER, StatsRange.THIS_WEEK) to (userThisWeekListeningActivity ?: listOf()), - Pair(UserGlobal.USER, StatsRange.THIS_MONTH) to (userThisMonthListeningActivity ?: listOf()), - Pair(UserGlobal.USER, StatsRange.THIS_YEAR) to (userThisYearListeningActivity ?: listOf()), - Pair(UserGlobal.USER, StatsRange.LAST_WEEK) to (userLastWeekListeningActivity ?: listOf()), - Pair(UserGlobal.USER, StatsRange.LAST_MONTH) to (userLastMonthListeningActivity ?: listOf()), - Pair(UserGlobal.USER, StatsRange.LAST_YEAR) to (userLastYearListeningActivity ?: listOf()), - Pair(UserGlobal.USER, StatsRange.ALL_TIME) to (userAllTimeListeningActivity ?: listOf()), + Pair(UserGlobal.USER, StatsRange.THIS_WEEK) to userThisWeekListeningActivity, + Pair(UserGlobal.USER, StatsRange.THIS_MONTH) to userThisMonthListeningActivity, + Pair(UserGlobal.USER, StatsRange.THIS_YEAR) to userThisYearListeningActivity, + Pair(UserGlobal.USER, StatsRange.LAST_WEEK) to userLastWeekListeningActivity, + Pair(UserGlobal.USER, StatsRange.LAST_MONTH) to userLastMonthListeningActivity, + Pair(UserGlobal.USER, StatsRange.LAST_YEAR) to userLastYearListeningActivity, + Pair(UserGlobal.USER, StatsRange.ALL_TIME) to userAllTimeListeningActivity, + + Pair(UserGlobal.GLOBAL, StatsRange.THIS_WEEK) to globalThisWeekListeningActivity, + Pair(UserGlobal.GLOBAL, StatsRange.THIS_MONTH) to globalThisMonthListeningActivity, + Pair(UserGlobal.GLOBAL, StatsRange.THIS_YEAR) to globalThisYearListeningActivity, + Pair(UserGlobal.GLOBAL, StatsRange.LAST_WEEK) to globalLastWeekListeningActivity, + Pair(UserGlobal.GLOBAL, StatsRange.LAST_MONTH) to globalLastMonthListeningActivity, + Pair(UserGlobal.GLOBAL, StatsRange.LAST_YEAR) to globalLastYearListeningActivity, + Pair(UserGlobal.GLOBAL, StatsRange.ALL_TIME) to globalAllTimeListeningActivity, ) val userTopArtists = userRepository.getTopArtists(inputUsername, rangeString = StatsRange.THIS_YEAR.apiIdenfier).data diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 50f62279..756cf647 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,7 +42,7 @@ testRunner = "1.6.1" testExtJunit = "1.2.1" espresso = "3.6.1" turbine = "1.1.0" -vicoCompose = "1.15.0" +vicoCompose = "2.0.0-alpha.22" composeRatingbar = "1.3.4" loggerAndroid = "1.0.0" compileSdk = "34" From c1100d02d421775ac19ff616575de547c97c361e Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:28:33 +0530 Subject: [PATCH 4/5] Added top artists of user in stats page --- .../ui/screens/profile/ProfileUiState.kt | 3 +- .../ui/screens/profile/stats/StatsScreen.kt | 90 +++++++++++++++++-- .../android/viewmodel/ProfileViewModel.kt | 19 +++- 3 files changed, 104 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileUiState.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileUiState.kt index 87291fb4..6d4597ac 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileUiState.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileUiState.kt @@ -7,6 +7,7 @@ import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.SimilarUser import org.listenbrainz.android.model.user.AllPinnedRecordings import org.listenbrainz.android.model.user.ListeningActivity +import org.listenbrainz.android.model.user.TopArtists import org.listenbrainz.android.model.user.UserFeedback import org.listenbrainz.android.ui.screens.profile.listens.ListeningNowUiState import org.listenbrainz.android.ui.screens.profile.stats.StatsRange @@ -45,7 +46,7 @@ data class TasteTabUIState ( data class StatsTabUIState( val isLoading: Boolean = true, val userListeningActivity: Map, List> = mapOf(), - val sortedListeningActivity: List? = listOf() + val topArtists: Map? = null, ) data class ListeningNowUiState( diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt index 39277223..4b26f618 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt @@ -4,10 +4,12 @@ import CategoryState import android.text.TextUtils import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -18,6 +20,7 @@ import androidx.compose.material3.ElevatedSuggestionChip import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SuggestionChipDefaults +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -31,6 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel @@ -50,6 +54,7 @@ import kotlinx.coroutines.withContext import org.listenbrainz.android.R import org.listenbrainz.android.ui.screens.profile.ProfileUiState import org.listenbrainz.android.ui.theme.ListenBrainzTheme +import org.listenbrainz.android.ui.theme.app_bg_secondary_dark import org.listenbrainz.android.ui.theme.lb_purple_night import org.listenbrainz.android.viewmodel.ProfileViewModel @@ -133,6 +138,15 @@ fun StatsScreen( mutableStateOf(CategoryState.ARTISTS) } + val artistsCollapseState: MutableState = remember { + mutableStateOf(true) + } + + val topArtists = when(artistsCollapseState.value){ + true -> uiState.statsTabUIState.topArtists?.get(statsRangeState)?.payload?.artists?.take(5) ?: listOf() + false -> uiState.statsTabUIState.topArtists?.get(statsRangeState)?.payload?.artists ?: listOf() + } + LazyColumn { item { RangeBar( @@ -246,13 +260,11 @@ fun StatsScreen( } item { - Spacer(modifier = Modifier.height(10.dp)) Column (modifier = Modifier - .padding(start = 10.dp) - .background( - ListenBrainzTheme.colorScheme.userPageGradient - )) { + .padding(start = 10.dp, top = 30.dp) + ) { Text("Top ...", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp)) + Box(modifier = Modifier.height(10.dp)) Row { repeat(3){ position -> @@ -298,13 +310,79 @@ fun StatsScreen( Spacer(modifier = Modifier.width(10.dp)) } } - } + + topArtists.map { + topArtist -> + ArtistCard(artistName = topArtist.artistName, listenCount = topArtist.listenCount) { + + } + } + + + } } } } +@Composable +fun ArtistCard( + modifier: Modifier = Modifier, + artistName: String, + listenCount: Int? = 0, + onClick: () -> Unit, +) { + Surface( + modifier = modifier + .fillMaxWidth() + .padding(end = 10.dp, top = 10.dp) + .clickable(enabled = true) { onClick() }, + shape = ListenBrainzTheme.shapes.listenCardSmall, + shadowElevation = 4.dp, + color = app_bg_secondary_dark + ) { + Column { + Box( + modifier = Modifier + .fillMaxWidth() + .height(ListenBrainzTheme.sizes.listenCardHeight), + contentAlignment = Alignment.CenterStart + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(10.dp)) + Text(artistName, color = ListenBrainzTheme.colorScheme.followerChipSelected, style = ListenBrainzTheme.textStyles.listenTitle, + maxLines = 1, + overflow = TextOverflow.Ellipsis) + } + + Box( + modifier = modifier + .align(Alignment.CenterEnd) + .padding(end = 10.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .background(ListenBrainzTheme.colorScheme.followerChipSelected) + .padding(6.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = listenCount.toString(), + color = Color.Black + ) + } + } + + } + } + } +} + @Composable private fun RangeBar( statsRangeState: StatsRange, diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt index 476231b9..d2c87497 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt @@ -229,10 +229,27 @@ class ProfileViewModel @Inject constructor( Pair(UserGlobal.GLOBAL, StatsRange.ALL_TIME) to globalAllTimeListeningActivity, ) - val userTopArtists = userRepository.getTopArtists(inputUsername, rangeString = StatsRange.THIS_YEAR.apiIdenfier).data + val userTopArtistsThisWeek = userRepository.getTopArtists(inputUsername, rangeString = StatsRange.THIS_WEEK.apiIdenfier).data + val userTopArtistsThisMonth = userRepository.getTopArtists(inputUsername, rangeString = StatsRange.THIS_MONTH.apiIdenfier).data + val userTopArtistsThisYear = userRepository.getTopArtists(inputUsername, rangeString = StatsRange.THIS_YEAR.apiIdenfier).data + val userTopArtistsLastWeek = userRepository.getTopArtists(inputUsername, rangeString = StatsRange.LAST_WEEK.apiIdenfier).data + val userTopArtistsLastMonth = userRepository.getTopArtists(inputUsername, rangeString = StatsRange.LAST_MONTH.apiIdenfier).data + val userTopArtistsLastYear = userRepository.getTopArtists(inputUsername, rangeString = StatsRange.LAST_YEAR.apiIdenfier).data + val userTopArtistsAllTime = userRepository.getTopArtists(inputUsername, rangeString = StatsRange.ALL_TIME.apiIdenfier).data + + val topArtists = mapOf( + StatsRange.THIS_WEEK to userTopArtistsThisWeek, + StatsRange.THIS_MONTH to userTopArtistsThisMonth, + StatsRange.THIS_YEAR to userTopArtistsThisYear, + StatsRange.LAST_WEEK to userTopArtistsLastWeek, + StatsRange.LAST_MONTH to userTopArtistsLastMonth, + StatsRange.LAST_YEAR to userTopArtistsLastYear, + StatsRange.ALL_TIME to userTopArtistsAllTime + ) val statsTabState = StatsTabUIState( isLoading = false, userListeningActivity = userListeningActivityMap, + topArtists = topArtists ) statsStateFlow.emit(statsTabState) } From e081a6705068a270102ca871a9859759c3978cc3 Mon Sep 17 00:00:00 2001 From: Pranav <122373207+pranavkonidena@users.noreply.github.com> Date: Fri, 9 Aug 2024 19:20:23 +0530 Subject: [PATCH 5/5] Added top songs Avoided fetching data all at once as rate limit was being hit, made it to work like the web version by making a request only if a user requests --- .../android/model/user/Recording.kt | 16 +++ .../android/model/user/Release.kt | 14 ++ .../android/model/user/TopAlbums.kt | 5 + .../android/model/user/TopAlbumsPayload.kt | 15 ++ .../android/model/user/TopArtistInfo.kt | 9 ++ .../android/model/user/TopSongPayload.kt | 15 ++ .../android/model/user/TopSongs.kt | 5 + .../android/repository/user/UserRepository.kt | 4 + .../repository/user/UserRepositoryImpl.kt | 16 +++ .../android/service/UserService.kt | 8 ++ .../android/ui/components/ListenCardSmall.kt | 7 +- .../ui/screens/profile/ProfileUiState.kt | 4 + .../ui/screens/profile/stats/StatsScreen.kt | 134 ++++++++++++++++-- .../android/viewmodel/ProfileViewModel.kt | 68 ++++++++- 14 files changed, 305 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/Recording.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/Release.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/TopAlbums.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/TopAlbumsPayload.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/TopArtistInfo.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/TopSongPayload.kt create mode 100644 app/src/main/java/org/listenbrainz/android/model/user/TopSongs.kt diff --git a/app/src/main/java/org/listenbrainz/android/model/user/Recording.kt b/app/src/main/java/org/listenbrainz/android/model/user/Recording.kt new file mode 100644 index 00000000..0500d7f7 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/Recording.kt @@ -0,0 +1,16 @@ +package org.listenbrainz.android.model.user + +import com.google.gson.annotations.SerializedName + +data class Recording( + @SerializedName("artist_mbids") val artistMbids: List? = listOf(), + @SerializedName("artist_name") val artistName: String? = "", + val artists: List? = listOf(), + @SerializedName("caa_id") val caaId: Long? = 0, + @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = "", + @SerializedName("listen_count") val listenCount: Int? = 0, + @SerializedName("recording_mbid") val recordingMbid: String? = "", + @SerializedName("release_mbid") val releaseMbid: String? = "", + @SerializedName("release_name") val releaseName: String? = "", + @SerializedName("track_name") val trackName: String? = "" +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/user/Release.kt b/app/src/main/java/org/listenbrainz/android/model/user/Release.kt new file mode 100644 index 00000000..b9b73821 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/Release.kt @@ -0,0 +1,14 @@ +package org.listenbrainz.android.model.user + +import com.google.gson.annotations.SerializedName + +data class Release( + @SerializedName("artist_mbids") val artistMbids: List? = listOf(), + @SerializedName("artist_name") val artistName: String? = "", + val artists: List? = listOf(), + @SerializedName("caa_id") val caaId: Long? = 0, + @SerializedName("caa_release_mbid") val caaReleaseMbid: String? = "", + @SerializedName("listen_count") val listenCount: Int? = 0, + @SerializedName("release_mbid") val releaseMbid: String? = "", + @SerializedName("release_name") val releaseName: String? = "" +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/user/TopAlbums.kt b/app/src/main/java/org/listenbrainz/android/model/user/TopAlbums.kt new file mode 100644 index 00000000..b0ca3384 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/TopAlbums.kt @@ -0,0 +1,5 @@ +package org.listenbrainz.android.model.user + +data class TopAlbums( + val payload: TopAlbumsPayload? = TopAlbumsPayload() +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/user/TopAlbumsPayload.kt b/app/src/main/java/org/listenbrainz/android/model/user/TopAlbumsPayload.kt new file mode 100644 index 00000000..1cd53827 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/TopAlbumsPayload.kt @@ -0,0 +1,15 @@ +package org.listenbrainz.android.model.user + +import com.google.gson.annotations.SerializedName + +data class TopAlbumsPayload( + val count: Int? = 0, + @SerializedName("from_ts") val fromTs: Int? = 0, + @SerializedName("last_updated") val lastUpdated: Int? = 0, + @SerializedName("offset") val offset: Int? = 0, + val range: String? = "", + val releases: List? = listOf(), + @SerializedName("to_ts") val toTs: Int? = 0, + @SerializedName("total_release_count") val totalReleaseCount: Int? = 0, + @SerializedName("user_id") val userId: String? = "" +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/user/TopArtistInfo.kt b/app/src/main/java/org/listenbrainz/android/model/user/TopArtistInfo.kt new file mode 100644 index 00000000..6a967d4e --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/TopArtistInfo.kt @@ -0,0 +1,9 @@ +package org.listenbrainz.android.model.user + +import com.google.gson.annotations.SerializedName + +data class TopArtistInfo( + @SerializedName("artist_credit_name") val artistCreditName: String? = null, + @SerializedName("artist_mbid") val artistMbid: String? = null, + @SerializedName("join_phrase") val joinPhrase: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/user/TopSongPayload.kt b/app/src/main/java/org/listenbrainz/android/model/user/TopSongPayload.kt new file mode 100644 index 00000000..7a9f5866 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/TopSongPayload.kt @@ -0,0 +1,15 @@ +package org.listenbrainz.android.model.user + +import com.google.gson.annotations.SerializedName + +data class TopSongPayload( + val count: Int? = 0, + @SerializedName("from_ts") val fromTs: Int? = 0, + @SerializedName("last_updated") val lastUpdated: Int? = 0, + val offset: Int? = 0, + val range: String? = "", + val recordings: List? = listOf(), + @SerializedName("to_ts") val toTs: Int? = 0, + @SerializedName("total_recording_count") val totalRecordingCount: Int? = 0, + @SerializedName("user_id") val userId: String? = "" +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/model/user/TopSongs.kt b/app/src/main/java/org/listenbrainz/android/model/user/TopSongs.kt new file mode 100644 index 00000000..aef23865 --- /dev/null +++ b/app/src/main/java/org/listenbrainz/android/model/user/TopSongs.kt @@ -0,0 +1,5 @@ +package org.listenbrainz.android.model.user + +data class TopSongs( + val payload: TopSongPayload? = TopSongPayload() +) \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/user/UserRepository.kt b/app/src/main/java/org/listenbrainz/android/repository/user/UserRepository.kt index 42d9a0fa..3d625c65 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/user/UserRepository.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/user/UserRepository.kt @@ -3,7 +3,9 @@ package org.listenbrainz.android.repository.user import org.listenbrainz.android.model.Listens import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.user.AllPinnedRecordings +import org.listenbrainz.android.model.user.TopAlbums import org.listenbrainz.android.model.user.TopArtists +import org.listenbrainz.android.model.user.TopSongs import org.listenbrainz.android.model.user.UserFeedback import org.listenbrainz.android.model.user.UserListeningActivity import org.listenbrainz.android.model.user.UserSimilarityPayload @@ -19,4 +21,6 @@ interface UserRepository { suspend fun getUserFeedback(username: String?, score: Int?): Resource suspend fun getUserListeningActivity(username: String?, rangeString: String = "all_time"): Resource suspend fun getGlobalListeningActivity(rangeString: String = "all_time"): Resource + suspend fun getTopAlbums(username: String?, rangeString: String = "all_time" ,count: Int = 25): Resource + suspend fun getTopSongs(username: String?, rangeString: String = "all_time"): Resource } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/repository/user/UserRepositoryImpl.kt b/app/src/main/java/org/listenbrainz/android/repository/user/UserRepositoryImpl.kt index c249c2cd..45b67023 100644 --- a/app/src/main/java/org/listenbrainz/android/repository/user/UserRepositoryImpl.kt +++ b/app/src/main/java/org/listenbrainz/android/repository/user/UserRepositoryImpl.kt @@ -4,7 +4,9 @@ import org.listenbrainz.android.model.Listens import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.ResponseError import org.listenbrainz.android.model.user.AllPinnedRecordings +import org.listenbrainz.android.model.user.TopAlbums import org.listenbrainz.android.model.user.TopArtists +import org.listenbrainz.android.model.user.TopSongs import org.listenbrainz.android.model.user.UserFeedback import org.listenbrainz.android.model.user.UserListeningActivity import org.listenbrainz.android.model.user.UserSimilarityPayload @@ -63,4 +65,18 @@ class UserRepositoryImpl @Inject constructor( service.getGlobalListeningActivity(rangeString) } + override suspend fun getTopAlbums( + username: String?, + rangeString: String, + count: Int + ): Resource = parseResponse { + if(username.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() + service.getTopAlbumsOfUser(username, rangeString) + } + + override suspend fun getTopSongs(username: String?, rangeString: String): Resource = parseResponse{ + if(username.isNullOrEmpty()) return ResponseError.BAD_REQUEST.asResource() + service.getTopSongsOfUser(username, rangeString) + } + } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/service/UserService.kt b/app/src/main/java/org/listenbrainz/android/service/UserService.kt index 3ef84bea..82f01a2d 100644 --- a/app/src/main/java/org/listenbrainz/android/service/UserService.kt +++ b/app/src/main/java/org/listenbrainz/android/service/UserService.kt @@ -3,7 +3,9 @@ package org.listenbrainz.android.service import org.listenbrainz.android.model.Listens import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.user.AllPinnedRecordings +import org.listenbrainz.android.model.user.TopAlbums import org.listenbrainz.android.model.user.TopArtists +import org.listenbrainz.android.model.user.TopSongs import org.listenbrainz.android.model.user.UserFeedback import org.listenbrainz.android.model.user.UserListeningActivity import org.listenbrainz.android.model.user.UserSimilarityPayload @@ -37,4 +39,10 @@ interface UserService { @GET("stats/sitewide/listening-activity") suspend fun getGlobalListeningActivity(@Query("range") rangeString: String?) : Response + @GET("stats/user/{user_name}/releases") + suspend fun getTopAlbumsOfUser(@Path("user_name") username: String?, @Query("range") rangeString: String?): Response + + @GET("stats/user/{user_name}/recordings") + suspend fun getTopSongsOfUser(@Path("user_name") username: String?, @Query("range") rangeString: String?): Response + } \ No newline at end of file diff --git a/app/src/main/java/org/listenbrainz/android/ui/components/ListenCardSmall.kt b/app/src/main/java/org/listenbrainz/android/ui/components/ListenCardSmall.kt index 71bf7f4a..e8e27015 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/components/ListenCardSmall.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/components/ListenCardSmall.kt @@ -59,6 +59,9 @@ fun ListenCardSmall( trailingContent: @Composable (modifier: Modifier) -> Unit = {}, enableBlurbContent: Boolean = false, blurbContent: @Composable (ColumnScope.(modifier: Modifier) -> Unit) = {}, + color: Color = ListenBrainzTheme.colorScheme.level1, + titleColor: Color = ListenBrainzTheme.colorScheme.listenText, + subtitleColor: Color = titleColor.copy(alpha = 0.7f), onClick: () -> Unit, ) { Surface( @@ -67,7 +70,7 @@ fun ListenCardSmall( .clickable(enabled = true) { onClick() }, shape = ListenBrainzTheme.shapes.listenCardSmall, shadowElevation = 4.dp, - color = ListenBrainzTheme.colorScheme.level1 + color = color ) { Column { @@ -96,7 +99,7 @@ fun ListenCardSmall( Spacer(modifier = Modifier.width(ListenBrainzTheme.paddings.coverArtAndTextGap)) - TitleAndSubtitle(modifier = Modifier.padding(end = 6.dp), title = trackName, subtitle = artistName) + TitleAndSubtitle(modifier = Modifier.padding(end = 6.dp), title = trackName, subtitle = artistName, titleColor = titleColor, subtitleColor = subtitleColor) } diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileUiState.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileUiState.kt index 6d4597ac..aff967b5 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileUiState.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/ProfileUiState.kt @@ -7,7 +7,9 @@ import org.listenbrainz.android.model.PinnedRecording import org.listenbrainz.android.model.SimilarUser import org.listenbrainz.android.model.user.AllPinnedRecordings import org.listenbrainz.android.model.user.ListeningActivity +import org.listenbrainz.android.model.user.TopAlbums import org.listenbrainz.android.model.user.TopArtists +import org.listenbrainz.android.model.user.TopSongs import org.listenbrainz.android.model.user.UserFeedback import org.listenbrainz.android.ui.screens.profile.listens.ListeningNowUiState import org.listenbrainz.android.ui.screens.profile.stats.StatsRange @@ -47,6 +49,8 @@ data class StatsTabUIState( val isLoading: Boolean = true, val userListeningActivity: Map, List> = mapOf(), val topArtists: Map? = null, + val topAlbums: Map? = null, + val topSongs: Map? = null, ) data class ListeningNowUiState( diff --git a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt index 4b26f618..421a48c8 100644 --- a/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt +++ b/app/src/main/java/org/listenbrainz/android/ui/screens/profile/stats/StatsScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ElevatedSuggestionChip import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -52,10 +53,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import org.listenbrainz.android.R +import org.listenbrainz.android.ui.components.ListenCardSmall import org.listenbrainz.android.ui.screens.profile.ProfileUiState +import org.listenbrainz.android.ui.screens.profile.listens.LoadMoreButton import org.listenbrainz.android.ui.theme.ListenBrainzTheme import org.listenbrainz.android.ui.theme.app_bg_secondary_dark import org.listenbrainz.android.ui.theme.lb_purple_night +import org.listenbrainz.android.util.Utils.getCoverArtUrl import org.listenbrainz.android.viewmodel.ProfileViewModel @Composable @@ -81,6 +85,15 @@ fun StatsScreen( userGlobalState = userGlobalState.value, setUserGlobal = { selection -> userGlobalState.value = selection + }, + fetchTopArtists = { + viewModel.getUserTopArtists(it) + }, + fetchTopAlbums = { + viewModel.getUserTopAlbums(it) + }, + fetchTopSongs = { + viewModel.getUserTopSongs(it) } ) } @@ -132,6 +145,9 @@ fun StatsScreen( setStatsRange: (StatsRange) -> Unit, userGlobalState: UserGlobal, setUserGlobal: (UserGlobal) -> Unit, + fetchTopArtists: suspend (String?) -> Unit, + fetchTopAlbums: suspend (String?) -> Unit, + fetchTopSongs: suspend (String?) -> Unit, ) { val currentTabSelection: MutableState = remember { @@ -142,11 +158,54 @@ fun StatsScreen( mutableStateOf(true) } + val albumsCollapseState: MutableState = remember { + mutableStateOf(true) + } + + val songsCollapseState: MutableState = remember { + mutableStateOf(true) + } + + when(currentTabSelection.value){ + CategoryState.ARTISTS -> { + if(uiState.statsTabUIState.topArtists == null){ + LaunchedEffect(Unit) { + fetchTopArtists(username) + } + } + + } + CategoryState.ALBUMS -> { + if(uiState.statsTabUIState.topAlbums == null){ + LaunchedEffect(Unit) { + fetchTopAlbums(username) + } + } + } + CategoryState.SONGS -> { + if(uiState.statsTabUIState.topSongs == null){ + LaunchedEffect(Unit) { + fetchTopSongs(username) + } + } + } + } + val topArtists = when(artistsCollapseState.value){ true -> uiState.statsTabUIState.topArtists?.get(statsRangeState)?.payload?.artists?.take(5) ?: listOf() false -> uiState.statsTabUIState.topArtists?.get(statsRangeState)?.payload?.artists ?: listOf() } + val topAlbums = when(albumsCollapseState.value){ + true -> uiState.statsTabUIState.topAlbums?.get(statsRangeState)?.payload?.releases?.take(5) ?: listOf() + false -> uiState.statsTabUIState.topAlbums?.get(statsRangeState)?.payload?.releases ?: listOf() + } + + val topSongs = when(songsCollapseState.value){ + true -> uiState.statsTabUIState.topSongs?.get(statsRangeState)?.payload?.recordings?.take(5) ?: listOf() + false -> uiState.statsTabUIState.topSongs?.get(statsRangeState)?.payload?.recordings ?: listOf() + } + LazyColumn { item { RangeBar( @@ -166,7 +225,7 @@ fun StatsScreen( } item { Spacer(modifier = Modifier.height(10.dp)) - Box(){ + Box{ Column { Text("Listening activity", color = Color.White, style = MaterialTheme.typography.bodyLarge.copy(fontSize = 22.sp), modifier = Modifier.padding(start = 10.dp)) Spacer(modifier = Modifier.height(15.dp)) @@ -310,19 +369,74 @@ fun StatsScreen( Spacer(modifier = Modifier.width(10.dp)) } } - - topArtists.map { - topArtist -> - ArtistCard(artistName = topArtist.artistName, listenCount = topArtist.listenCount) { - + when(currentTabSelection.value){ + CategoryState.ARTISTS -> + if(uiState.statsTabUIState.isLoading){ + CircularProgressIndicator() + } + else{ + Column (horizontalAlignment = Alignment.CenterHorizontally) { + topArtists.map { + topArtist -> + ArtistCard(artistName = topArtist.artistName, listenCount = topArtist.listenCount){} + } + Spacer(modifier = Modifier.height(10.dp)) + if((uiState.statsTabUIState.topArtists?.size ?: 0) > 5){ + LoadMoreButton(state = artistsCollapseState.value) { + artistsCollapseState.value = !artistsCollapseState.value + } + } + + } + } + + CategoryState.ALBUMS -> + if(uiState.statsTabUIState.isLoading){ + CircularProgressIndicator( + color = lb_purple_night + ) + } + else{ + Column (horizontalAlignment = Alignment.CenterHorizontally) { + topAlbums.map { + topAlbum -> + ListenCardSmall(trackName = topAlbum.releaseName ?: "", artistName = topAlbum.artistName ?: "", coverArtUrl = getCoverArtUrl(topAlbum.caaReleaseMbid, topAlbum.caaId), modifier = Modifier.padding(top = 10.dp, bottom = 10.dp, end = 10.dp), color = app_bg_secondary_dark, titleColor = ListenBrainzTheme.colorScheme.followerChipSelected, subtitleColor = ListenBrainzTheme.colorScheme.listenText.copy(alpha = 0.7f)) { + + } + } + Spacer(modifier = Modifier.height(10.dp)) + if((uiState.statsTabUIState.topAlbums?.size ?: 0) > 5) { + LoadMoreButton(state = albumsCollapseState.value) { + albumsCollapseState.value = !albumsCollapseState.value + } + } + } + } + + CategoryState.SONGS -> { + if(uiState.statsTabUIState.isLoading){ + CircularProgressIndicator() + } + else{ + Column (horizontalAlignment = Alignment.CenterHorizontally) { + topSongs.map { + topSong -> + ListenCardSmall(trackName = topSong.trackName ?: "", artistName = topSong.artistName ?: "", coverArtUrl = getCoverArtUrl(topSong.caaReleaseMbid, topSong.caaId), modifier = Modifier.padding(top = 10.dp, bottom = 10.dp, end = 10.dp), color = app_bg_secondary_dark, titleColor = ListenBrainzTheme.colorScheme.followerChipSelected, subtitleColor = ListenBrainzTheme.colorScheme.listenText.copy(alpha = 0.7f),) { + + } + } + Spacer(modifier = Modifier.height(10.dp)) + if((uiState.statsTabUIState.topSongs?.size ?: 0) > 5) { + LoadMoreButton(state = songsCollapseState.value) { + songsCollapseState.value = !songsCollapseState.value + } + } + } + } } } - - } - } - } } diff --git a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt index d2c87497..8b09f7bd 100644 --- a/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/org/listenbrainz/android/viewmodel/ProfileViewModel.kt @@ -229,6 +229,16 @@ class ProfileViewModel @Inject constructor( Pair(UserGlobal.GLOBAL, StatsRange.ALL_TIME) to globalAllTimeListeningActivity, ) + val statsTabState = StatsTabUIState( + isLoading = false, + userListeningActivity = userListeningActivityMap, + ) + + statsStateFlow.emit(statsTabState) + } + + suspend fun getUserTopArtists(inputUsername: String?){ + statsStateFlow.value = statsStateFlow.value.copy(isLoading = true) val userTopArtistsThisWeek = userRepository.getTopArtists(inputUsername, rangeString = StatsRange.THIS_WEEK.apiIdenfier).data val userTopArtistsThisMonth = userRepository.getTopArtists(inputUsername, rangeString = StatsRange.THIS_MONTH.apiIdenfier).data val userTopArtistsThisYear = userRepository.getTopArtists(inputUsername, rangeString = StatsRange.THIS_YEAR.apiIdenfier).data @@ -246,12 +256,64 @@ class ProfileViewModel @Inject constructor( StatsRange.LAST_YEAR to userTopArtistsLastYear, StatsRange.ALL_TIME to userTopArtistsAllTime ) - val statsTabState = StatsTabUIState( + + statsStateFlow.value = statsStateFlow.value.copy( isLoading = false, - userListeningActivity = userListeningActivityMap, topArtists = topArtists ) - statsStateFlow.emit(statsTabState) + } + + + suspend fun getUserTopAlbums(inputUsername: String?){ + statsStateFlow.value = statsStateFlow.value.copy(isLoading = true) + val userTopAlbumsThisWeek = userRepository.getTopAlbums(inputUsername, rangeString = StatsRange.THIS_WEEK.apiIdenfier).data + val userTopAlbumsThisMonth = userRepository.getTopAlbums(inputUsername, rangeString = StatsRange.THIS_MONTH.apiIdenfier).data + val userTopAlbumsThisYear = userRepository.getTopAlbums(inputUsername, rangeString = StatsRange.THIS_YEAR.apiIdenfier).data + val userTopAlbumsLastWeek = userRepository.getTopAlbums(inputUsername, rangeString = StatsRange.LAST_WEEK.apiIdenfier).data + val userTopAlbumsLastMonth = userRepository.getTopAlbums(inputUsername, rangeString = StatsRange.LAST_MONTH.apiIdenfier).data + val userTopAlbumsLastYear = userRepository.getTopAlbums(inputUsername, rangeString = StatsRange.LAST_YEAR.apiIdenfier).data + val userTopAlbumsAllTime = userRepository.getTopAlbums(inputUsername, rangeString = StatsRange.ALL_TIME.apiIdenfier).data + + val topAlbums = mapOf( + StatsRange.THIS_WEEK to userTopAlbumsThisWeek, + StatsRange.THIS_MONTH to userTopAlbumsThisMonth, + StatsRange.THIS_YEAR to userTopAlbumsThisYear, + StatsRange.LAST_WEEK to userTopAlbumsLastWeek, + StatsRange.LAST_MONTH to userTopAlbumsLastMonth, + StatsRange.LAST_YEAR to userTopAlbumsLastYear, + StatsRange.ALL_TIME to userTopAlbumsAllTime + ) + + statsStateFlow.value = statsStateFlow.value.copy( + isLoading = false, + topAlbums = topAlbums + ) + } + + suspend fun getUserTopSongs(inputUsername: String?){ + statsStateFlow.value = statsStateFlow.value.copy(isLoading = true) + val userTopSongsThisWeek = userRepository.getTopSongs(inputUsername, rangeString = StatsRange.THIS_WEEK.apiIdenfier).data + val userTopSongsThisMonth = userRepository.getTopSongs(inputUsername, rangeString = StatsRange.THIS_MONTH.apiIdenfier).data + val userTopSongsThisYear = userRepository.getTopSongs(inputUsername, rangeString = StatsRange.THIS_YEAR.apiIdenfier).data + val userTopSongsLastWeek = userRepository.getTopSongs(inputUsername, rangeString = StatsRange.LAST_WEEK.apiIdenfier).data + val userTopSongsLastMonth = userRepository.getTopSongs(inputUsername, rangeString = StatsRange.LAST_MONTH.apiIdenfier).data + val userTopSongsLastYear = userRepository.getTopSongs(inputUsername, rangeString = StatsRange.LAST_YEAR.apiIdenfier).data + val userTopSongsAllTime = userRepository.getTopSongs(inputUsername, rangeString = StatsRange.ALL_TIME.apiIdenfier).data + + val topSongs = mapOf( + StatsRange.THIS_WEEK to userTopSongsThisWeek, + StatsRange.THIS_MONTH to userTopSongsThisMonth, + StatsRange.THIS_YEAR to userTopSongsThisYear, + StatsRange.LAST_WEEK to userTopSongsLastWeek, + StatsRange.LAST_MONTH to userTopSongsLastMonth, + StatsRange.LAST_YEAR to userTopSongsLastYear, + StatsRange.ALL_TIME to userTopSongsAllTime + ) + + statsStateFlow.value = statsStateFlow.value.copy( + isLoading = false, + topSongs = topSongs + ) } private suspend fun getUserTasteData(inputUsername: String?) {